@procedures/store 0.17.2 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +34 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -2
- package/dist/indexeddb/document-storage.d.ts +186 -0
- package/dist/indexeddb/document-storage.d.ts.map +1 -0
- package/dist/indexeddb/document-storage.js +598 -0
- package/dist/indexeddb/document-storage.js.map +1 -0
- package/dist/indexeddb/idb.d.ts +82 -9
- package/dist/indexeddb/idb.d.ts.map +1 -1
- package/dist/indexeddb/idb.js +100 -22
- package/dist/indexeddb/idb.js.map +1 -1
- package/dist/indexeddb.d.ts +8 -3
- package/dist/indexeddb.d.ts.map +1 -1
- package/dist/indexeddb.js +8 -3
- package/dist/indexeddb.js.map +1 -1
- package/package.json +4 -4
- package/src/browser.ts +2 -2
- package/src/indexeddb/document-storage.ts +689 -0
- package/src/indexeddb/idb.ts +136 -21
- package/src/indexeddb.ts +8 -3
package/src/indexeddb/idb.ts
CHANGED
|
@@ -30,8 +30,8 @@ export interface IdbFactoryLike {
|
|
|
30
30
|
/** An open database. */
|
|
31
31
|
export interface IdbDatabaseLike {
|
|
32
32
|
readonly objectStoreNames: { contains(name: string): boolean };
|
|
33
|
-
createObjectStore(name: string, options?: { keyPath?: string; autoIncrement?: boolean }):
|
|
34
|
-
transaction(names: string, mode?: "readonly" | "readwrite"): IdbTransactionLike;
|
|
33
|
+
createObjectStore(name: string, options?: { keyPath?: string; autoIncrement?: boolean }): IdbObjectStoreLike;
|
|
34
|
+
transaction(names: string | string[], mode?: "readonly" | "readwrite"): IdbTransactionLike;
|
|
35
35
|
close(): void;
|
|
36
36
|
onversionchange: ((event: any) => void) | null;
|
|
37
37
|
}
|
|
@@ -55,6 +55,30 @@ export interface IdbObjectStoreLike {
|
|
|
55
55
|
add(value: unknown, key?: string): IdbRequestLike<unknown>;
|
|
56
56
|
delete(key: string): IdbRequestLike<unknown>;
|
|
57
57
|
clear(): IdbRequestLike<unknown>;
|
|
58
|
+
index(name: string): IdbIndexLike;
|
|
59
|
+
createIndex(name: string, keyPath: string, options?: { unique?: boolean }): unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* An index of an object store. The results follow the order of the keys of the index. A record
|
|
64
|
+
* without the key path of the index is not in the index.
|
|
65
|
+
*/
|
|
66
|
+
export interface IdbIndexLike {
|
|
67
|
+
/** The records, the first `count` of them when `count` is given. */
|
|
68
|
+
getAll(query?: null, count?: number): IdbRequestLike<unknown[]>;
|
|
69
|
+
/** The primary keys of the records, without their values. */
|
|
70
|
+
getAllKeys(query?: null, count?: number): IdbRequestLike<unknown[]>;
|
|
71
|
+
count(): IdbRequestLike<number>;
|
|
72
|
+
/** A cursor over the records. `"prev"` starts at the highest key of the index. */
|
|
73
|
+
openCursor(query?: null, direction?: "next" | "prev"): IdbRequestLike<IdbCursorLike | null>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A cursor of an index, with the value of its record. */
|
|
77
|
+
export interface IdbCursorLike {
|
|
78
|
+
/** The key of the index. */
|
|
79
|
+
readonly key: unknown;
|
|
80
|
+
readonly value: unknown;
|
|
81
|
+
continue(): void;
|
|
58
82
|
}
|
|
59
83
|
|
|
60
84
|
/** The options of the IndexedDB stores. */
|
|
@@ -85,48 +109,95 @@ function errorOf(cause: unknown, message: string): Error {
|
|
|
85
109
|
}
|
|
86
110
|
|
|
87
111
|
/**
|
|
88
|
-
*
|
|
112
|
+
* This function calls `then` with the result of the request when the request succeeds. An error of
|
|
113
|
+
* `then` goes to `fail`, which aborts the transaction. Thus a check in a step of a transaction
|
|
114
|
+
* undoes each request of the transaction.
|
|
115
|
+
*/
|
|
116
|
+
export function onResult<R>(request: IdbRequestLike<R>, fail: (error: unknown) => void, then: (result: R) => void): void {
|
|
117
|
+
request.onsuccess = () => {
|
|
118
|
+
try {
|
|
119
|
+
then(request.result);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
fail(error);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** An object store of a database, with its indexes. */
|
|
127
|
+
export interface IdbStoreSchema {
|
|
128
|
+
readonly name: string;
|
|
129
|
+
readonly options?: { keyPath?: string; autoIncrement?: boolean };
|
|
130
|
+
readonly indexes?: ReadonlyArray<{ readonly name: string; readonly keyPath: string; readonly unique?: boolean }>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The work of one transaction. It makes the requests, and it gives a function that reads their
|
|
135
|
+
* results after the commit. `fail` aborts the transaction: the promise of the transaction then
|
|
136
|
+
* rejects with the error of `fail`, and each request is undone.
|
|
137
|
+
*/
|
|
138
|
+
export type IdbWork<R> = (transaction: IdbTransactionLike, fail: (error: unknown) => void) => () => R;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* One database, version 1, with its object stores. The connection opens at the first operation,
|
|
89
142
|
* and a failed open tries again at the next operation.
|
|
90
143
|
*/
|
|
91
|
-
export class
|
|
144
|
+
export class IdbConnection {
|
|
92
145
|
private connection: Promise<IdbDatabaseLike> | undefined;
|
|
93
146
|
private closed = false;
|
|
94
147
|
|
|
95
148
|
constructor(
|
|
96
149
|
/** The name of the database. */
|
|
97
150
|
readonly database: string,
|
|
98
|
-
/** The
|
|
99
|
-
readonly
|
|
100
|
-
private readonly storeOptions: { keyPath?: string; autoIncrement?: boolean },
|
|
151
|
+
/** The object stores of the database. */
|
|
152
|
+
private readonly stores: readonly IdbStoreSchema[],
|
|
101
153
|
private readonly factory: IdbFactoryLike | undefined,
|
|
154
|
+
/** The function that writes the first records when the open makes the object stores. */
|
|
155
|
+
private readonly onCreate?: (store: (name: string) => IdbObjectStoreLike) => void,
|
|
102
156
|
) {}
|
|
103
157
|
|
|
104
158
|
/**
|
|
105
|
-
* This method runs work in one transaction
|
|
106
|
-
*
|
|
107
|
-
* the
|
|
159
|
+
* This method runs work in one transaction over the object stores of `names`. The promise resolves
|
|
160
|
+
* after the transaction commits, so a write is durable when the promise resolves. An error of a
|
|
161
|
+
* request, a throw of the work and a call of `fail` abort the transaction: each request is undone.
|
|
108
162
|
*/
|
|
109
|
-
async
|
|
163
|
+
async transaction<R>(mode: "readonly" | "readwrite", names: readonly string[], work: IdbWork<R>): Promise<R> {
|
|
110
164
|
const db = await this.open();
|
|
111
165
|
return new Promise<R>((resolve, reject) => {
|
|
112
|
-
let transaction: IdbTransactionLike;
|
|
166
|
+
let transaction: IdbTransactionLike | undefined;
|
|
167
|
+
let failure: { readonly error: unknown } | undefined;
|
|
168
|
+
const abort = (): void => {
|
|
169
|
+
try {
|
|
170
|
+
transaction?.abort();
|
|
171
|
+
} catch {
|
|
172
|
+
// The transaction ended already
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const fail = (error: unknown): void => {
|
|
176
|
+
if (failure) return;
|
|
177
|
+
failure = { error };
|
|
178
|
+
abort();
|
|
179
|
+
reject(error);
|
|
180
|
+
};
|
|
113
181
|
let read: () => R;
|
|
114
182
|
try {
|
|
115
|
-
transaction = db.transaction(
|
|
116
|
-
read = work(transaction
|
|
183
|
+
transaction = db.transaction(names.length === 1 ? names[0]! : [...names], mode);
|
|
184
|
+
read = work(transaction, fail);
|
|
117
185
|
} catch (error) {
|
|
118
|
-
|
|
186
|
+
// A request that the work made before the throw does not commit
|
|
187
|
+
fail(error);
|
|
119
188
|
return;
|
|
120
189
|
}
|
|
121
|
-
|
|
190
|
+
const current = transaction;
|
|
191
|
+
current.oncomplete = () => {
|
|
122
192
|
try {
|
|
123
193
|
resolve(read());
|
|
124
194
|
} catch (error) {
|
|
125
195
|
reject(error);
|
|
126
196
|
}
|
|
127
197
|
};
|
|
128
|
-
|
|
129
|
-
|
|
198
|
+
current.onerror = (event?: { target?: { error?: unknown } }) =>
|
|
199
|
+
reject(failure ? failure.error : errorOf(event?.target?.error ?? current.error, `The IndexedDB transaction on ${this.database} failed`));
|
|
200
|
+
current.onabort = () => reject(failure ? failure.error : errorOf(current.error, `The IndexedDB transaction on ${this.database} was aborted`));
|
|
130
201
|
});
|
|
131
202
|
}
|
|
132
203
|
|
|
@@ -168,13 +239,27 @@ export class IdbStoreConnection {
|
|
|
168
239
|
}
|
|
169
240
|
request.onupgradeneeded = () => {
|
|
170
241
|
const db = request.result;
|
|
171
|
-
|
|
242
|
+
const created = new Map<string, IdbObjectStoreLike>();
|
|
243
|
+
for (const schema of this.stores) {
|
|
244
|
+
if (db.objectStoreNames.contains(schema.name)) continue;
|
|
245
|
+
const store = db.createObjectStore(schema.name, schema.options ?? {});
|
|
246
|
+
for (const index of schema.indexes ?? []) store.createIndex(index.name, index.keyPath, { unique: index.unique ?? false });
|
|
247
|
+
created.set(schema.name, store);
|
|
248
|
+
}
|
|
249
|
+
if (this.onCreate && created.size > 0) {
|
|
250
|
+
this.onCreate((name) => {
|
|
251
|
+
const store = created.get(name);
|
|
252
|
+
if (!store) throw new Error(`The object store ${name} of ${this.database} existed before the open`);
|
|
253
|
+
return store;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
172
256
|
};
|
|
173
257
|
request.onsuccess = () => {
|
|
174
258
|
const db = request.result;
|
|
175
|
-
|
|
259
|
+
const missing = this.stores.find((schema) => !db.objectStoreNames.contains(schema.name));
|
|
260
|
+
if (missing) {
|
|
176
261
|
db.close();
|
|
177
|
-
reject(new Error(`The IndexedDB database ${this.database} exists, and it has no object store ${
|
|
262
|
+
reject(new Error(`The IndexedDB database ${this.database} exists, and it has no object store ${missing.name}`));
|
|
178
263
|
return;
|
|
179
264
|
}
|
|
180
265
|
// Another tab wants a new version: close this connection, so the upgrade can run
|
|
@@ -188,3 +273,33 @@ export class IdbStoreConnection {
|
|
|
188
273
|
});
|
|
189
274
|
}
|
|
190
275
|
}
|
|
276
|
+
|
|
277
|
+
/** One object store in its own database, version 1. The stores of programs, queues and sync states use it. */
|
|
278
|
+
export class IdbStoreConnection {
|
|
279
|
+
private readonly connection: IdbConnection;
|
|
280
|
+
|
|
281
|
+
constructor(
|
|
282
|
+
/** The name of the database. */
|
|
283
|
+
readonly database: string,
|
|
284
|
+
/** The name of the object store. */
|
|
285
|
+
readonly store: string,
|
|
286
|
+
storeOptions: { keyPath?: string; autoIncrement?: boolean },
|
|
287
|
+
factory: IdbFactoryLike | undefined,
|
|
288
|
+
) {
|
|
289
|
+
this.connection = new IdbConnection(database, [{ name: store, options: storeOptions }], factory);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* This method runs work in one transaction. The work makes the requests, and it gives a function that reads
|
|
294
|
+
* their results. The promise resolves after the transaction commits, so a write is durable when
|
|
295
|
+
* the promise resolves. An error of a request aborts the transaction: each request is undone.
|
|
296
|
+
*/
|
|
297
|
+
run<R>(mode: "readonly" | "readwrite", work: (store: IdbObjectStoreLike) => () => R): Promise<R> {
|
|
298
|
+
return this.connection.transaction(mode, [this.store], (transaction) => work(transaction.objectStore(this.store)));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** This method closes the connection. A later operation throws. */
|
|
302
|
+
close(): Promise<void> {
|
|
303
|
+
return this.connection.close();
|
|
304
|
+
}
|
|
305
|
+
}
|
package/src/indexeddb.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The entry `@procedures/store/indexeddb`: the stores of a browser (roadmap R45).
|
|
3
3
|
*
|
|
4
|
+
* - `IndexedDbStorage` is a collection storage in IndexedDB (roadmap CLI-22). It is the durable
|
|
5
|
+
* local copy of a `HybridStorage` in a browser app without SignalDB.
|
|
4
6
|
* - `IndexedDbProgramStore` keeps programs in IndexedDB.
|
|
5
7
|
* - `IndexedDbQueueStore` keeps the queue of a `HybridStorage` in IndexedDB.
|
|
6
8
|
* - `IndexedDbSyncStateStore` keeps the sync state of a `HybridStorage` in IndexedDB: the cursor
|
|
7
9
|
* and the base versions (roadmap CLI-21).
|
|
8
10
|
* - `MemoryProgramStore`, `MemoryQueueStore` and `MemorySyncStateStore` are the fallbacks for a
|
|
9
|
-
* browser without IndexedDB. `hasIndexedDb()` tells which one to use.
|
|
11
|
+
* browser without IndexedDB. `hasIndexedDb()` tells which one to use. `InMemoryStorage` of
|
|
12
|
+
* `@procedures/collections` is the fallback of `IndexedDbStorage`.
|
|
10
13
|
*
|
|
11
|
-
* The package root does not import this entry
|
|
12
|
-
* a page that loads it gets only these stores.
|
|
14
|
+
* The package root does not import this entry. This entry does not import the root of the client or
|
|
15
|
+
* zod: a page that loads it gets only these stores. It imports `@procedures/client/query`, a module
|
|
16
|
+
* without imports, and `@procedures/collections` (the contract of the storages and its listeners).
|
|
13
17
|
*/
|
|
14
18
|
|
|
19
|
+
export { IndexedDbStorage, type IndexedDbStorageOptions } from "./indexeddb/document-storage.js";
|
|
15
20
|
export { IndexedDbProgramStore, DEFAULT_PROGRAM_DATABASE } from "./indexeddb/program-store.js";
|
|
16
21
|
export { IndexedDbQueueStore } from "./indexeddb/queue-store.js";
|
|
17
22
|
export { IndexedDbSyncStateStore } from "./indexeddb/sync-state-store.js";
|