@meshline/storage-browser 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +340 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
- package/src/index.ts +304 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Meshline contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Meshline IndexedDB storage
|
|
2
|
+
|
|
3
|
+
`IndexedDbStore` provides persistent transactional storage in a browser.
|
|
4
|
+
It requires **IndexedDB, Web Locks, and cryptographic randomness on a secure origin**.
|
|
5
|
+
The package is ESM with TypeScript declarations.
|
|
6
|
+
|
|
7
|
+
## Install and configure
|
|
8
|
+
|
|
9
|
+
Install this adapter and the matching core together:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @meshline/sdk@0.1.0-alpha.1 @meshline/storage-browser@0.1.0-alpha.1
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Import `IndexedDbStore` from `@meshline/storage-browser` and construct it
|
|
16
|
+
with a database name. Construction does no I/O. Call `migrate()`, pass it to
|
|
17
|
+
`MeshlineClient`, then initialize the client. Direct store consumers call
|
|
18
|
+
`initialize({ context, accountId })` instead.
|
|
19
|
+
|
|
20
|
+
Use one database for each network/account/local device. The
|
|
21
|
+
[browser example](https://github.com/meshline-network/sdk/blob/main/typescript/examples/browser.ts)
|
|
22
|
+
shows the complete session and cleanup.
|
|
23
|
+
|
|
24
|
+
## Persistence
|
|
25
|
+
|
|
26
|
+
Commits atomically compare the database revision and write state plus cursors.
|
|
27
|
+
Writes request strict durability and resolve after transaction completion.
|
|
28
|
+
Query readers materialize independent snapshot rows, then release the IndexedDB
|
|
29
|
+
transaction before returning. Dispose readers to remove their rows.
|
|
30
|
+
Web Locks distinguish live readers from abandoned snapshots after a crash.
|
|
31
|
+
|
|
32
|
+
Quotas, eviction, user clearing, and private-mode restrictions still apply.
|
|
33
|
+
Errors surface to the application; there is no in-memory fallback. Applications
|
|
34
|
+
may request persistent browser storage. Preserve access to the original secret
|
|
35
|
+
protector across reloads. The adapter does not encrypt the database or message text.
|
|
36
|
+
|
|
37
|
+
This package supplies storage. Browser HTTP and WebSocket cookie behavior has
|
|
38
|
+
separate constraints; review them before choosing a relay origin.
|
|
39
|
+
|
|
40
|
+
[Storage guide](https://github.com/meshline-network/sdk/blob/main/typescript/docs/guides/storage-and-pagination.md)
|
|
41
|
+
· [Browser requirements and limits](https://github.com/meshline-network/sdk/blob/main/typescript/docs/platforms.md#browser)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type MeshlineStore, type QueryReader, type RandomSource, type RecordQuery, type StorageBinding, type StoredRecord, type StoreMutation, type StoreSnapshot } from '@meshline/sdk';
|
|
2
|
+
/** Explicit platform dependencies permit worker use without importing DOM application code. */
|
|
3
|
+
export interface IndexedDbOptions {
|
|
4
|
+
readonly indexedDB?: IDBFactory;
|
|
5
|
+
readonly locks?: LockManager;
|
|
6
|
+
readonly random?: RandomSource;
|
|
7
|
+
}
|
|
8
|
+
/** IndexedDB storage with atomic commits, cross-tab conflicts and durable snapshots. */
|
|
9
|
+
export declare class IndexedDbStore implements MeshlineStore {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
private readonly options;
|
|
12
|
+
private readonly gate;
|
|
13
|
+
private database;
|
|
14
|
+
private owner;
|
|
15
|
+
private releaseOwner;
|
|
16
|
+
private ownerLock;
|
|
17
|
+
private disposed;
|
|
18
|
+
private invalidated;
|
|
19
|
+
private readonly readers;
|
|
20
|
+
constructor(name: string, options?: IndexedDbOptions);
|
|
21
|
+
migrate(signal?: AbortSignal): Promise<void>;
|
|
22
|
+
initialize(binding: StorageBinding, signal?: AbortSignal): Promise<void>;
|
|
23
|
+
read(queries: readonly RecordQuery[], signal?: AbortSignal): Promise<StoreSnapshot>;
|
|
24
|
+
commit(expectedVersion: number, mutations: readonly StoreMutation[], signal?: AbortSignal): Promise<number>;
|
|
25
|
+
openQuery(query: RecordQuery, signal?: AbortSignal): Promise<QueryReader<StoredRecord>>;
|
|
26
|
+
dispose(): Promise<void>;
|
|
27
|
+
private removeOwner;
|
|
28
|
+
private lockName;
|
|
29
|
+
private assertOpen;
|
|
30
|
+
private connection;
|
|
31
|
+
private openDatabase;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAC9F,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAC5D,MAAM,eAAe,CAAC;AAMvB,+FAA+F;AAC/F,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;CAClC;AAkED,wFAAwF;AACxF,qBAAa,cAAe,YAAW,aAAa;IAUpC,QAAQ,CAAC,IAAI,EAAE,MAAM;IAAE,OAAO,CAAC,QAAQ,CAAC,OAAO;IAT3D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmB;IACxC,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,YAAY,CAA2B;IAC/C,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8B;IAEtD,YAAqB,IAAI,EAAE,MAAM,EAAmB,OAAO,GAAE,gBAAqB,EAEjF;IAED,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAO3C;IAED,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDvE;IAED,IAAI,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CAYlF;IAED,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,aAAa,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAc1G;IAED,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CAiBtF;IAED,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAgBvB;IAED,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,YAAY;CAuBvB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { AsyncGate, StateConflictError, bindingJson, canonicalJson, encodeBase64Url, parseJson, prepareCommit, requireBatchCount, requireObject, throwIfAborted, systemRandom, validateRecordQuery, } from '@meshline/sdk';
|
|
2
|
+
function request(operation) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
operation.onsuccess = () => resolve(operation.result);
|
|
5
|
+
operation.onerror = () => reject(operation.error ?? new Error('IndexedDB request failed.'));
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
async function transaction(database, stores, mode, action, signal) {
|
|
9
|
+
throwIfAborted(signal);
|
|
10
|
+
const current = database.transaction(stores, mode, mode === 'readwrite' ? { durability: 'strict' } : {});
|
|
11
|
+
const abort = () => { current.abort(); };
|
|
12
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
13
|
+
const completed = new Promise((resolve, reject) => {
|
|
14
|
+
current.oncomplete = () => resolve();
|
|
15
|
+
current.onabort = () => reject(signal?.aborted ? signal.reason : current.error ?? new Error('IndexedDB transaction aborted.'));
|
|
16
|
+
current.onerror = () => { };
|
|
17
|
+
});
|
|
18
|
+
let settled = false;
|
|
19
|
+
current.addEventListener('complete', () => { settled = true; });
|
|
20
|
+
current.addEventListener('abort', () => { settled = true; });
|
|
21
|
+
const work = action(current).catch(error => {
|
|
22
|
+
if (!settled) {
|
|
23
|
+
try {
|
|
24
|
+
current.abort();
|
|
25
|
+
}
|
|
26
|
+
catch (abortError) {
|
|
27
|
+
if (!(abortError instanceof DOMException && abortError.name === 'InvalidStateError'))
|
|
28
|
+
throw new AggregateError([error, abortError], 'IndexedDB rollback failed.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
});
|
|
33
|
+
try {
|
|
34
|
+
const [result] = await Promise.all([work, completed]);
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
await Promise.allSettled([work, completed]);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
signal?.removeEventListener('abort', abort);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function keyRange(query) {
|
|
46
|
+
validateRecordQuery(query);
|
|
47
|
+
if (query.key !== undefined)
|
|
48
|
+
return IDBKeyRange.only([query.collection, query.key]);
|
|
49
|
+
const prefix = query.prefix ?? '';
|
|
50
|
+
return IDBKeyRange.bound([query.collection, prefix], [query.collection, prefix + '\uffff']);
|
|
51
|
+
}
|
|
52
|
+
function decode(record) {
|
|
53
|
+
return { collection: record.collection, key: record.key, revision: record.revision, value: requireObject(parseJson(record.value)) };
|
|
54
|
+
}
|
|
55
|
+
function scan(store, range, reverse, visit) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const operation = store.openCursor(range, reverse ? 'prev' : 'next');
|
|
58
|
+
operation.onerror = () => reject(operation.error);
|
|
59
|
+
operation.onsuccess = () => {
|
|
60
|
+
const cursor = operation.result;
|
|
61
|
+
if (!cursor) {
|
|
62
|
+
resolve();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
if (visit(cursor) === false)
|
|
67
|
+
resolve();
|
|
68
|
+
else
|
|
69
|
+
cursor.continue();
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
reject(error);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** IndexedDB storage with atomic commits, cross-tab conflicts and durable snapshots. */
|
|
78
|
+
export class IndexedDbStore {
|
|
79
|
+
name;
|
|
80
|
+
options;
|
|
81
|
+
gate = new AsyncGate();
|
|
82
|
+
database;
|
|
83
|
+
owner;
|
|
84
|
+
releaseOwner;
|
|
85
|
+
ownerLock;
|
|
86
|
+
disposed = false;
|
|
87
|
+
invalidated = false;
|
|
88
|
+
readers = new Set();
|
|
89
|
+
constructor(name, options = {}) {
|
|
90
|
+
this.name = name;
|
|
91
|
+
this.options = options;
|
|
92
|
+
if (!name.trim())
|
|
93
|
+
throw new TypeError('An IndexedDB database name is required.');
|
|
94
|
+
}
|
|
95
|
+
migrate(signal) {
|
|
96
|
+
return this.gate.run(async () => {
|
|
97
|
+
this.assertOpen();
|
|
98
|
+
if (this.database)
|
|
99
|
+
throw new Error('Migrate before initializing the store.');
|
|
100
|
+
const database = await this.openDatabase(true, signal);
|
|
101
|
+
database.close();
|
|
102
|
+
}, signal);
|
|
103
|
+
}
|
|
104
|
+
initialize(binding, signal) {
|
|
105
|
+
return this.gate.run(async () => {
|
|
106
|
+
this.assertOpen();
|
|
107
|
+
const encoded = bindingJson(binding);
|
|
108
|
+
if (this.database) {
|
|
109
|
+
const actual = await transaction(this.database, ['meta'], 'readonly', async (tx) => (await request(tx.objectStore('meta').get('binding'))).value, signal);
|
|
110
|
+
if (actual !== encoded)
|
|
111
|
+
throw new Error('Database is bound to another network or account.');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const locks = this.options.locks ?? globalThis.navigator?.locks;
|
|
115
|
+
if (!locks)
|
|
116
|
+
throw new Error('Web Locks are required to manage snapshot ownership.');
|
|
117
|
+
const database = await this.openDatabase(false, signal);
|
|
118
|
+
const owner = encodeBase64Url((this.options.random ?? systemRandom).bytes(16));
|
|
119
|
+
let release;
|
|
120
|
+
const lifetime = new Promise(resolve => { release = resolve; });
|
|
121
|
+
let acquired;
|
|
122
|
+
let failed;
|
|
123
|
+
const ready = new Promise((resolve, reject) => { acquired = resolve; failed = reject; });
|
|
124
|
+
const ownerLock = locks.request(this.lockName(owner), async () => { acquired(); await lifetime; });
|
|
125
|
+
ownerLock.catch(failed);
|
|
126
|
+
try {
|
|
127
|
+
await ready;
|
|
128
|
+
await transaction(database, ['meta', 'owners'], 'readwrite', async (tx) => {
|
|
129
|
+
const meta = tx.objectStore('meta');
|
|
130
|
+
const existing = await request(meta.get('binding'));
|
|
131
|
+
if (existing && existing.value !== encoded)
|
|
132
|
+
throw new Error('Database is bound to another network or account.');
|
|
133
|
+
if (!existing)
|
|
134
|
+
await request(meta.put({ key: 'binding', value: encoded }));
|
|
135
|
+
await request(tx.objectStore('owners').add({ id: owner }));
|
|
136
|
+
}, signal);
|
|
137
|
+
// Only a released owner lock permits cleanup; a suspended live tab keeps its snapshots.
|
|
138
|
+
const owners = await transaction(database, ['owners'], 'readonly', async (tx) => await request(tx.objectStore('owners').getAll()), signal);
|
|
139
|
+
for (const previous of owners) {
|
|
140
|
+
if (previous.id === owner)
|
|
141
|
+
continue;
|
|
142
|
+
await locks.request(this.lockName(previous.id), { ifAvailable: true }, async (lock) => {
|
|
143
|
+
if (lock)
|
|
144
|
+
await this.removeOwner(database, previous.id, signal);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
throwIfAborted(signal);
|
|
148
|
+
this.database = database;
|
|
149
|
+
this.owner = owner;
|
|
150
|
+
this.releaseOwner = release;
|
|
151
|
+
this.ownerLock = ownerLock;
|
|
152
|
+
database.onversionchange = () => { this.invalidated = true; database.close(); };
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
database.close();
|
|
156
|
+
release();
|
|
157
|
+
await ownerLock;
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}, signal);
|
|
161
|
+
}
|
|
162
|
+
read(queries, signal) {
|
|
163
|
+
const selections = queries.map(query => ({ range: keyRange(query), reverse: query.reverse ?? false }));
|
|
164
|
+
return this.gate.run(() => transaction(this.connection(), ['meta', 'records'], 'readonly', async (tx) => {
|
|
165
|
+
const version = (await request(tx.objectStore('meta').get('version'))).value;
|
|
166
|
+
const sets = [];
|
|
167
|
+
for (const query of selections) {
|
|
168
|
+
const values = [];
|
|
169
|
+
await scan(tx.objectStore('records'), query.range, query.reverse, cursor => { values.push(decode(cursor.value)); });
|
|
170
|
+
sets.push(values);
|
|
171
|
+
}
|
|
172
|
+
return { version, sets };
|
|
173
|
+
}, signal), signal);
|
|
174
|
+
}
|
|
175
|
+
commit(expectedVersion, mutations, signal) {
|
|
176
|
+
const changes = prepareCommit(expectedVersion, mutations);
|
|
177
|
+
return this.gate.run(() => transaction(this.connection(), ['meta', 'records'], 'readwrite', async (tx) => {
|
|
178
|
+
const meta = tx.objectStore('meta');
|
|
179
|
+
const version = (await request(meta.get('version'))).value;
|
|
180
|
+
if (version !== expectedVersion)
|
|
181
|
+
throw new StateConflictError('Database revision changed before commit.');
|
|
182
|
+
const next = version + 1;
|
|
183
|
+
for (const change of changes) {
|
|
184
|
+
if (change.kind === 'put')
|
|
185
|
+
await request(tx.objectStore('records').put({ collection: change.collection, key: change.key, revision: next, value: canonicalJson(change.value) }));
|
|
186
|
+
else
|
|
187
|
+
await request(tx.objectStore('records').delete([change.collection, change.key]));
|
|
188
|
+
}
|
|
189
|
+
await request(meta.put({ key: 'version', value: next }));
|
|
190
|
+
return next;
|
|
191
|
+
}, signal), signal);
|
|
192
|
+
}
|
|
193
|
+
openQuery(query, signal) {
|
|
194
|
+
const range = keyRange(query);
|
|
195
|
+
const reverse = query.reverse ?? false;
|
|
196
|
+
return this.gate.run(async () => {
|
|
197
|
+
const database = this.connection();
|
|
198
|
+
const owner = this.owner;
|
|
199
|
+
const id = encodeBase64Url((this.options.random ?? systemRandom).bytes(16));
|
|
200
|
+
await transaction(database, ['records', 'snapshots'], 'readwrite', async (tx) => {
|
|
201
|
+
let index = 0;
|
|
202
|
+
await scan(tx.objectStore('records'), range, reverse, cursor => {
|
|
203
|
+
tx.objectStore('snapshots').add({ owner, reader: id, index: index++, record: cursor.value });
|
|
204
|
+
});
|
|
205
|
+
}, signal);
|
|
206
|
+
const reader = new IndexedDbReader(database, owner, id, () => this.readers.delete(reader));
|
|
207
|
+
this.readers.add(reader);
|
|
208
|
+
return reader;
|
|
209
|
+
}, signal);
|
|
210
|
+
}
|
|
211
|
+
dispose() {
|
|
212
|
+
return this.gate.run(async () => {
|
|
213
|
+
if (this.disposed)
|
|
214
|
+
return;
|
|
215
|
+
this.disposed = true;
|
|
216
|
+
const errors = [];
|
|
217
|
+
for (const reader of this.readers) {
|
|
218
|
+
try {
|
|
219
|
+
await reader.dispose();
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
errors.push(error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
if (this.database && this.owner && !this.invalidated)
|
|
227
|
+
await this.removeOwner(this.database, this.owner);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
errors.push(error);
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
this.database?.close();
|
|
234
|
+
this.database = undefined;
|
|
235
|
+
this.releaseOwner?.();
|
|
236
|
+
await this.ownerLock;
|
|
237
|
+
}
|
|
238
|
+
if (errors.length)
|
|
239
|
+
throw new AggregateError(errors, 'Failed to dispose IndexedDB storage.');
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
removeOwner(database, id, signal) {
|
|
243
|
+
return transaction(database, ['owners', 'snapshots'], 'readwrite', async (tx) => {
|
|
244
|
+
await request(tx.objectStore('snapshots').delete(IDBKeyRange.bound([id, ''], [id, '\uffff'])));
|
|
245
|
+
await request(tx.objectStore('owners').delete(id));
|
|
246
|
+
}, signal);
|
|
247
|
+
}
|
|
248
|
+
lockName(owner) { return `meshline:${this.name}:snapshots:${owner}`; }
|
|
249
|
+
assertOpen() {
|
|
250
|
+
if (this.disposed)
|
|
251
|
+
throw new Error('Store is disposed.');
|
|
252
|
+
if (this.invalidated)
|
|
253
|
+
throw new Error('Database was closed for an external schema change. Reopen the store.');
|
|
254
|
+
}
|
|
255
|
+
connection() {
|
|
256
|
+
this.assertOpen();
|
|
257
|
+
if (!this.database)
|
|
258
|
+
throw new Error('Store is not initialized.');
|
|
259
|
+
return this.database;
|
|
260
|
+
}
|
|
261
|
+
openDatabase(migrate, signal) {
|
|
262
|
+
throwIfAborted(signal);
|
|
263
|
+
const factory = this.options.indexedDB ?? globalThis.indexedDB;
|
|
264
|
+
if (!factory)
|
|
265
|
+
return Promise.reject(new Error('IndexedDB is unavailable.'));
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
const operation = factory.open(this.name, 1);
|
|
268
|
+
let rejected = false;
|
|
269
|
+
operation.onblocked = () => { rejected = true; reject(new Error('Database migration is blocked by another connection.')); };
|
|
270
|
+
operation.onupgradeneeded = () => {
|
|
271
|
+
if (!migrate || rejected || signal?.aborted) {
|
|
272
|
+
operation.transaction.abort();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const database = operation.result;
|
|
276
|
+
database.createObjectStore('meta', { keyPath: 'key' }).add({ key: 'version', value: 0 });
|
|
277
|
+
database.createObjectStore('records', { keyPath: ['collection', 'key'] });
|
|
278
|
+
database.createObjectStore('owners', { keyPath: 'id' });
|
|
279
|
+
database.createObjectStore('snapshots', { keyPath: ['owner', 'reader', 'index'] });
|
|
280
|
+
};
|
|
281
|
+
operation.onerror = () => reject(signal?.aborted ? signal.reason : operation.error ?? new Error('Explicit database migration is required.'));
|
|
282
|
+
operation.onsuccess = () => {
|
|
283
|
+
if (rejected || signal?.aborted) {
|
|
284
|
+
operation.result.close();
|
|
285
|
+
reject(signal?.reason ?? new Error('Database open failed.'));
|
|
286
|
+
}
|
|
287
|
+
else
|
|
288
|
+
resolve(operation.result);
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
class IndexedDbReader {
|
|
294
|
+
database;
|
|
295
|
+
owner;
|
|
296
|
+
reader;
|
|
297
|
+
released;
|
|
298
|
+
gate = new AsyncGate();
|
|
299
|
+
offset = 0;
|
|
300
|
+
disposed = false;
|
|
301
|
+
constructor(database, owner, reader, released) {
|
|
302
|
+
this.database = database;
|
|
303
|
+
this.owner = owner;
|
|
304
|
+
this.reader = reader;
|
|
305
|
+
this.released = released;
|
|
306
|
+
}
|
|
307
|
+
readNext(count, signal) {
|
|
308
|
+
requireBatchCount(count);
|
|
309
|
+
return this.gate.run(async () => {
|
|
310
|
+
if (this.disposed)
|
|
311
|
+
throw new Error('Reader is disposed.');
|
|
312
|
+
const values = await transaction(this.database, ['snapshots'], 'readonly', async (tx) => {
|
|
313
|
+
const values = [];
|
|
314
|
+
await scan(tx.objectStore('snapshots'), IDBKeyRange.bound([this.owner, this.reader, this.offset], [this.owner, this.reader, Number.MAX_SAFE_INTEGER]), false, cursor => {
|
|
315
|
+
values.push(decode(cursor.value.record));
|
|
316
|
+
return values.length < count;
|
|
317
|
+
});
|
|
318
|
+
return values;
|
|
319
|
+
}, signal);
|
|
320
|
+
this.offset += values.length;
|
|
321
|
+
return values;
|
|
322
|
+
}, signal);
|
|
323
|
+
}
|
|
324
|
+
dispose() {
|
|
325
|
+
return this.gate.run(async () => {
|
|
326
|
+
if (this.disposed)
|
|
327
|
+
return;
|
|
328
|
+
this.disposed = true;
|
|
329
|
+
try {
|
|
330
|
+
await transaction(this.database, ['snapshots'], 'readwrite', async (tx) => {
|
|
331
|
+
await request(tx.objectStore('snapshots').delete(IDBKeyRange.bound([this.owner, this.reader, 0], [this.owner, this.reader, Number.MAX_SAFE_INTEGER])));
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
this.released();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meshline/storage-browser",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Transactional IndexedDB storage for the Meshline TypeScript SDK",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@meshline/sdk": "0.1.0-alpha.1"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/meshline-network/sdk.git",
|
|
26
|
+
"directory": "typescript/packages/storage-browser"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"registry": "https://registry.npmjs.org"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AsyncGate, StateConflictError, bindingJson, canonicalJson, encodeBase64Url, parseJson,
|
|
3
|
+
prepareCommit, requireBatchCount, requireObject, throwIfAborted, systemRandom, validateRecordQuery,
|
|
4
|
+
type MeshlineStore, type QueryReader, type RandomSource, type RecordQuery, type StorageBinding,
|
|
5
|
+
type StoredRecord, type StoreMutation, type StoreSnapshot,
|
|
6
|
+
} from '@meshline/sdk';
|
|
7
|
+
|
|
8
|
+
interface DiskRecord { collection: string; key: string; revision: number; value: string }
|
|
9
|
+
interface SnapshotRow { owner: string; reader: string; index: number; record: DiskRecord }
|
|
10
|
+
interface Owner { id: string }
|
|
11
|
+
|
|
12
|
+
/** Explicit platform dependencies permit worker use without importing DOM application code. */
|
|
13
|
+
export interface IndexedDbOptions {
|
|
14
|
+
readonly indexedDB?: IDBFactory;
|
|
15
|
+
readonly locks?: LockManager;
|
|
16
|
+
readonly random?: RandomSource;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function request<T>(operation: IDBRequest<T>): Promise<T> {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
operation.onsuccess = () => resolve(operation.result);
|
|
22
|
+
operation.onerror = () => reject(operation.error ?? new Error('IndexedDB request failed.'));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function transaction<T>(database: IDBDatabase, stores: string[], mode: IDBTransactionMode, action: (value: IDBTransaction) => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
27
|
+
throwIfAborted(signal);
|
|
28
|
+
const current = database.transaction(stores, mode, mode === 'readwrite' ? { durability: 'strict' } : {});
|
|
29
|
+
const abort = (): void => { current.abort(); };
|
|
30
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
31
|
+
const completed = new Promise<void>((resolve, reject) => {
|
|
32
|
+
current.oncomplete = () => resolve();
|
|
33
|
+
current.onabort = () => reject(signal?.aborted ? signal.reason : current.error ?? new Error('IndexedDB transaction aborted.'));
|
|
34
|
+
current.onerror = () => { /* onabort supplies the final transaction outcome. */ };
|
|
35
|
+
});
|
|
36
|
+
let settled = false;
|
|
37
|
+
current.addEventListener('complete', () => { settled = true; });
|
|
38
|
+
current.addEventListener('abort', () => { settled = true; });
|
|
39
|
+
const work = action(current).catch(error => {
|
|
40
|
+
if (!settled) {
|
|
41
|
+
try { current.abort(); }
|
|
42
|
+
catch (abortError) {
|
|
43
|
+
if (!(abortError instanceof DOMException && abortError.name === 'InvalidStateError')) throw new AggregateError([error, abortError], 'IndexedDB rollback failed.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
const [result] = await Promise.all([work, completed]);
|
|
50
|
+
return result;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
await Promise.allSettled([work, completed]);
|
|
53
|
+
throw error;
|
|
54
|
+
} finally { signal?.removeEventListener('abort', abort); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function keyRange(query: RecordQuery): IDBKeyRange {
|
|
58
|
+
validateRecordQuery(query);
|
|
59
|
+
if (query.key !== undefined) return IDBKeyRange.only([query.collection, query.key]);
|
|
60
|
+
const prefix = query.prefix ?? '';
|
|
61
|
+
return IDBKeyRange.bound([query.collection, prefix], [query.collection, prefix + '\uffff']);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function decode(record: DiskRecord): StoredRecord {
|
|
65
|
+
return { collection: record.collection, key: record.key, revision: record.revision, value: requireObject(parseJson(record.value)) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function scan(store: IDBObjectStore, range: IDBKeyRange, reverse: boolean, visit: (cursor: IDBCursorWithValue) => boolean | void): Promise<void> {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const operation = store.openCursor(range, reverse ? 'prev' : 'next');
|
|
71
|
+
operation.onerror = () => reject(operation.error);
|
|
72
|
+
operation.onsuccess = () => {
|
|
73
|
+
const cursor = operation.result;
|
|
74
|
+
if (!cursor) { resolve(); return; }
|
|
75
|
+
try {
|
|
76
|
+
if (visit(cursor) === false) resolve();
|
|
77
|
+
else cursor.continue();
|
|
78
|
+
} catch (error) { reject(error); }
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** IndexedDB storage with atomic commits, cross-tab conflicts and durable snapshots. */
|
|
84
|
+
export class IndexedDbStore implements MeshlineStore {
|
|
85
|
+
private readonly gate = new AsyncGate();
|
|
86
|
+
private database: IDBDatabase | undefined;
|
|
87
|
+
private owner: string | undefined;
|
|
88
|
+
private releaseOwner: (() => void) | undefined;
|
|
89
|
+
private ownerLock: Promise<void> | undefined;
|
|
90
|
+
private disposed = false;
|
|
91
|
+
private invalidated = false;
|
|
92
|
+
private readonly readers = new Set<IndexedDbReader>();
|
|
93
|
+
|
|
94
|
+
constructor(readonly name: string, private readonly options: IndexedDbOptions = {}) {
|
|
95
|
+
if (!name.trim()) throw new TypeError('An IndexedDB database name is required.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
migrate(signal?: AbortSignal): Promise<void> {
|
|
99
|
+
return this.gate.run(async () => {
|
|
100
|
+
this.assertOpen();
|
|
101
|
+
if (this.database) throw new Error('Migrate before initializing the store.');
|
|
102
|
+
const database = await this.openDatabase(true, signal);
|
|
103
|
+
database.close();
|
|
104
|
+
}, signal);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
initialize(binding: StorageBinding, signal?: AbortSignal): Promise<void> {
|
|
108
|
+
return this.gate.run(async () => {
|
|
109
|
+
this.assertOpen();
|
|
110
|
+
const encoded = bindingJson(binding);
|
|
111
|
+
if (this.database) {
|
|
112
|
+
const actual = await transaction(this.database, ['meta'], 'readonly', async tx => (await request(tx.objectStore('meta').get('binding')) as { value: string }).value, signal);
|
|
113
|
+
if (actual !== encoded) throw new Error('Database is bound to another network or account.');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const locks = this.options.locks ?? globalThis.navigator?.locks;
|
|
117
|
+
if (!locks) throw new Error('Web Locks are required to manage snapshot ownership.');
|
|
118
|
+
const database = await this.openDatabase(false, signal);
|
|
119
|
+
const owner = encodeBase64Url((this.options.random ?? systemRandom).bytes(16));
|
|
120
|
+
let release!: () => void;
|
|
121
|
+
const lifetime = new Promise<void>(resolve => { release = resolve; });
|
|
122
|
+
let acquired!: () => void;
|
|
123
|
+
let failed!: (reason: unknown) => void;
|
|
124
|
+
const ready = new Promise<void>((resolve, reject) => { acquired = resolve; failed = reject; });
|
|
125
|
+
const ownerLock = locks.request(this.lockName(owner), async () => { acquired(); await lifetime; });
|
|
126
|
+
ownerLock.catch(failed);
|
|
127
|
+
try {
|
|
128
|
+
await ready;
|
|
129
|
+
await transaction(database, ['meta', 'owners'], 'readwrite', async tx => {
|
|
130
|
+
const meta = tx.objectStore('meta');
|
|
131
|
+
const existing = await request(meta.get('binding')) as { value: string } | undefined;
|
|
132
|
+
if (existing && existing.value !== encoded) throw new Error('Database is bound to another network or account.');
|
|
133
|
+
if (!existing) await request(meta.put({ key: 'binding', value: encoded }));
|
|
134
|
+
await request(tx.objectStore('owners').add({ id: owner }));
|
|
135
|
+
}, signal);
|
|
136
|
+
// Only a released owner lock permits cleanup; a suspended live tab keeps its snapshots.
|
|
137
|
+
const owners = await transaction(database, ['owners'], 'readonly', async tx => await request(tx.objectStore('owners').getAll()) as Owner[], signal);
|
|
138
|
+
for (const previous of owners) {
|
|
139
|
+
if (previous.id === owner) continue;
|
|
140
|
+
await locks.request(this.lockName(previous.id), { ifAvailable: true }, async lock => {
|
|
141
|
+
if (lock) await this.removeOwner(database, previous.id, signal);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
throwIfAborted(signal);
|
|
145
|
+
this.database = database;
|
|
146
|
+
this.owner = owner;
|
|
147
|
+
this.releaseOwner = release;
|
|
148
|
+
this.ownerLock = ownerLock;
|
|
149
|
+
database.onversionchange = () => { this.invalidated = true; database.close(); };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
database.close();
|
|
152
|
+
release();
|
|
153
|
+
await ownerLock;
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}, signal);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
read(queries: readonly RecordQuery[], signal?: AbortSignal): Promise<StoreSnapshot> {
|
|
160
|
+
const selections = queries.map(query => ({ range: keyRange(query), reverse: query.reverse ?? false }));
|
|
161
|
+
return this.gate.run(() => transaction(this.connection(), ['meta', 'records'], 'readonly', async tx => {
|
|
162
|
+
const version = (await request(tx.objectStore('meta').get('version')) as { value: number }).value;
|
|
163
|
+
const sets: StoredRecord[][] = [];
|
|
164
|
+
for (const query of selections) {
|
|
165
|
+
const values: StoredRecord[] = [];
|
|
166
|
+
await scan(tx.objectStore('records'), query.range, query.reverse, cursor => { values.push(decode(cursor.value as DiskRecord)); });
|
|
167
|
+
sets.push(values);
|
|
168
|
+
}
|
|
169
|
+
return { version, sets };
|
|
170
|
+
}, signal), signal);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
commit(expectedVersion: number, mutations: readonly StoreMutation[], signal?: AbortSignal): Promise<number> {
|
|
174
|
+
const changes = prepareCommit(expectedVersion, mutations);
|
|
175
|
+
return this.gate.run(() => transaction(this.connection(), ['meta', 'records'], 'readwrite', async tx => {
|
|
176
|
+
const meta = tx.objectStore('meta');
|
|
177
|
+
const version = (await request(meta.get('version')) as { value: number }).value;
|
|
178
|
+
if (version !== expectedVersion) throw new StateConflictError('Database revision changed before commit.');
|
|
179
|
+
const next = version + 1;
|
|
180
|
+
for (const change of changes) {
|
|
181
|
+
if (change.kind === 'put') await request(tx.objectStore('records').put({ collection: change.collection, key: change.key, revision: next, value: canonicalJson(change.value) }));
|
|
182
|
+
else await request(tx.objectStore('records').delete([change.collection, change.key]));
|
|
183
|
+
}
|
|
184
|
+
await request(meta.put({ key: 'version', value: next }));
|
|
185
|
+
return next;
|
|
186
|
+
}, signal), signal);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
openQuery(query: RecordQuery, signal?: AbortSignal): Promise<QueryReader<StoredRecord>> {
|
|
190
|
+
const range = keyRange(query);
|
|
191
|
+
const reverse = query.reverse ?? false;
|
|
192
|
+
return this.gate.run(async () => {
|
|
193
|
+
const database = this.connection();
|
|
194
|
+
const owner = this.owner!;
|
|
195
|
+
const id = encodeBase64Url((this.options.random ?? systemRandom).bytes(16));
|
|
196
|
+
await transaction(database, ['records', 'snapshots'], 'readwrite', async tx => {
|
|
197
|
+
let index = 0;
|
|
198
|
+
await scan(tx.objectStore('records'), range, reverse, cursor => {
|
|
199
|
+
tx.objectStore('snapshots').add({ owner, reader: id, index: index++, record: cursor.value as DiskRecord });
|
|
200
|
+
});
|
|
201
|
+
}, signal);
|
|
202
|
+
const reader = new IndexedDbReader(database, owner, id, () => this.readers.delete(reader));
|
|
203
|
+
this.readers.add(reader);
|
|
204
|
+
return reader;
|
|
205
|
+
}, signal);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
dispose(): Promise<void> {
|
|
209
|
+
return this.gate.run(async () => {
|
|
210
|
+
if (this.disposed) return;
|
|
211
|
+
this.disposed = true;
|
|
212
|
+
const errors: unknown[] = [];
|
|
213
|
+
for (const reader of this.readers) { try { await reader.dispose(); } catch (error) { errors.push(error); } }
|
|
214
|
+
try { if (this.database && this.owner && !this.invalidated) await this.removeOwner(this.database, this.owner); }
|
|
215
|
+
catch (error) { errors.push(error); }
|
|
216
|
+
finally {
|
|
217
|
+
this.database?.close();
|
|
218
|
+
this.database = undefined;
|
|
219
|
+
this.releaseOwner?.();
|
|
220
|
+
await this.ownerLock;
|
|
221
|
+
}
|
|
222
|
+
if (errors.length) throw new AggregateError(errors, 'Failed to dispose IndexedDB storage.');
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private removeOwner(database: IDBDatabase, id: string, signal?: AbortSignal): Promise<void> {
|
|
227
|
+
return transaction(database, ['owners', 'snapshots'], 'readwrite', async tx => {
|
|
228
|
+
await request(tx.objectStore('snapshots').delete(IDBKeyRange.bound([id, ''], [id, '\uffff'])));
|
|
229
|
+
await request(tx.objectStore('owners').delete(id));
|
|
230
|
+
}, signal);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private lockName(owner: string): string { return `meshline:${this.name}:snapshots:${owner}`; }
|
|
234
|
+
private assertOpen(): void {
|
|
235
|
+
if (this.disposed) throw new Error('Store is disposed.');
|
|
236
|
+
if (this.invalidated) throw new Error('Database was closed for an external schema change. Reopen the store.');
|
|
237
|
+
}
|
|
238
|
+
private connection(): IDBDatabase {
|
|
239
|
+
this.assertOpen();
|
|
240
|
+
if (!this.database) throw new Error('Store is not initialized.');
|
|
241
|
+
return this.database;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private openDatabase(migrate: boolean, signal?: AbortSignal): Promise<IDBDatabase> {
|
|
245
|
+
throwIfAborted(signal);
|
|
246
|
+
const factory = this.options.indexedDB ?? globalThis.indexedDB;
|
|
247
|
+
if (!factory) return Promise.reject(new Error('IndexedDB is unavailable.'));
|
|
248
|
+
return new Promise((resolve, reject) => {
|
|
249
|
+
const operation = factory.open(this.name, 1);
|
|
250
|
+
let rejected = false;
|
|
251
|
+
operation.onblocked = () => { rejected = true; reject(new Error('Database migration is blocked by another connection.')); };
|
|
252
|
+
operation.onupgradeneeded = () => {
|
|
253
|
+
if (!migrate || rejected || signal?.aborted) { operation.transaction!.abort(); return; }
|
|
254
|
+
const database = operation.result;
|
|
255
|
+
database.createObjectStore('meta', { keyPath: 'key' }).add({ key: 'version', value: 0 });
|
|
256
|
+
database.createObjectStore('records', { keyPath: ['collection', 'key'] });
|
|
257
|
+
database.createObjectStore('owners', { keyPath: 'id' });
|
|
258
|
+
database.createObjectStore('snapshots', { keyPath: ['owner', 'reader', 'index'] });
|
|
259
|
+
};
|
|
260
|
+
operation.onerror = () => reject(signal?.aborted ? signal.reason : operation.error ?? new Error('Explicit database migration is required.'));
|
|
261
|
+
operation.onsuccess = () => {
|
|
262
|
+
if (rejected || signal?.aborted) { operation.result.close(); reject(signal?.reason ?? new Error('Database open failed.')); }
|
|
263
|
+
else resolve(operation.result);
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
class IndexedDbReader implements QueryReader<StoredRecord> {
|
|
270
|
+
private readonly gate = new AsyncGate();
|
|
271
|
+
private offset = 0;
|
|
272
|
+
private disposed = false;
|
|
273
|
+
|
|
274
|
+
constructor(private readonly database: IDBDatabase, private readonly owner: string, private readonly reader: string, private readonly released: () => void) {}
|
|
275
|
+
|
|
276
|
+
readNext(count: number, signal?: AbortSignal): Promise<readonly StoredRecord[]> {
|
|
277
|
+
requireBatchCount(count);
|
|
278
|
+
return this.gate.run(async () => {
|
|
279
|
+
if (this.disposed) throw new Error('Reader is disposed.');
|
|
280
|
+
const values = await transaction(this.database, ['snapshots'], 'readonly', async tx => {
|
|
281
|
+
const values: StoredRecord[] = [];
|
|
282
|
+
await scan(tx.objectStore('snapshots'), IDBKeyRange.bound([this.owner, this.reader, this.offset], [this.owner, this.reader, Number.MAX_SAFE_INTEGER]), false, cursor => {
|
|
283
|
+
values.push(decode((cursor.value as SnapshotRow).record));
|
|
284
|
+
return values.length < count;
|
|
285
|
+
});
|
|
286
|
+
return values;
|
|
287
|
+
}, signal);
|
|
288
|
+
this.offset += values.length;
|
|
289
|
+
return values;
|
|
290
|
+
}, signal);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
dispose(): Promise<void> {
|
|
294
|
+
return this.gate.run(async () => {
|
|
295
|
+
if (this.disposed) return;
|
|
296
|
+
this.disposed = true;
|
|
297
|
+
try {
|
|
298
|
+
await transaction(this.database, ['snapshots'], 'readwrite', async tx => {
|
|
299
|
+
await request(tx.objectStore('snapshots').delete(IDBKeyRange.bound([this.owner, this.reader, 0], [this.owner, this.reader, Number.MAX_SAFE_INTEGER])));
|
|
300
|
+
});
|
|
301
|
+
} finally { this.released(); }
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|