From b1e340bb4db75166ad6708151f76d8e2829da70f Mon Sep 17 00:00:00 2001 From: hasundue Date: Mon, 22 Apr 2024 14:15:22 +0900 Subject: [PATCH 1/3] fix(deno/idb): abandon ambient type declaration --- deno/idb/databases.ts | 2 - deno/idb/mod.ts | 14 +---- deno/idb/requests.ts | 7 --- deno/idb_test.ts | 130 ++++++++++++++++++++++++++++++++++-------- 4 files changed, 108 insertions(+), 45 deletions(-) diff --git a/deno/idb/databases.ts b/deno/idb/databases.ts index cba2986..882b784 100644 --- a/deno/idb/databases.ts +++ b/deno/idb/databases.ts @@ -39,7 +39,6 @@ type NormalTransactionMode = "readonly" | "readwrite"; export class _IDBDatabase extends EventTarget implements IDBDatabaseInVersionChange { - readonly _key: string[]; _transaction: _IDBTransaction<"versionchange"> | null = null; constructor( @@ -57,7 +56,6 @@ export class _IDBDatabase extends EventTarget this.onversionchange?.(event); } }); - this._key = ["databases", name]; } createObjectStore( diff --git a/deno/idb/mod.ts b/deno/idb/mod.ts index 5f790b1..3409814 100644 --- a/deno/idb/mod.ts +++ b/deno/idb/mod.ts @@ -17,14 +17,14 @@ export type { IDBOpenDBRequest, IDBRequest } from "./requests.ts"; export type { IDBObjectStore } from "./stores.ts"; export type { IDBTransaction } from "./transactions.ts"; -interface IDBFactory { +export interface IDBFactory { open(name: string, version?: number): IDBOpenDBRequest; cmp(a: unknown, b: unknown): -1 | 0 | 1; databases(): Promise; deleteDatabase(name: string): IDBOpenDBRequest; } -const indexedDB: IDBFactory = { +export const indexedDB: IDBFactory = { open( name: string, version: number = 1, @@ -82,16 +82,6 @@ const indexedDB: IDBFactory = { }, }; -// FIXME: Can we avoid writing this twice? -declare global { - interface IDBFactory { - open(name: string, version?: number): IDBOpenDBRequest; - cmp(a: unknown, b: unknown): -1 | 0 | 1; - databases(): Promise; - deleteDatabase(name: string): IDBOpenDBRequest; - } -} - if (self.indexedDB === undefined && self.Deno.Kv) { console.warn("Using an experimental IndexedDB polyfill with Deno KV"); // @ts-ignore We type IDBFactory better diff --git a/deno/idb/requests.ts b/deno/idb/requests.ts index 996f892..619dc37 100644 --- a/deno/idb/requests.ts +++ b/deno/idb/requests.ts @@ -104,10 +104,3 @@ export class _IDBOpenDBRequest< onblocked: VersionChangeEventHandler | null = null; onupgradeneeded: VersionChangeEventHandler | null = null; } - -export interface IDBFactory { - open(name: string, version?: number): IDBOpenDBRequest; - cmp(a: unknown, b: unknown): -1 | 0 | 1; - databases(): Promise; - deleteDatabase(name: string): IDBOpenDBRequest; -} diff --git a/deno/idb_test.ts b/deno/idb_test.ts index e6bd3ff..a7e727f 100644 --- a/deno/idb_test.ts +++ b/deno/idb_test.ts @@ -13,6 +13,7 @@ import { it, } from "@std/testing/bdd"; import type { + IDBFactory, IDBDatabase, IDBIndex, IDBObjectStore, @@ -21,6 +22,12 @@ import type { } from "./idb/mod.ts"; import "./idb/mod.ts"; +declare global { + interface Window { + indexedDB: IDBFactory; + } +} + function ensure(target: EventTarget, event: string): Promise { return new Promise((resolve) => { target.addEventListener(event, resolve, { once: true }); @@ -126,10 +133,9 @@ describe("IDBDatabase", () => { }); }); -describe('IDBObjectStore<"readonly">', () => { +describe("IDBObjectStore", () => { const name = crypto.randomUUID(); - let db: IDBDatabase; - let store: IDBObjectStore<"readonly">; + let store: IDBObjectStore; beforeAll(async () => { const request = self.indexedDB.open(name); @@ -140,29 +146,54 @@ describe('IDBObjectStore<"readonly">', () => { ); }); await ensure(request, "success"); - db = request.result; - store = db.transaction("events").objectStore("events"); + store = request.result.transaction("events").objectStore("events"); }); afterAll(async () => { - using kv = await Deno.openKv(); - await kv.delete(["databases", name]); + const request = self.indexedDB.deleteDatabase(name); + await ensure(request, "success"); }); - it("should have a name", () => { + it("should have `name` property", () => { assertEquals(store.name, "events"); }); - it("should have the keyPath property", () => { + it("should have `keyPath` property", () => { assertEquals(store.keyPath, "id"); }); - it("should have the autoIncrement property", () => { + it("should have `autoIncrement` property", () => { assertEquals(store.autoIncrement, false); }); +}); - it("should have a transaction", () => { - assertEquals(store.transaction.db, db); +describe('IDBObjectStore<"readonly">', () => { + const name = crypto.randomUUID(); + let store: IDBObjectStore<"readonly">; + + beforeAll(async () => { + const request = self.indexedDB.open(name); + await new Promise((resolve) => { + request.onupgradeneeded = (event) => + resolve( + event.target.result.createObjectStore("events", { keyPath: "id" }), + ); + }); + await ensure(request, "success"); + store = request.result.transaction("events", "readonly").objectStore( + "events", + ); + }); + + afterAll(async () => { + const request = self.indexedDB.deleteDatabase(name); + await ensure(request, "success"); + }); + + describe("transaction", () => { + it("should be a readonly transaction", () => { + assertEquals(store.transaction.mode, "readonly"); + }); }); describe("add", () => { @@ -184,20 +215,22 @@ describe('IDBObjectStore<"readonly">', () => { }); }); -describe('IDBObjectStore<"versionchange">', () => { +describe('IDBObjectStore<"readwrite">', () => { const name = crypto.randomUUID(); - let store: IDBObjectStore<"versionchange">; + let store: IDBObjectStore<"readwrite">; beforeAll(async () => { const request = self.indexedDB.open(name); - await new Promise((resolve) => { - request.onupgradeneeded = (event) => { - const db = event.target.result; - store = db.createObjectStore("events", { keyPath: "id" }); - resolve(); - }; + await new Promise((resolve) => { + request.onupgradeneeded = (event) => + resolve( + event.target.result.createObjectStore("events", { keyPath: "id" }), + ); }); await ensure(request, "success"); + store = request.result.transaction("events", "readwrite").objectStore( + "events", + ); }); afterAll(async () => { @@ -205,6 +238,12 @@ describe('IDBObjectStore<"versionchange">', () => { await ensure(request, "success"); }); + describe("transaction", () => { + it("should be a readwrite transaction", () => { + assertEquals(store.transaction.mode, "readwrite"); + }); + }); + describe("add", () => { it("should add an object", async () => { const object = { id: 1, pubkey: "pubkey" }; @@ -218,6 +257,49 @@ describe('IDBObjectStore<"versionchange">', () => { }); }); + describe("createIndex", () => { + it("should throw InvalidStateError", () => { + assertThrows(() => + // @ts-expect-error createIndex is not available in a normal transaction + store.createIndex("pubkey", "pubkey") + ); + }); + }); +}); + +describe('IDBObjectStore<"versionchange">', () => { + const name = crypto.randomUUID(); + let store: IDBObjectStore<"versionchange">; + + beforeAll(async () => { + const request = self.indexedDB.open(name); + await new Promise((resolve) => { + request.onupgradeneeded = (event) => { + const db = event.target.result; + store = db.createObjectStore("events", { keyPath: "id" }); + resolve(); + }; + }); + await ensure(request, "success"); + }); + + afterAll(async () => { + const request = self.indexedDB.deleteDatabase(name); + await ensure(request, "success"); + }); + + describe("transaction", () => { + it("should be a versionchange transaction", () => { + assertEquals(store.transaction.mode, "versionchange"); + }); + }); + + describe("add", () => { + it("should be available", () => { + assert(typeof store.add === "function"); + }); + }); + describe("createIndex", () => { it("should create an index", () => { const index = store.createIndex("pubkey", "pubkey"); @@ -258,20 +340,20 @@ describe("IDBTransaction", () => { transaction = db.transaction("events"); }); - it("should have the `db` property", () => { + it("should have `db` property", () => { assertEquals(transaction.db, db); }); - it("should have the `mode` property", () => { + it("should have `mode` property", () => { assertEquals(transaction.mode, "readonly"); }); - it("should have the `objectStoreNames` property", () => { + it("should have `objectStoreNames` property", () => { assertEquals(transaction.objectStoreNames.length, 1); assertEquals(transaction.objectStoreNames[0], "events"); }); - it("should have the `objectStore` method", () => { + it("should have `objectStore` method", () => { const store = transaction.objectStore("events"); assertEquals(store.name, "events"); }); From 47823508f193b9caad9609195cde7129cfcae50d Mon Sep 17 00:00:00 2001 From: hasundue Date: Mon, 22 Apr 2024 15:42:37 +0900 Subject: [PATCH 2/3] feat(deno/idb): implement `IDBObjectStore.get` --- deno/idb/mod.ts | 5 ++--- deno/idb/requests.ts | 6 +++--- deno/idb/stores.ts | 29 ++++++++++++++++++++++------- deno/idb/transactions.ts | 34 ++++++++++++++++++++++++++++++++++ deno/idb_test.ts | 35 ++++++++++++++++++++++++++--------- 5 files changed, 87 insertions(+), 22 deletions(-) diff --git a/deno/idb/mod.ts b/deno/idb/mod.ts index 3409814..6176a86 100644 --- a/deno/idb/mod.ts +++ b/deno/idb/mod.ts @@ -24,13 +24,14 @@ export interface IDBFactory { deleteDatabase(name: string): IDBOpenDBRequest; } +const kv = await Deno.openKv(); + export const indexedDB: IDBFactory = { open( name: string, version: number = 1, ): IDBOpenDBRequest { return new _IDBOpenDBRequest(async function () { - using kv = await Deno.openKv(); // Check if the database already exists and is up to date. const existed = await kv.get($.database(name)); if (existed.value?.version && existed.value.version >= version) { @@ -68,14 +69,12 @@ export const indexedDB: IDBFactory = { }, async databases(): Promise { - using kv = await Deno.openKv(); const iter = kv.list({ prefix: $.database.prefix }); return (await Array.fromAsync(iter)).map((it) => it.value); }, deleteDatabase(name: string): IDBOpenDBRequest { return new _IDBOpenDBRequest(async () => { - using kv = await Deno.openKv(); await kv.delete($.database(name)); return undefined; }) as IDBOpenDBRequest; diff --git a/deno/idb/requests.ts b/deno/idb/requests.ts index 619dc37..7d2f9c7 100644 --- a/deno/idb/requests.ts +++ b/deno/idb/requests.ts @@ -8,7 +8,7 @@ import { IDBObjectStore } from "./stores.ts"; import { AnyIDBTransaction, IDBTransaction } from "./transactions.ts"; export interface IDBRequest< - Result, + Result = unknown, Transaction extends AnyIDBTransaction | null = AnyIDBTransaction | null, > extends EventTarget { readonly error: DOMException | null; @@ -21,7 +21,7 @@ export interface IDBRequest< onsuccess: EventHandler | null; } -export class _IDBRequest extends EventTarget +export class _IDBRequest extends EventTarget implements IDBRequest { constructor( readonly source: IDBRequestSource | null, @@ -65,7 +65,7 @@ export class _IDBRequest extends EventTarget onsuccess: EventHandler | null = null; } -type IDBRequestSource = IDBObjectStore | IDBIndex | IDBCursor; +export type IDBRequestSource = IDBObjectStore | IDBIndex | IDBCursor; export interface IDBOpenDBRequest< Result extends IDBDatabase | undefined = IDBDatabase, diff --git a/deno/idb/stores.ts b/deno/idb/stores.ts index b89b07a..19b2032 100644 --- a/deno/idb/stores.ts +++ b/deno/idb/stores.ts @@ -1,14 +1,11 @@ import { match, placeholder as _, RegularPlaceholder } from "@core/match"; import { associateWith } from "@std/collections/associate-with"; +import { _IDBDatabase } from "./databases.ts"; import { _IDBIndex, IDBIndex } from "./indexes.ts"; import { IDBValidKey, isIDBKey, KvKeyFactoryRecord as $ } from "./keys.ts"; import { _IDBRequest, IDBRequest } from "./requests.ts"; -import { - _IDBTransaction, - AnyIDBTransaction, - IDBTransaction, -} from "./transactions.ts"; +import { _IDBTransaction, IDBTransaction } from "./transactions.ts"; export interface IDBObjectStore< M extends IDBTransactionMode = IDBTransactionMode, @@ -28,6 +25,8 @@ export interface IDBObjectStore< keyPath: string | string[], options?: IDBIndexParameters, ) => IDBIndex; + + get(key: IDBValidKey): IDBRequest; } /** @@ -47,6 +46,7 @@ export function _IDBObjectStore( transaction, autoIncrement, + // @ts-ignore: TS doesn't understand the conditional type add: transaction.mode === "readonly" ? undefined : ( value: unknown, key?: IDBValidKey, @@ -59,9 +59,8 @@ export function _IDBObjectStore( } const result = ensureKey(options, value, key); const parts = Array.isArray(result) ? result : [result]; - return new _IDBRequest( + return transaction._createRequest( store, - transaction as AnyIDBTransaction, () => { (transaction as _IDBTransaction)._atomic.set( $.value(transaction.db.name, name, ...parts), @@ -72,6 +71,7 @@ export function _IDBObjectStore( ); }, + // @ts-ignore: TS doesn't understand the conditional type createIndex: transaction.mode !== "versionchange" ? undefined : ( indexName: string, keyPath: string | string[], @@ -85,6 +85,21 @@ export function _IDBObjectStore( } return new _IDBIndex(indexName, keyPath, options); }, + + get(key: IDBValidKey): IDBRequest { + const keyArray = Array.isArray(key) ? key : [key]; + const request = transaction._createRequest( + store, + async () => { + const kv = (this.transaction.db as _IDBDatabase)._kv; + const result = await kv.get( + $.value(transaction.db.name, name, ...keyArray), + ); + return result.versionstamp ? result.value : undefined; + }, + ); + return request; + }, }; return store; } diff --git a/deno/idb/transactions.ts b/deno/idb/transactions.ts index 81b6f67..c4399e0 100644 --- a/deno/idb/transactions.ts +++ b/deno/idb/transactions.ts @@ -2,6 +2,7 @@ import { DOMStringList } from "@lophus/lib/legacy"; import { EventHandler } from "./events.ts"; import { _IDBDatabase, IDBDatabase } from "./databases.ts"; import { _IDBObjectStore, IDBObjectStore } from "./stores.ts"; +import { _IDBRequest, IDBRequestSource } from "./requests.ts"; export interface IDBTransaction< Mode extends IDBTransactionMode = IDBTransactionMode, @@ -62,4 +63,37 @@ export class _IDBTransaction } return _IDBObjectStore(this, name, options); } + + /** + * An internal method to add a request to the transaction. + * @internal + */ + _createRequest( + source: IDBRequestSource | null, + operation: () => Promise, + ): _IDBRequest { + if (this._completed) { + throw new DOMException( + "The transaction has already completed.", + "TransactionInactiveError", + ); + } + const request = new _IDBRequest( + source, + this as AnyIDBTransaction, + operation, + ); + this._requests.add(request); + request.addEventListener("success", async () => { + this._requests.delete(request); + if (this._requests.size === 0) { + await this._atomic.commit(); + this._completed = true; + this.dispatchEvent(new Event("complete")); + } + }); + return request; + } + readonly _requests: Set<_IDBRequest> = new Set(); + _completed: boolean = false; } diff --git a/deno/idb_test.ts b/deno/idb_test.ts index a7e727f..7f13278 100644 --- a/deno/idb_test.ts +++ b/deno/idb_test.ts @@ -13,8 +13,8 @@ import { it, } from "@std/testing/bdd"; import type { - IDBFactory, IDBDatabase, + IDBFactory, IDBIndex, IDBObjectStore, IDBOpenDBRequest, @@ -173,11 +173,13 @@ describe('IDBObjectStore<"readonly">', () => { beforeAll(async () => { const request = self.indexedDB.open(name); - await new Promise((resolve) => { - request.onupgradeneeded = (event) => - resolve( - event.target.result.createObjectStore("events", { keyPath: "id" }), - ); + await new Promise((resolve) => { + request.onupgradeneeded = (event) => { + const db = event.target.result; + const store = db.createObjectStore("events", { keyPath: "id" }); + store.add({ id: 1, pubkey: "pubkey" }); + resolve(); + }; }); await ensure(request, "success"); store = request.result.transaction("events", "readonly").objectStore( @@ -213,6 +215,14 @@ describe('IDBObjectStore<"readonly">', () => { ); }); }); + + describe("get", () => { + it("should get an object", async () => { + const request = store.get(1); + await ensure(store.transaction, "complete"); + assertEquals(request.result, { id: 1, pubkey: "pubkey" }); + }); + }); }); describe('IDBObjectStore<"readwrite">', () => { @@ -228,9 +238,9 @@ describe('IDBObjectStore<"readwrite">', () => { ); }); await ensure(request, "success"); - store = request.result.transaction("events", "readwrite").objectStore( - "events", - ); + store = request.result + .transaction("events", "readwrite") + .objectStore("events"); }); afterAll(async () => { @@ -265,6 +275,13 @@ describe('IDBObjectStore<"readwrite">', () => { ); }); }); + + describe("get", () => { + it("should throw a TransactionInactiveError once the transaction is completed", async () => { + await ensure(store.transaction, "complete"); + assertThrows(() => store.get(1)); + }); + }); }); describe('IDBObjectStore<"versionchange">', () => { From 74954034d2f40b0f2c205c698277c81adae66e68 Mon Sep 17 00:00:00 2001 From: hasundue Date: Mon, 22 Apr 2024 15:49:39 +0900 Subject: [PATCH 3/3] refactor(deno/idb): do not replace self.indexedDB --- app/common/nostr.ts | 26 ++--------- deno.lock | 32 ++----------- deno/idb/mod.ts | 110 +++++++++++++++++++++----------------------- deno/idb_test.ts | 56 +++++++++++----------- std/stores.ts | 25 +++++++--- std/stores_test.ts | 6 +-- 6 files changed, 108 insertions(+), 147 deletions(-) diff --git a/app/common/nostr.ts b/app/common/nostr.ts index 24b7469..e655077 100644 --- a/app/common/nostr.ts +++ b/app/common/nostr.ts @@ -4,7 +4,7 @@ import { EventFilter, EventKind, NostrEvent } from "@lophus/nips"; export class Relay extends WithPool(_Relay) implements RelayLike {} -interface EventSource extends Pick { +export interface EventSource extends Pick { list( filter: EventFilter, ): AsyncIterable>; @@ -13,7 +13,7 @@ interface EventSource extends Pick { ): Promise | undefined>; } -interface EventStore extends EventSource { +export interface EventStore extends EventSource { put(event: NostrEvent): Promise; } @@ -26,32 +26,14 @@ const knowns = new RelayGroup([ */ export const nostr: EventSource = { config: { name: "nostr" }, - async get(filter) { - return await cache.get(filter) ?? _get(knowns, filter); + get(filter) { + return _get(knowns, filter); }, list(filter) { return knowns.subscribe(filter); }, }; -export const cache: EventStore = { - config: { name: "cache" }, - async get(filter: EventFilter) { - const kv = await Deno.openKv(); - if (filter.ids?.length) { - return Promise.any( - filter.ids.map((id) => - new Promise>((resolve, reject) => - kv.get>(["events", id]).then(({ value }) => - value ? resolve(value) : reject() - ) - ) - ), - ).catch(() => undefined); - } - }, -}; - async function _get( source: RelayLike, filter: EventFilter, diff --git a/deno.lock b/deno.lock index bab1595..8361445 100644 --- a/deno.lock +++ b/deno.lock @@ -6,20 +6,16 @@ "jsr:@std/assert@^0.219.1": "jsr:@std/assert@0.219.1", "jsr:@std/assert@^0.221.0": "jsr:@std/assert@0.221.0", "jsr:@std/bytes@^0.219.1": "jsr:@std/bytes@0.219.1", - "jsr:@std/bytes@^0.221.0": "jsr:@std/bytes@0.221.0", "jsr:@std/collections@^0.221.0": "jsr:@std/collections@0.221.0", - "jsr:@std/fmt@^0.219.1": "jsr:@std/fmt@0.219.1", "jsr:@std/fmt@^0.221.0": "jsr:@std/fmt@0.221.0", "jsr:@std/io@^0.219.1": "jsr:@std/io@0.219.1", - "jsr:@std/io@^0.221.0": "jsr:@std/io@0.221.0", "jsr:@std/streams@^0.219.1": "jsr:@std/streams@0.219.1", - "jsr:@std/streams@^0.221.0": "jsr:@std/streams@0.221.0", "jsr:@std/testing@^0.221.0": "jsr:@std/testing@0.221.0", "npm:@noble/curves@^1.3.0": "npm:@noble/curves@1.4.0", "npm:@noble/hashes@^1.3.3": "npm:@noble/hashes@1.4.0", "npm:esbuild@^0.20.2": "npm:esbuild@0.20.2", "npm:mini-van-plate@^0.5.6": "npm:mini-van-plate@0.5.6", - "npm:nostr-tools@^2.3.2": "npm:nostr-tools@2.3.2", + "npm:nostr-tools@^2.3.2": "npm:nostr-tools@2.5.0", "npm:sharp@^0.33.3": "npm:sharp@0.33.3", "npm:ts-toolbelt@9.6.0": "npm:ts-toolbelt@9.6.0" }, @@ -31,10 +27,7 @@ ] }, "@std/assert@0.219.1": { - "integrity": "e76c2a1799a78f0f4db7de04bdc9b908a7a4b821bb65eda0285885297d4fb8af", - "dependencies": [ - "jsr:@std/fmt@^0.219.1" - ] + "integrity": "e76c2a1799a78f0f4db7de04bdc9b908a7a4b821bb65eda0285885297d4fb8af" }, "@std/assert@0.221.0": { "integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a", @@ -45,24 +38,15 @@ "@std/bytes@0.219.1": { "integrity": "693e5f3f7796b33a448fb16c448b75d7d6e62914b55e2f1f715f9a04f77853b4" }, - "@std/bytes@0.221.0": { - "integrity": "64a047011cf833890a4a2ab7293ac55a1b4f5a050624ebc6a0159c357de91966" - }, "@std/collections@0.221.0": { "integrity": "789a365bb79b06c4da79d2b93fe32ddfe03b93ab083e72c1de96303d12f84be5" }, - "@std/fmt@0.219.1": { - "integrity": "2432152e927df249a207177aa048a6d9465956ea0047653ee6abd4f514db504f" - }, "@std/fmt@0.221.0": { "integrity": "379fed69bdd9731110f26b9085aeb740606b20428ce6af31ef6bd45ef8efa62a" }, "@std/io@0.219.1": { "integrity": "4f5073dd150fb7deff5c57449f4304edaf89a1137e0003602a22c133eb7cf93a" }, - "@std/io@0.221.0": { - "integrity": "faf7f8700d46ab527fa05cc6167f4b97701a06c413024431c6b4d207caa010da" - }, "@std/streams@0.219.1": { "integrity": "6f5dac5773a4fafdbe7ee612d0a0d5a2cbe465b9c9e2c85d371877dc8a52d1d3", "dependencies": [ @@ -71,14 +55,6 @@ "jsr:@std/io@^0.219.1" ] }, - "@std/streams@0.221.0": { - "integrity": "47f2f74634b47449277c0ee79fe878da4424b66bd8975c032e3afdca88986e61", - "dependencies": [ - "jsr:@std/assert@^0.221.0", - "jsr:@std/bytes@^0.221.0", - "jsr:@std/io@^0.221.0" - ] - }, "@std/testing@0.221.0": { "integrity": "d9b5ed7d5dc4e8d193734a594e7f80c04a5a6f34c2493e69c6713ae7fe6f880e" } @@ -399,8 +375,8 @@ "integrity": "sha512-gAJPXBihI8EFTutUc7oj9tmnOJKgLme5SQNAwj3gN0VU95Nzptiwo1oZy5SJRBwl5ZH7KmjQ6tsqxGHfSgdcoQ==", "dependencies": {} }, - "nostr-tools@2.3.2": { - "integrity": "sha512-8ceZ2ItkAGjR5b9+QOkkV9KWBOK0WPlpFrPPXmbWnNMcnlj9zB7rjdYPK2sV/OK4Ty9J3xL6+bvYKY77gup5EQ==", + "nostr-tools@2.5.0": { + "integrity": "sha512-G02O3JYNCfhx9NDjd3NOCw/5ck8PX5hiOIhHKpsXyu49ZtZbxGH3OLP9tf0fpUZ+EVWdjIYFR689sV0i7+TOng==", "dependencies": { "@noble/ciphers": "@noble/ciphers@0.5.2", "@noble/curves": "@noble/curves@1.2.0", diff --git a/deno/idb/mod.ts b/deno/idb/mod.ts index 6176a86..341345b 100644 --- a/deno/idb/mod.ts +++ b/deno/idb/mod.ts @@ -24,65 +24,61 @@ export interface IDBFactory { deleteDatabase(name: string): IDBOpenDBRequest; } -const kv = await Deno.openKv(); - -export const indexedDB: IDBFactory = { - open( - name: string, - version: number = 1, - ): IDBOpenDBRequest { - return new _IDBOpenDBRequest(async function () { - // Check if the database already exists and is up to date. - const existed = await kv.get($.database(name)); - if (existed.value?.version && existed.value.version >= version) { - const stores = new Map(); - const iter = kv.list({ - prefix: $.store.prefix, - }); - for await (const { key, value } of iter) { - const name = key.toReversed()[0] as string; - stores.set(name, value); +export function createIDBFactory( + kv: Deno.Kv, +): IDBFactory { + return { + open( + name: string, + version: number = 1, + ): IDBOpenDBRequest { + return new _IDBOpenDBRequest(async function () { + // Check if the database already exists and is up to date. + const existed = await kv.get($.database(name)); + if (existed.value?.version && existed.value.version >= version) { + const stores = new Map(); + const iter = kv.list({ + prefix: $.store.prefix, + }); + for await (const { key, value } of iter) { + const name = key.toReversed()[0] as string; + stores.set(name, value); + } + return new _IDBDatabase(name, version, kv, stores); } - return new _IDBDatabase(name, version, kv, stores); - } - // Create the new database. - await kv.set($.database(name), { name, version }); - const db = new _IDBDatabase(name, version, kv, new Map()); - const transaction = new _IDBTransaction(db, "versionchange"); - this.transaction = transaction as IDBTransaction<"versionchange">; - db._transaction = transaction; - this.result = db; - this.dispatchEvent( - new _IDBVersionChangeEvent(existed.value?.version ?? 0, version), - ); - await new Promise((resolve) => { - transaction.addEventListener("complete", resolve); - }); - db._transaction = null; - return db; - }) as IDBOpenDBRequest; - }, - - cmp(_a: unknown, _b: unknown): -1 | 0 | 1 { - console.warn("indexedDB.cmp is not implemented"); - return 0; - }, + // Create the new database. + await kv.set($.database(name), { name, version }); + const db = new _IDBDatabase(name, version, kv, new Map()); + const transaction = new _IDBTransaction(db, "versionchange"); + this.transaction = transaction as IDBTransaction<"versionchange">; + db._transaction = transaction; + this.result = db; + this.dispatchEvent( + new _IDBVersionChangeEvent(existed.value?.version ?? 0, version), + ); + await new Promise((resolve) => { + transaction.addEventListener("complete", resolve); + }); + db._transaction = null; + return db; + }) as IDBOpenDBRequest; + }, - async databases(): Promise { - const iter = kv.list({ prefix: $.database.prefix }); - return (await Array.fromAsync(iter)).map((it) => it.value); - }, + cmp(_a: unknown, _b: unknown): -1 | 0 | 1 { + console.warn("indexedDB.cmp is not implemented"); + return 0; + }, - deleteDatabase(name: string): IDBOpenDBRequest { - return new _IDBOpenDBRequest(async () => { - await kv.delete($.database(name)); - return undefined; - }) as IDBOpenDBRequest; - }, -}; + async databases(): Promise { + const iter = kv.list({ prefix: $.database.prefix }); + return (await Array.fromAsync(iter)).map((it) => it.value); + }, -if (self.indexedDB === undefined && self.Deno.Kv) { - console.warn("Using an experimental IndexedDB polyfill with Deno KV"); - // @ts-ignore We type IDBFactory better - self.indexedDB = indexedDB; + deleteDatabase(name: string): IDBOpenDBRequest { + return new _IDBOpenDBRequest(async () => { + await kv.delete($.database(name)); + return undefined; + }) as IDBOpenDBRequest; + }, + }; } diff --git a/deno/idb_test.ts b/deno/idb_test.ts index 7f13278..0314b49 100644 --- a/deno/idb_test.ts +++ b/deno/idb_test.ts @@ -14,25 +14,16 @@ import { } from "@std/testing/bdd"; import type { IDBDatabase, - IDBFactory, IDBIndex, IDBObjectStore, IDBOpenDBRequest, IDBTransaction, } from "./idb/mod.ts"; -import "./idb/mod.ts"; +import { createIDBFactory } from "./idb/mod.ts"; -declare global { - interface Window { - indexedDB: IDBFactory; - } -} - -function ensure(target: EventTarget, event: string): Promise { - return new Promise((resolve) => { - target.addEventListener(event, resolve, { once: true }); - }); -} +const indexedDB = createIDBFactory( + await Deno.openKv(), +); describe("IDBOpenRequest", () => { describe("onupgradeneeded", () => { @@ -41,11 +32,11 @@ describe("IDBOpenRequest", () => { beforeEach(() => { name = crypto.randomUUID(); - request = self.indexedDB.open(name); + request = indexedDB.open(name); }); afterEach(() => { - self.indexedDB.deleteDatabase(name); + indexedDB.deleteDatabase(name); }); it("should be called when the database is created", async () => { @@ -82,7 +73,7 @@ describe("IDBOpenRequest", () => { }); it("should be called when the database is opened", async () => { - const request = self.indexedDB.open(name); + const request = indexedDB.open(name); await ensure(request, "success"); assertEquals(request.result.name, name); }); @@ -94,7 +85,7 @@ describe("IDBDatabase", () => { let db: IDBDatabase; beforeAll(async () => { - const request = self.indexedDB.open(name); + const request = indexedDB.open(name); await new Promise((resolve) => { request.onupgradeneeded = (event) => resolve( @@ -138,7 +129,7 @@ describe("IDBObjectStore", () => { let store: IDBObjectStore; beforeAll(async () => { - const request = self.indexedDB.open(name); + const request = indexedDB.open(name); await new Promise((resolve) => { request.onupgradeneeded = (event) => resolve( @@ -150,7 +141,7 @@ describe("IDBObjectStore", () => { }); afterAll(async () => { - const request = self.indexedDB.deleteDatabase(name); + const request = indexedDB.deleteDatabase(name); await ensure(request, "success"); }); @@ -172,7 +163,7 @@ describe('IDBObjectStore<"readonly">', () => { let store: IDBObjectStore<"readonly">; beforeAll(async () => { - const request = self.indexedDB.open(name); + const request = indexedDB.open(name); await new Promise((resolve) => { request.onupgradeneeded = (event) => { const db = event.target.result; @@ -188,7 +179,7 @@ describe('IDBObjectStore<"readonly">', () => { }); afterAll(async () => { - const request = self.indexedDB.deleteDatabase(name); + const request = indexedDB.deleteDatabase(name); await ensure(request, "success"); }); @@ -230,7 +221,7 @@ describe('IDBObjectStore<"readwrite">', () => { let store: IDBObjectStore<"readwrite">; beforeAll(async () => { - const request = self.indexedDB.open(name); + const request = indexedDB.open(name); await new Promise((resolve) => { request.onupgradeneeded = (event) => resolve( @@ -244,7 +235,7 @@ describe('IDBObjectStore<"readwrite">', () => { }); afterAll(async () => { - const request = self.indexedDB.deleteDatabase(name); + const request = indexedDB.deleteDatabase(name); await ensure(request, "success"); }); @@ -289,7 +280,7 @@ describe('IDBObjectStore<"versionchange">', () => { let store: IDBObjectStore<"versionchange">; beforeAll(async () => { - const request = self.indexedDB.open(name); + const request = indexedDB.open(name); await new Promise((resolve) => { request.onupgradeneeded = (event) => { const db = event.target.result; @@ -301,7 +292,7 @@ describe('IDBObjectStore<"versionchange">', () => { }); afterAll(async () => { - const request = self.indexedDB.deleteDatabase(name); + const request = indexedDB.deleteDatabase(name); await ensure(request, "success"); }); @@ -337,7 +328,7 @@ describe("IDBTransaction", () => { let transaction: IDBTransaction; beforeAll(async () => { - const request = self.indexedDB.open(name); + const request = indexedDB.open(name); await new Promise((resolve) => { request.onupgradeneeded = (event) => resolve( @@ -349,7 +340,7 @@ describe("IDBTransaction", () => { }); afterAll(async () => { - const request = self.indexedDB.deleteDatabase(name); + const request = indexedDB.deleteDatabase(name); await ensure(request, "success"); }); @@ -391,7 +382,7 @@ describe("IDBIndex", () => { let index: IDBIndex; beforeAll(async () => { - const request = self.indexedDB.open(name); + const request = indexedDB.open(name); await new Promise((resolve) => { request.onupgradeneeded = (event) => { const db = event.target.result; @@ -420,3 +411,12 @@ describe("IDBIndex", () => { assertEquals(index.unique, false); }); }); + +/** + * A convenience function to wait for an event to be dispatched. + */ +function ensure(target: EventTarget, event: string): Promise { + return new Promise((resolve) => { + target.addEventListener(event, resolve, { once: true }); + }); +} diff --git a/std/stores.ts b/std/stores.ts index d862f72..83469e5 100644 --- a/std/stores.ts +++ b/std/stores.ts @@ -3,22 +3,33 @@ * @module */ -import "@lophus/deno/idb"; +import type { IDBFactory } from "@lophus/deno/idb"; import type { EventKind, NostrEvent } from "@lophus/core/protocol"; -export class EventStore { - #request: IDBOpenDBRequest; +let indexedDB: IDBFactory; + +if (self.indexedDB === undefined && Deno.Kv) { + const { createIDBFactory } = await import("@lophus/deno/idb"); + indexedDB = createIDBFactory( + await Deno.openKv(), + ); +} else { + // Use improved types from @lophus/deno/idb + indexedDB = self.indexedDB as unknown as IDBFactory; +} +export class EventStore { constructor(name: string, version?: number) { - this.#request = self.indexedDB.open(name, version); + const request = indexedDB.open(name, version); - this.#request.onupgradeneeded = () => { - const db = this.#request.result; + request.onupgradeneeded = (event) => { + const db = event.target.result; const events = db.createObjectStore("events", { keyPath: "id" }); events.createIndex("kind", "kind"); }; } - put(event: NostrEvent): Promise { + put(_event: NostrEvent) { + // TODO: Implement } } diff --git a/std/stores_test.ts b/std/stores_test.ts index ea7f91b..a10d65d 100644 --- a/std/stores_test.ts +++ b/std/stores_test.ts @@ -1,4 +1,4 @@ -import { assertExists, assertInstanceOf } from "@std/assert"; +import { assertInstanceOf } from "@std/assert"; import { describe, it } from "@std/testing/bdd"; import { EventStore } from "./stores.ts"; @@ -12,10 +12,6 @@ describe("EventStore", () => { describe("put", () => { it("should put an event into the store", async () => { - const store = new EventStore("test"); - await store.put({ id: "1", kind: 0, created_at: 0 }); - const event = await store.get({ ids: ["1"] }); - assertExists(event); }); }); });