Skip to content

Instantly share code, notes, and snippets.

@maierfelix
Last active June 13, 2023 12:50
Show Gist options
  • Select an option

  • Save maierfelix/e7492160f2f8f926b9ab9d54c6b397b1 to your computer and use it in GitHub Desktop.

Select an option

Save maierfelix/e7492160f2f8f926b9ab9d54c6b397b1 to your computer and use it in GitHub Desktop.
Simple IDB-based virtual file system for binary files. Fast and with support for sorting
const fs = await new VirtualFileSystem({name: "my-project"}).onCreate();
const worldCount = await fs.readCount("worlds");
console.log(`Saved worlds: '${worldCount}'`);
const insertId = await fs.insert("worlds", "hello", inputWorld);
console.log(`Inserted world at '${insertId}'`);
const outputWorld = await fs.readById("worlds", insertId);
console.log(`Output world '${insertId}':`, outputWorld);
console.log("Worlds with name 'hello':", await fs.readByName("worlds", "hello"));
console.log("All Worlds - not sorted:", await fs.readAll("worlds", false));
console.log("All Worlds - sorted:", await fs.readAll("worlds", true));
/**
* Interface representing a key entry
*/
export interface IKeyEntry {
/**
* The id of the entry
*/
id: number;
/**
* The name of the entry
*/
name: string;
/**
* The sorting index of the entry
*/
sort: number;
/**
* The binary data blob of the entry
*/
data: Blob;
}
/**
* Interface representing a binary data entry
*/
export interface IBinaryDataEntry {
/**
* The name of the entry
*/
name: string;
/**
* The sorting index of the entry
*/
sort: number;
/**
* The binary data of the entry
*/
data: Uint8Array;
}
/**
* The options to construct a virtual file system
*/
export interface IVirtualFileSystemOptions {
/**
* Name of the database
*/
name: string;
}
/**
* The default options used when constructing an editor
*/
export const DEFAULT_OPTIONS_EDITOR: Required<IVirtualFileSystemOptions> = Object.freeze({
name: null,
});
/**
* A class that represents a virtual file system
*/
export class VirtualFileSystem {
/**
* The name of the virtual file system
*/
private _name: string = null;
/**
* IDB handle
*/
private _handle: IDBDatabase = null;
/**
* The constructor of this editor
* @param options - The options to construct the editor
*/
public constructor(options: IVirtualFileSystemOptions) {
// Normalize options
const _options = Object.assign({}, DEFAULT_OPTIONS_EDITOR, options);
this._name = _options.name;
}
/**
* Returns the name of the fs
*/
public getName(): string {return this._name;}
/**
* Returns the IDB handle of the fs
*/
public getHandle(): IDBDatabase {return this._handle;}
/**
* Create the fs
*/
public onCreate(): Promise<VirtualFileSystem> {
return new Promise(resolve => {
// Create layout
const request = indexedDB.open(this.getName(), 1);
request.onupgradeneeded = (e): void => {
this._handle = (e.target as any).result || null;
const LAYOUT_NAMES = ["worlds"];
for (let ii = 0; ii < LAYOUT_NAMES.length; ++ii) {
const layoutName = LAYOUT_NAMES[ii];
// Create layout
const layout = this._handle.createObjectStore(layoutName, {keyPath: "id", autoIncrement: true});
layout.createIndex("name", "name", {unique: false});
layout.createIndex("data", "data", {unique: false});
layout.createIndex("sort", "sort", {unique: false});
layout.createIndex("category", "category", {unique: false});
}
};
request.onerror = (e): void => {
console.error(e);
};
request.onsuccess = (): void => {
resolve(this);
};
});
}
/**
* Delete all data in the fs
*/
public onDelete(): void {
indexedDB.deleteDatabase(this.getName());
}
/**
* Insert a new entry into the database
* @param table - The name of the table to insert into
* @param name - The name of the entry to insert
* @param data - The data to insert
*/
public insert(table: string, name: string, data: Uint8Array): Promise<number> {
return new Promise(resolve => {
const request = indexedDB.open(this.getName());
request.onsuccess = (e): void => {
const handle = (e.target as any).result as IDBDatabase;
const tx = handle.transaction(table, "readwrite").objectStore(table);
const entry: any = {name, data: new Blob([data]), sort: -1, category: ""};
const query = tx.add(entry);
query.onsuccess = (e): void => {
const target = e.target as any;
if (target && Number.isInteger(target.result)) {
// The auto increment id of the entry we inserted
const id = target.result;
// Sort id is currently -1, update it
const request = indexedDB.open(this.getName());
request.onsuccess = (e): void => {
const handle = (e.target as any).result as IDBDatabase;
const tx = handle.transaction(table, "readwrite").objectStore(table);
// Update sort id
entry.id = id;
entry.sort = id;
const query = tx.put(entry);
query.onsuccess = (e): void => {
const target = e.target as any;
if (target && Number.isInteger(target.result)) {
resolve(id);
} else {
resolve(-1);
}
};
query.onerror = (): void => {
resolve(-1);
};
};
} else {
resolve(-1);
}
};
query.onerror = (): void => {
resolve(-1);
};
};
});
}
/**
* Moves an existing entry within the database
* @param table - The name of the table to insert at
* @param updateKeys - The new keys to use
*/
public updateKeys(table: string, updateKeys: number[]): Promise<boolean> {
return new Promise(resolve => {
this.readKeys(table, false).then(currentKeys => {
(async(): Promise<void> => {
for (let ii = 0; ii < currentKeys.length; ++ii) {
const currentKey = currentKeys[ii].id;
const updateKey = updateKeys[ii];
const isSuccess = await this.updateSortAtId(table, currentKey, updateKey);
// Abort if failed
if (!isSuccess) {
resolve(false);
return;
}
}
resolve(true);
})();
});
});
}
/**
* Write a new entry into the database
* @param table - The name of the table to write into
* @param id - The id of the entry to write
* @param name - The name of the entry to write
* @param data - The data to write
* @param sort - An optional sort id to write
*/
public writeAtId(table: string, id: number, name: string, data: Uint8Array, sort: number = -1): Promise<number> {
return new Promise(resolve => {
const request = indexedDB.open(this.getName());
request.onsuccess = (e): void => {
const handle = (e.target as any).result as IDBDatabase;
const tx = handle.transaction(table, "readwrite").objectStore(table);
const blob = new Blob([data]);
const query = tx.put({id, name, data: blob, sort: sort !== -1 ? sort : id, category: ""});
query.onsuccess = (e): void => {
const target = e.target as any;
if (target && Number.isInteger(target.result)) {
const id = target.result;
resolve(id);
} else {
resolve(-1);
}
};
query.onerror = (): void => {
resolve(-1);
};
};
});
}
/**
* Read an entry from the database, based on the provided id
* @param table - The name of the table to read from
* @param id - The id of the entry to read
* @param raw - Instead of parsing the data, returns the raw data of the entry
*/
public readById(table: string, id: number, raw: boolean = false): Promise<IBinaryDataEntry> {
return new Promise(resolve => {
const request = indexedDB.open(this.getName());
request.onsuccess = (e): void => {
const handle = (e.target as any).result as IDBDatabase;
const tx = handle.transaction(table, "readonly").objectStore(table);
const query = tx.get(id);
query.onsuccess = (e): void => {
const target = e.target as any;
if (target && target.result && (target.result.data instanceof Blob)) {
// Raw data
if (raw) {
resolve(target.result);
}
// Parse the data
else {
target.result.data.arrayBuffer().then((buffer: ArrayBuffer) => {
resolve({
name: target.result.name,
sort: target.result.sort,
data: new Uint8Array(buffer),
});
});
}
} else {
resolve(null);
}
};
query.onerror = (): void => {
resolve(null);
};
};
});
}
/**
* Read all entries within the table from the database
* @param table - The table to read from
* @param stored - Optionally sort the results by their sort id
*/
public readAll(table: string, sorted: boolean = false): Promise<IBinaryDataEntry[]> {
return new Promise(resolve => {
this.readKeys(table, sorted).then(async(keys): Promise<void> => {
const out: IBinaryDataEntry[] = [];
for (let ii = 0; ii < keys.length; ++ii) {
out.push(await this.readById(table, keys[ii].id));
}
resolve(out);
});
});
}
/**
* Read all entries with the given name within the table from the database
* @param table - The table to read from
* @param name - The name to query
* @param stored - Optionally sort the results by their sort id
*/
public readByName(table: string, name: string, sorted: boolean = false): Promise<IBinaryDataEntry[]> {
return new Promise(resolve => {
this.readKeys(table, sorted).then(async(entries): Promise<void> => {
const matches = [];
for (let ii = 0; ii < entries.length; ++ii) {
const entry = entries[ii];
if (entry.name === name) {
const data = await this.readById(table, entry.id);
matches.push(data);
}
}
resolve(matches);
});
});
}
/**
* Delete all entries within the table from the database
* @param table - The table to delete at
*/
public deleteAll(table: string): Promise<void> {
return new Promise(resolve => {
const request = indexedDB.open(this.getName());
request.onsuccess = (e): void => {
const handle = (e.target as any).result as IDBDatabase;
const tx = handle.transaction(table, "readwrite").objectStore(table);
const query = tx.clear();
query.onsuccess = (): void => {
resolve();
};
query.onerror = (): void => {
resolve();
};
};
});
}
/**
* Returns a list of all keys within a table from the database
* @param table - The table to read the keys from
*/
public readCount(table: string): Promise<number> {
return new Promise(resolve => {
const request = indexedDB.open(this.getName());
request.onsuccess = (e): void => {
const handle = (e.target as any).result as IDBDatabase;
const tx = handle.transaction(table, "readonly").objectStore(table);
const query = tx.count();
query.onsuccess = (e): void => {
const target = e.target as any;
resolve(target.result);
};
query.onerror = (): void => {
resolve(-1);
};
};
});
}
/**
* Delete an entry from the database, based on the provided id
* @param table - The name of the table to delete from
* @param id - The id of the entry to delete
*/
public deleteById(table: string, id: number): Promise<boolean> {
return new Promise(resolve => {
const request = indexedDB.open(this.getName());
request.onsuccess = (e): void => {
const handle = (e.target as any).result as IDBDatabase;
const tx = handle.transaction(table, "readwrite").objectStore(table);
const query = tx.delete(id);
query.onsuccess = (): void => {
resolve(true);
};
query.onerror = (): void => {
resolve(false);
};
};
});
}
/**
* Returns a list of all keys within a table from the database
* @param table - The table to read the keys from
* @param stored - Optionally sort the keys by their sort id
*/
public readKeys(table: string, sorted: boolean = false): Promise<IKeyEntry[]> {
return new Promise(resolve => {
const request = indexedDB.open(this.getName());
request.onsuccess = (e): void => {
const handle = (e.target as any).result as IDBDatabase;
const tx = handle.transaction(table, "readonly").objectStore(table);
const query = tx.getAll();
query.onsuccess = (e): void => {
const target = e.target as any;
if (target.result) {
const data = target.result as IKeyEntry[];
if (sorted) {
const sorts = data.map(v => v.sort);
const ids = data.map(v => v.id);
const sortedIds = ids.sort((a: number, b: number) => sorts.indexOf(a) - sorts.indexOf(b));
const sorted = data.map((_, index) => data.find(v => v.id === sortedIds[index]));
resolve(sorted);
} else {
resolve(data);
}
} else {
resolve([]);
}
};
query.onerror = (): void => {
resolve([]);
};
};
});
}
/**
* Update the sort id of an existing entry in the database
* @param table - The name of the table to update in
* @param id - The id of the entry to update
* @param sort - The sort id of the entry to write
*/
public updateSortAtId(table: string, id: number, sort: number): Promise<boolean> {
return new Promise(resolve => {
const request = indexedDB.open(this.getName());
request.onsuccess = (e): void => {
const handle = (e.target as any).result as IDBDatabase;
const tx = handle.transaction(table, "readonly").objectStore(table);
const query = tx.get(id);
query.onsuccess = (e): void => {
const target = e.target as any;
if (target && target.result) {
const current = target.result;
const request = indexedDB.open(this.getName());
request.onsuccess = (e): void => {
const handle = (e.target as any).result as IDBDatabase;
const tx = handle.transaction(table, "readwrite").objectStore(table);
// Update sort field with new value
current.sort = sort;
// Write new data
const query = tx.put(current);
query.onsuccess = (e): void => {
const target = e.target as any;
if (target && Number.isInteger(target.result)) {
resolve(true);
} else {
resolve(false);
}
};
query.onerror = (): void => {
resolve(false);
};
};
} else {
resolve(false);
}
};
query.onerror = (): void => {
resolve(false);
};
};
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment