diff --git a/bun.lockb b/bun.lockb index 8ccf0b2..bcac739 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/website/canvas/src/file-system.ts b/labs/persistence/file.ts similarity index 51% rename from website/canvas/src/file-system.ts rename to labs/persistence/file.ts index 35c5e8b..89be0c7 100644 --- a/website/canvas/src/file-system.ts +++ b/labs/persistence/file.ts @@ -1,4 +1,6 @@ -export class FileSaver { +import { KeyValueStore } from '@lib/indexeddb'; + +export class FilePicker { #id; #fileType; #fileExtension; @@ -85,7 +87,7 @@ export class FileSaver { types: [ { description: `${this.#fileType.toUpperCase()} document`, - accept: { [this.#mimeType]: [this.#fileExtension] }, + accept: { [this.#mimeType]: [this.#fileExtension] } as FilePickerAcceptType['accept'], }, ], }); @@ -99,11 +101,10 @@ export class FileSaver { this.#fileHandlerPromise = window .showOpenFilePicker({ id: this.#id, - suggestedName: `${this.#id}.${this.#fileType}`, types: [ { description: `${this.#fileType.toUpperCase()} document`, - accept: { [this.#mimeType]: [this.#fileExtension] }, + accept: { [this.#mimeType]: [this.#fileExtension] } as FilePickerAcceptType['accept'], }, ], }) @@ -114,92 +115,3 @@ export class FileSaver { return fileHandler; } } - -declare global { - var showSaveFilePicker: (args: any) => Promise; - var showOpenFilePicker: (args: any) => Promise; - - interface FileSystemHandle { - queryPermission: (args: any) => Promise; - requestPermission: (args: any) => Promise; - } -} - -class KeyValueStore { - #db: Promise; - #storeName; - - constructor(name = 'keyval-store') { - this.#storeName = name; - const request = indexedDB.open(name); - request.onupgradeneeded = () => request.result.createObjectStore(name); - - this.#db = this.#promisifyRequest(request); - } - - #promisifyRequest(transaction: IDBRequest) { - return new Promise((resolve, reject) => { - transaction.onsuccess = () => resolve(transaction.result); - transaction.onerror = () => reject(transaction.error); - }); - } - - #promisifyTransaction(transaction: IDBTransaction) { - return new Promise((resolve, reject) => { - transaction.oncomplete = () => resolve(); - transaction.onabort = transaction.onerror = () => reject(transaction.error); - }); - } - - #getStore(mode: 'readonly' | 'readwrite') { - return this.#db.then((db) => db.transaction(this.#storeName, mode).objectStore(this.#storeName)); - } - - get(key: IDBValidKey): Promise { - return this.#getStore('readonly').then((store) => this.#promisifyRequest(store.get(key))); - } - - set(key: IDBValidKey, value: Data) { - return this.#getStore('readwrite').then((store) => { - store.put(value, key); - return this.#promisifyTransaction(store.transaction); - }); - } - - setMany(entries: [IDBValidKey, Data][]) { - return this.#getStore('readwrite').then((store) => { - entries.forEach((entry) => store.put(entry[1], entry[0])); - return this.#promisifyTransaction(store.transaction); - }); - } - - delete(key: IDBValidKey) { - return this.#getStore('readwrite').then((store) => { - store.delete(key); - return this.#promisifyTransaction(store.transaction); - }); - } - - clear() { - return this.#getStore('readwrite').then((store) => { - store.clear(); - return this.#promisifyTransaction(store.transaction); - }); - } - - keys() { - return this.#getStore('readwrite').then((store) => this.#promisifyRequest(store.getAllKeys())); - } - - values(): Promise { - return this.#getStore('readwrite').then((store) => this.#promisifyRequest(store.getAll())); - } - - entries(): Promise<[IDBValidKey, Data][]> { - return this.#getStore('readwrite').then((store) => - Promise.all([this.#promisifyRequest(store.getAllKeys()), this.#promisifyRequest(store.getAll())]).then( - ([keys, values]) => keys.map((key, i) => [key, values[i]]) - ) - ); - } -} diff --git a/lib/index.ts b/lib/index.ts index 494b92a..deed108 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -29,5 +29,7 @@ export * from './EffectIntegrator'; // WebGL utilities export * from './webgl'; +export * from './indexeddb'; + // Experimental features export * from './Experimental'; diff --git a/lib/indexeddb.ts b/lib/indexeddb.ts new file mode 100644 index 0000000..bf5e389 --- /dev/null +++ b/lib/indexeddb.ts @@ -0,0 +1,78 @@ +export class KeyValueStore { + #db: Promise; + #storeName; + + constructor(name = 'keyval-store') { + this.#storeName = name; + const request = indexedDB.open(name); + request.onupgradeneeded = () => request.result.createObjectStore(name); + + this.#db = this.#promisifyRequest(request); + } + + #promisifyRequest(transaction: IDBRequest) { + return new Promise((resolve, reject) => { + transaction.onsuccess = () => resolve(transaction.result); + transaction.onerror = () => reject(transaction.error); + }); + } + + #promisifyTransaction(transaction: IDBTransaction) { + return new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onabort = transaction.onerror = () => reject(transaction.error); + }); + } + + #getStore(mode: 'readonly' | 'readwrite') { + return this.#db.then((db) => db.transaction(this.#storeName, mode).objectStore(this.#storeName)); + } + + get(key: IDBValidKey): Promise { + return this.#getStore('readonly').then((store) => this.#promisifyRequest(store.get(key))); + } + + set(key: IDBValidKey, value: Data) { + return this.#getStore('readwrite').then((store) => { + store.put(value, key); + return this.#promisifyTransaction(store.transaction); + }); + } + + setMany(entries: [IDBValidKey, Data][]) { + return this.#getStore('readwrite').then((store) => { + entries.forEach((entry) => store.put(entry[1], entry[0])); + return this.#promisifyTransaction(store.transaction); + }); + } + + delete(key: IDBValidKey) { + return this.#getStore('readwrite').then((store) => { + store.delete(key); + return this.#promisifyTransaction(store.transaction); + }); + } + + clear() { + return this.#getStore('readwrite').then((store) => { + store.clear(); + return this.#promisifyTransaction(store.transaction); + }); + } + + keys() { + return this.#getStore('readwrite').then((store) => this.#promisifyRequest(store.getAllKeys())); + } + + values(): Promise { + return this.#getStore('readwrite').then((store) => this.#promisifyRequest(store.getAll())); + } + + entries(): Promise<[IDBValidKey, Data][]> { + return this.#getStore('readwrite').then((store) => + Promise.all([this.#promisifyRequest(store.getAllKeys()), this.#promisifyRequest(store.getAll())]).then( + ([keys, values]) => keys.map((key, i) => [key, values[i]]), + ), + ); + } +} diff --git a/package.json b/package.json index 18f5732..e7a9266 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "devDependencies": { "@types/leaflet": "^1.9.14", "@types/node": "^22.10.1", + "@types/wicg-file-system-access": "^2023.10.5", "@webgpu/types": "^0.1.51", "bun-types": "^1.1.38", "mitata": "^1.0.20", diff --git a/tsconfig.json b/tsconfig.json index 8e3085c..d48a893 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "skipLibCheck": true, "noUnusedLocals": false, "lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"], - "types": ["@webgpu/types", "@types/node", "bun-types"], + "types": ["@webgpu/types", "@types/node", "@types/wicg-file-system-access", "bun-types"], "baseUrl": ".", "paths": { "@lib": ["lib"],