@openlfcp/storage-idb 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -22,9 +22,9 @@ installs must never share sequence counters.
22
22
 
23
23
  Part of the OpenLFCP TypeScript SDK, which implements the OpenLFCP MVP 0.1
24
24
  subset of [LFCP-WIRE-01](https://github.com/openlfcp/spec/blob/main/wire/LFCP-WIRE-01.md)
25
- at `mvp-0.1-baseline.8`, not every deferred WIRE-01 feature. It does not
25
+ at `mvp-0.1-baseline.9`, not every deferred WIRE-01 feature. It does not
26
26
  claim full LFCP-WIRE-01 conformance. This is the MVP 0.1 release
27
- (`0.1.1`, npm dist-tag `latest`). Until 1.0, minor versions may change APIs.
27
+ (`0.1.2`, npm dist-tag `latest`). Until 1.0, minor versions may change APIs.
28
28
 
29
29
  ## Guarantees
30
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlfcp/storage-idb",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Durable portable storage for LFCP clients on IndexedDB (browsers, Electron, mobile WebViews).",
5
5
  "keywords": [
6
6
  "openlfcp",
@@ -42,8 +42,8 @@
42
42
  "tag": "latest"
43
43
  },
44
44
  "dependencies": {
45
- "@openlfcp/core": "^0.1.1",
46
- "@openlfcp/storage": "^0.1.1"
45
+ "@openlfcp/storage": "^0.1.2",
46
+ "@openlfcp/core": "^0.1.2"
47
47
  },
48
48
  "devDependencies": {
49
49
  "fake-indexeddb": "6.2.5"
package/dist/idb.d.ts DELETED
@@ -1,91 +0,0 @@
1
- import { type ActorSequence, type ControlRecordId, type DataEpoch, type DataUnitId, type Hash32, type PrincipalId, type ResourceId } from "@openlfcp/core";
2
- import { type ActorSequenceReservation, type CommitResult, type ControlConflictRow, type ControlHeadRow, type ControlRecordRow, type DataUnitRow, type DataUnitStatus, type EpochRow, type KeyPackageRow, type LfcpStorage, type OutboundItem, type ProfileCheckpoint, type ResourceRow, type RouteRow, type SeenRecord, type SnapshotRow, type SnapshotSequenceReservation, type StorageWrite, type StoredDataUnit, type SyncStateRow } from "@openlfcp/storage";
3
- /** A reservation that is durable and about to be returned. */
4
- export interface ReservedSequence {
5
- readonly kind: "actor" | "snapshot";
6
- /** The counter's key, as counters() lists it. */
7
- readonly key: string;
8
- readonly value: bigint;
9
- }
10
- export interface IdbStorageOptions {
11
- /** The IndexedDB factory (default: globalThis.indexedDB). */
12
- readonly indexedDB?: IDBFactory;
13
- /**
14
- * Called after a reservation is durable and before it is returned (e.g.
15
- * to mirror a high-water mark elsewhere). If it throws, the reservation
16
- * fails and its value is abandoned, never reissued.
17
- */
18
- readonly onReserved?: (reserved: ReservedSequence) => void;
19
- }
20
- export declare class IdbLfcpStorage implements LfcpStorage {
21
- #private;
22
- private constructor();
23
- /** Opens (creating or upgrading) the database `name`. */
24
- static open(name: string, options?: IdbStorageOptions): Promise<IdbLfcpStorage>;
25
- /** Closes the connection; later calls fail. */
26
- close(): void;
27
- commit(writes: readonly StorageWrite[]): Promise<CommitResult>;
28
- readonly actorSequences: ActorSequenceReservation;
29
- readonly snapshotSequences: SnapshotSequenceReservation;
30
- /** Every reservation counter: `actor:<resource>:<principal>` and `snapshot:<resource>:<epoch>:<publisher>`. */
31
- counters(): Promise<ReadonlyMap<string, bigint>>;
32
- /**
33
- * Adapter-local metadata for the application (e.g. an install marker):
34
- * structured-cloneable values, durable like everything else. Never secrets.
35
- */
36
- readonly meta: {
37
- get: (key: string) => Promise<unknown>;
38
- put: (key: string, value: unknown) => Promise<void>;
39
- };
40
- readonly control: {
41
- record: (id: ControlRecordId) => Promise<ControlRecordRow | undefined>;
42
- records: (resource: ResourceId) => Promise<ControlRecordRow[]>;
43
- head: (resource: ResourceId) => Promise<ControlHeadRow | undefined>;
44
- conflict: (resource: ResourceId) => Promise<ControlConflictRow | undefined>;
45
- epochs: (resource: ResourceId) => Promise<EpochRow[]>;
46
- };
47
- readonly dataUnits: {
48
- get: (id: DataUnitId) => Promise<StoredDataUnit | undefined>;
49
- at: (resource: ResourceId, actor: PrincipalId, seq: ActorSequence) => Promise<StoredDataUnit[]>;
50
- range: (resource: ResourceId, actor: PrincipalId, from: ActorSequence, to: ActorSequence) => Promise<StoredDataUnit[]>;
51
- withStatus: (resource: ResourceId, status: DataUnitStatus) => Promise<StoredDataUnit[]>;
52
- acceptedAt: (resource: ResourceId, actor: PrincipalId, seq: ActorSequence) => Promise<DataUnitId | undefined>;
53
- recordSeen: (unit: DataUnitRow) => Promise<SeenRecord>;
54
- };
55
- readonly keyPackages: {
56
- get: (id: Hash32) => Promise<KeyPackageRow | undefined>;
57
- list: (resource: ResourceId, filter?: {
58
- readonly epoch?: DataEpoch;
59
- readonly recipient?: PrincipalId;
60
- }) => Promise<KeyPackageRow[]>;
61
- };
62
- readonly snapshots: {
63
- get: (id: Hash32) => Promise<SnapshotRow | undefined>;
64
- list: (resource: ResourceId, filter?: {
65
- readonly epoch?: DataEpoch;
66
- }) => Promise<SnapshotRow[]>;
67
- };
68
- readonly resources: {
69
- get: (resource: ResourceId) => Promise<ResourceRow | undefined>;
70
- list: () => Promise<ResourceRow[]>;
71
- route: (resource: ResourceId) => Promise<RouteRow | undefined>;
72
- };
73
- readonly outbound: {
74
- list: (resource?: ResourceId) => Promise<OutboundItem[]>;
75
- get: (id: Hash32) => Promise<OutboundItem | undefined>;
76
- };
77
- readonly profileState: {
78
- checkpoint: (resource: ResourceId) => Promise<ProfileCheckpoint | undefined>;
79
- };
80
- readonly syncState: {
81
- get: (resource: ResourceId) => Promise<SyncStateRow | undefined>;
82
- };
83
- readonly localMarks: {
84
- get: (key: string) => Promise<string | undefined>;
85
- list: (prefix: string) => Promise<{
86
- key: string;
87
- value: string;
88
- }[]>;
89
- };
90
- }
91
- //# sourceMappingURL=idb.d.ts.map
package/dist/idb.js DELETED
@@ -1,541 +0,0 @@
1
- /// <reference lib="dom" />
2
- import { bytesEqual, LfcpError, toHex, UINT64_MAX, } from "@openlfcp/core";
3
- import { nextActorSequence, } from "@openlfcp/storage";
4
- /**
5
- * LfcpStorage on IndexedDB (LFCP-059): one database per client install,
6
- * named by the caller. Portable: browsers, Electron, mobile WebViews.
7
- *
8
- * - Atomicity: one commit() is one readwrite transaction over every object
9
- * store; the Control Head compare-and-set is read inside it, and any
10
- * failure aborts the whole transaction.
11
- * - Durability: every readwrite transaction asks for durability "strict"
12
- * (Chromium's default is relaxed), and a promise resolves only on the
13
- * transaction's complete event.
14
- * - Sequences: a reservation reads the counter, checks it against this
15
- * Principal's own stored objects (fail closed, SEQUENCE_REUSE) and writes
16
- * it in one strict transaction, then calls onReserved before returning.
17
- * - Exact bytes: signed objects are stored as their exact bytes; other
18
- * fields are indexes. Values are structured clones, so nothing returned
19
- * aliases a stored buffer or a caller's.
20
- *
21
- * A transaction only awaits IndexedDB requests (never other promises), so it
22
- * cannot auto-commit half way.
23
- */
24
- const VERSION = 1;
25
- const STORES = [
26
- "records",
27
- "heads",
28
- "conflicts",
29
- "epochs",
30
- "units",
31
- "keyPackages",
32
- "snapshots",
33
- "resources",
34
- "routes",
35
- "outbound",
36
- "checkpoints",
37
- "syncStates",
38
- "counters",
39
- "meta",
40
- ];
41
- const hex = toHex;
42
- /** uint64 as a fixed-width decimal string, so key order is numeric order. */
43
- const pad = (n) => n.toString().padStart(20, "0");
44
- /** Local marks share the meta store under this key prefix. */
45
- const MARK = "mark:";
46
- const LOW = "";
47
- const HIGH = "￿";
48
- const actorKey = (r, p) => `actor:${hex(r)}:${hex(p)}`;
49
- const snapshotKey = (r, e, p) => `snapshot:${hex(r)}:${e}:${hex(p)}`;
50
- const ORDER_KEY = "order:outbound";
51
- /** A deep copy, frozen; structured clones are already copies, this freezes them. */
52
- function own(value) {
53
- if (value instanceof Uint8Array)
54
- return Uint8Array.from(value);
55
- if (Array.isArray(value))
56
- return Object.freeze(value.map(own));
57
- if (value !== null && typeof value === "object")
58
- return Object.freeze(Object.fromEntries(Object.entries(value).map(([k, v]) => [k, own(v)])));
59
- return value;
60
- }
61
- const copyOf = (value) => value === undefined ? undefined : own(value);
62
- const byHex = (a, b) => {
63
- const [x, y] = [hex(a), hex(b)];
64
- return x < y ? -1 : x > y ? 1 : 0;
65
- };
66
- const byBig = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
67
- const UNIT_ORDER = (a, b) => byHex(a.actor, b.actor) || byBig(a.actorSeq, b.actorSeq) || byHex(a.unitId, b.unitId);
68
- function req(r) {
69
- return new Promise((resolve, reject) => {
70
- r.onsuccess = () => resolve(r.result);
71
- r.onerror = () => reject(r.error);
72
- });
73
- }
74
- const pickUnit = (u) => ({
75
- unitId: u.unitId,
76
- resourceId: u.resourceId,
77
- dataEpoch: u.dataEpoch,
78
- actor: u.actor,
79
- actorSeq: u.actorSeq,
80
- prevDataUnitId: u.prevDataUnitId,
81
- controlHead: u.controlHead,
82
- bytes: u.bytes,
83
- });
84
- const unitValue = (row) => ({
85
- r: hex(row.resourceId),
86
- a: hex(row.actor),
87
- s: pad(row.actorSeq),
88
- st: row.status,
89
- row,
90
- });
91
- class HeadMismatch {
92
- result;
93
- constructor(result) {
94
- this.result = result;
95
- }
96
- }
97
- function upgrade(db) {
98
- const plain = (name) => db.createObjectStore(name);
99
- db.createObjectStore("records").createIndex("r", "r");
100
- plain("heads");
101
- plain("conflicts");
102
- db.createObjectStore("epochs").createIndex("r", "r");
103
- const units = db.createObjectStore("units");
104
- units.createIndex("ras", ["r", "a", "s"]);
105
- units.createIndex("rst", ["r", "st"]);
106
- db.createObjectStore("keyPackages").createIndex("r", "r");
107
- const snapshots = db.createObjectStore("snapshots");
108
- snapshots.createIndex("r", "r");
109
- snapshots.createIndex("rep", ["r", "e", "p", "n"]);
110
- plain("resources");
111
- plain("routes");
112
- db.createObjectStore("outbound").createIndex("n", "n");
113
- plain("checkpoints");
114
- plain("syncStates");
115
- plain("counters");
116
- plain("meta");
117
- }
118
- export class IdbLfcpStorage {
119
- #db;
120
- #onReserved;
121
- constructor(db, options) {
122
- this.#db = db;
123
- this.#onReserved = options.onReserved;
124
- }
125
- /** Opens (creating or upgrading) the database `name`. */
126
- static async open(name, options = {}) {
127
- const factory = options.indexedDB ?? globalThis.indexedDB;
128
- if (factory === undefined)
129
- throw new LfcpError("UNSUPPORTED_VALUE", "IndexedDB is not available in this runtime");
130
- const open = factory.open(name, VERSION);
131
- open.onupgradeneeded = () => upgrade(open.result);
132
- const db = await new Promise((resolve, reject) => {
133
- open.onsuccess = () => resolve(open.result);
134
- open.onerror = () => reject(open.error);
135
- open.onblocked = () => reject(new LfcpError("UNSUPPORTED_VALUE", `IndexedDB ${name} is blocked by another connection`));
136
- });
137
- return new IdbLfcpStorage(db, options);
138
- }
139
- /** Closes the connection; later calls fail. */
140
- close() {
141
- this.#db.close();
142
- }
143
- async #tx(stores, mode, body) {
144
- const tx = mode === "readwrite"
145
- ? this.#db.transaction([...stores], mode, { durability: "strict" })
146
- : this.#db.transaction([...stores], mode);
147
- const done = new Promise((resolve, reject) => {
148
- tx.oncomplete = () => resolve();
149
- tx.onabort = () => reject(tx.error ?? new LfcpError("UNSUPPORTED_VALUE", "the transaction was aborted"));
150
- });
151
- let result;
152
- try {
153
- result = await body(tx);
154
- }
155
- catch (e) {
156
- try {
157
- tx.abort();
158
- }
159
- catch {
160
- // already aborting or finished
161
- }
162
- await done.catch(() => undefined);
163
- throw e;
164
- }
165
- await done;
166
- return result;
167
- }
168
- // -------------------------------------------------------------------------
169
- // Writes
170
- async commit(writes) {
171
- try {
172
- return await this.#tx(STORES, "readwrite", async (tx) => {
173
- for (const w of writes) {
174
- if (w.op !== "set-control-head")
175
- continue;
176
- const head = (await req(tx.objectStore("heads").get(hex(w.resourceId))));
177
- const current = head?.head ?? null;
178
- const matches = current === null
179
- ? w.expected === null
180
- : w.expected !== null && bytesEqual(current, w.expected);
181
- if (!matches)
182
- throw new HeadMismatch(Object.freeze({
183
- ok: false,
184
- reason: "CONTROL_HEAD_MISMATCH",
185
- resourceId: own(w.resourceId),
186
- current: current === null ? null : own(current),
187
- }));
188
- }
189
- for (const w of writes) {
190
- if (w.op !== "expect-previous-unit")
191
- continue;
192
- const [r, a] = [hex(w.resourceId), hex(w.actor)];
193
- let cursor = await req(tx
194
- .objectStore("units")
195
- .index("ras")
196
- .openCursor(IDBKeyRange.bound([r, a, LOW], [r, a, HIGH]), "prev"));
197
- while (cursor !== null && !cursor.value.row.accepted) {
198
- cursor.continue();
199
- cursor = await req(cursor.request);
200
- }
201
- const current = cursor === null ? null : cursor.value.row.unitId;
202
- const matches = current === null
203
- ? w.previous === null
204
- : w.previous !== null && bytesEqual(current, w.previous);
205
- if (!matches)
206
- throw new HeadMismatch(Object.freeze({
207
- ok: false,
208
- reason: "PREVIOUS_UNIT_MISMATCH",
209
- resourceId: own(w.resourceId),
210
- actor: own(w.actor),
211
- current: current === null ? null : own(current),
212
- }));
213
- }
214
- for (const w of writes)
215
- await this.#apply(tx, w);
216
- return Object.freeze({ ok: true });
217
- });
218
- }
219
- catch (e) {
220
- if (e instanceof HeadMismatch)
221
- return e.result;
222
- throw e;
223
- }
224
- }
225
- async #putImmutable(store, id, value, what) {
226
- const old = (await req(store.get(hex(id))));
227
- if (old !== undefined) {
228
- if (!bytesEqual(old.row.bytes, value.row.bytes))
229
- throw new LfcpError("INVALID_STRUCTURE", `${what} ${hex(id)} is stored with other bytes`);
230
- return;
231
- }
232
- await req(store.put(value, hex(id)));
233
- }
234
- async #knownUnit(tx, unitId) {
235
- const v = (await req(tx.objectStore("units").get(hex(unitId))));
236
- if (v === undefined)
237
- throw new LfcpError("INVALID_STRUCTURE", `no Data Unit ${hex(unitId)}`);
238
- return v.row;
239
- }
240
- async #apply(tx, w) {
241
- const s = (name) => tx.objectStore(name);
242
- switch (w.op) {
243
- case "put-control-records":
244
- for (const r of w.records)
245
- await this.#putImmutable(s("records"), r.recordId, { r: hex(r.resourceId), row: r }, "Control Record");
246
- return;
247
- case "set-control-head":
248
- await req(s("heads").put(w.head, hex(w.resourceId)));
249
- return;
250
- case "set-control-conflict":
251
- if (w.conflict === null)
252
- await req(s("conflicts").delete(hex(w.resourceId)));
253
- else
254
- await req(s("conflicts").put(w.conflict, hex(w.resourceId)));
255
- return;
256
- case "expect-previous-unit":
257
- return; // a precondition, checked by commit()
258
- case "put-epoch": {
259
- const key = `${hex(w.resourceId)}:${pad(BigInt(w.epoch.epoch))}`;
260
- // A null dekRef or closedBy never clears a stored one (read and write
261
- // in this transaction).
262
- const stored = (await req(s("epochs").get(key)));
263
- const row = {
264
- ...w.epoch,
265
- closedBy: w.epoch.closedBy ?? stored?.row.closedBy ?? null,
266
- dekRef: w.epoch.dekRef ?? stored?.row.dekRef ?? null,
267
- };
268
- await req(s("epochs").put({ r: hex(w.resourceId), row }, key));
269
- return;
270
- }
271
- case "put-data-unit": {
272
- const old = (await req(s("units").get(hex(w.unit.unitId))));
273
- if (old !== undefined && !bytesEqual(old.row.bytes, w.unit.bytes))
274
- throw new LfcpError("INVALID_STRUCTURE", `Data Unit ${hex(w.unit.unitId)} is stored with other bytes`);
275
- const row = {
276
- ...pickUnit(old?.row ?? w.unit),
277
- status: w.status,
278
- detail: w.detail ?? null,
279
- accepted: w.accepted ?? old?.row.accepted ?? false,
280
- };
281
- await req(s("units").put(unitValue(row), hex(w.unit.unitId)));
282
- return;
283
- }
284
- case "set-data-unit-status": {
285
- const old = await this.#knownUnit(tx, w.unitId);
286
- const row = { ...old, status: w.status, detail: w.detail ?? null };
287
- await req(s("units").put(unitValue(row), hex(w.unitId)));
288
- return;
289
- }
290
- case "set-accepted": {
291
- const old = await this.#knownUnit(tx, w.unitId);
292
- await req(s("units").put(unitValue({ ...old, accepted: w.accepted }), hex(w.unitId)));
293
- return;
294
- }
295
- case "put-key-package":
296
- await this.#putImmutable(s("keyPackages"), w.row.packageId, { r: hex(w.row.resourceId), row: w.row }, "Key Package");
297
- return;
298
- case "put-snapshot":
299
- await this.#putImmutable(s("snapshots"), w.row.snapshotId, {
300
- r: hex(w.row.resourceId),
301
- e: pad(BigInt(w.row.dataEpoch)),
302
- p: hex(w.row.publisher),
303
- n: pad(w.row.snapshotSeq),
304
- row: w.row,
305
- }, "Snapshot");
306
- return;
307
- case "delete-snapshot":
308
- await req(s("snapshots").delete(hex(w.snapshotId)));
309
- return;
310
- case "put-resource":
311
- await req(s("resources").put(w.row, hex(w.row.resourceId)));
312
- return;
313
- case "put-route":
314
- await req(s("routes").put(w.route, hex(w.resourceId)));
315
- return;
316
- case "enqueue": {
317
- const old = (await req(s("outbound").get(hex(w.item.itemId))));
318
- if (old !== undefined) {
319
- if (!bytesEqual(old.row.bytes, w.item.bytes))
320
- throw new LfcpError("INVALID_STRUCTURE", `outbound item ${hex(w.item.itemId)} is stored with other bytes`);
321
- return;
322
- }
323
- const n = (await req(s("counters").get(ORDER_KEY))) ?? 0;
324
- await req(s("counters").put(n + 1, ORDER_KEY));
325
- await req(s("outbound").put({ r: hex(w.item.resourceId), n: n + 1, row: w.item }, hex(w.item.itemId)));
326
- return;
327
- }
328
- case "update-outbound": {
329
- const old = (await req(s("outbound").get(hex(w.itemId))));
330
- if (old === undefined)
331
- throw new LfcpError("INVALID_STRUCTURE", `no outbound item ${hex(w.itemId)}`);
332
- const row = {
333
- ...old.row,
334
- ...(w.attempts === undefined ? {} : { attempts: w.attempts }),
335
- ...(w.lastAttempt === undefined ? {} : { lastAttempt: w.lastAttempt }),
336
- ...(w.nextAttempt === undefined ? {} : { nextAttempt: w.nextAttempt }),
337
- ...(w.blocked === undefined ? {} : { blocked: w.blocked }),
338
- };
339
- await req(s("outbound").put({ ...old, row }, hex(w.itemId)));
340
- return;
341
- }
342
- case "dequeue":
343
- await req(s("outbound").delete(hex(w.itemId)));
344
- return;
345
- case "put-profile-checkpoint":
346
- await req(s("checkpoints").put(w.checkpoint, hex(w.checkpoint.resourceId)));
347
- return;
348
- case "put-sync-state":
349
- await req(s("syncStates").put(w.row, hex(w.row.resourceId)));
350
- return;
351
- case "put-local-mark": {
352
- // Kept in the meta store under "mark:" (no schema change).
353
- const key = `${MARK}${w.key}`;
354
- if (w.value === null)
355
- await req(s("meta").delete(key));
356
- else
357
- await req(s("meta").put(w.value, key));
358
- return;
359
- }
360
- }
361
- }
362
- // -------------------------------------------------------------------------
363
- // Sequence reservations
364
- actorSequences = {
365
- reserveNext: async (resource, principal) => {
366
- const key = actorKey(resource, principal);
367
- const next = await this.#tx(["counters", "units"], "readwrite", async (tx) => {
368
- const last = (await req(tx.objectStore("counters").get(key)));
369
- const next = nextActorSequence(last);
370
- const [r, a] = [hex(resource), hex(principal)];
371
- const top = await req(tx
372
- .objectStore("units")
373
- .index("ras")
374
- .openCursor(IDBKeyRange.bound([r, a, LOW], [r, a, HIGH]), "prev"));
375
- const max = top === null ? 0n : top.value.row.actorSeq;
376
- // Fail closed (§9): a counter behind this Principal's own stored units
377
- // (a restored backup, a lost row) must never hand out a used sequence.
378
- if (next <= max)
379
- throw new LfcpError("SEQUENCE_REUSE", `the actor sequence state (${next - 1n}) is behind the stored units of this Principal (${max}); refusing to reserve (§9)`);
380
- await req(tx.objectStore("counters").put(next, key));
381
- return next;
382
- });
383
- this.#onReserved?.({ kind: "actor", key, value: next });
384
- return next;
385
- },
386
- };
387
- snapshotSequences = {
388
- reserveNext: async (resource, epoch, publisher) => {
389
- const key = snapshotKey(resource, epoch, publisher);
390
- const next = await this.#tx(["counters", "snapshots"], "readwrite", async (tx) => {
391
- const last = (await req(tx.objectStore("counters").get(key))) ?? 0n;
392
- if (last >= UINT64_MAX)
393
- throw new LfcpError("OUT_OF_RANGE", "the Snapshot Sequence space is exhausted (§29)");
394
- const [r, e, p] = [hex(resource), pad(BigInt(epoch)), hex(publisher)];
395
- const top = await req(tx
396
- .objectStore("snapshots")
397
- .index("rep")
398
- .openCursor(IDBKeyRange.bound([r, e, p, LOW], [r, e, p, HIGH]), "prev"));
399
- const max = top === null ? 0n : top.value.row.snapshotSeq;
400
- if (last + 1n <= max)
401
- throw new LfcpError("SEQUENCE_REUSE", `the Snapshot Sequence state (${last}) is behind the stored Snapshots of this publisher (${max}); refusing to reserve (§29)`);
402
- await req(tx.objectStore("counters").put(last + 1n, key));
403
- return last + 1n;
404
- });
405
- this.#onReserved?.({ kind: "snapshot", key, value: next });
406
- return next;
407
- },
408
- };
409
- /** Every reservation counter: `actor:<resource>:<principal>` and `snapshot:<resource>:<epoch>:<publisher>`. */
410
- async counters() {
411
- return this.#tx(["counters"], "readonly", async (tx) => {
412
- const store = tx.objectStore("counters");
413
- const [keys, values] = await Promise.all([req(store.getAllKeys()), req(store.getAll())]);
414
- const out = new Map();
415
- keys.forEach((k, i) => {
416
- if (typeof k === "string" && (k.startsWith("actor:") || k.startsWith("snapshot:")))
417
- out.set(k, values[i]);
418
- });
419
- return out;
420
- });
421
- }
422
- /**
423
- * Adapter-local metadata for the application (e.g. an install marker):
424
- * structured-cloneable values, durable like everything else. Never secrets.
425
- */
426
- meta = {
427
- get: (key) => this.#tx(["meta"], "readonly", async (tx) => copyOf(await req(tx.objectStore("meta").get(key)))),
428
- put: (key, value) => this.#tx(["meta"], "readwrite", async (tx) => {
429
- await req(tx.objectStore("meta").put(value, key));
430
- }),
431
- };
432
- // -------------------------------------------------------------------------
433
- // Readers
434
- #get(store, key) {
435
- return this.#tx([store], "readonly", async (tx) => {
436
- const v = (await req(tx.objectStore(store).get(key)));
437
- return copyOf(v);
438
- });
439
- }
440
- #byResource(store, resource) {
441
- return this.#tx([store], "readonly", async (tx) => (await req(tx.objectStore(store).index("r").getAll(hex(resource)))).map((v) => v.row));
442
- }
443
- #unitsWhere(range, index) {
444
- return this.#tx(["units"], "readonly", async (tx) => (await req(tx.objectStore("units").index(index).getAll(range))).map((v) => v.row));
445
- }
446
- #at(resource, actor, seq) {
447
- return this.#unitsWhere(IDBKeyRange.only([hex(resource), hex(actor), pad(seq)]), "ras").then((us) => us.sort((a, b) => byHex(a.unitId, b.unitId)));
448
- }
449
- control = {
450
- record: (id) => this.#get("records", hex(id)).then((v) => v?.row),
451
- records: async (resource) => (await this.#byResource("records", resource))
452
- .sort((a, b) => byBig(a.controlSeq, b.controlSeq) || byHex(a.recordId, b.recordId))
453
- .map(own),
454
- head: (resource) => this.#get("heads", hex(resource)),
455
- conflict: (resource) => this.#get("conflicts", hex(resource)),
456
- epochs: async (resource) => (await this.#byResource("epochs", resource))
457
- .sort((a, b) => byBig(a.epoch, b.epoch))
458
- .map(own),
459
- };
460
- dataUnits = {
461
- get: (id) => this.#get("units", hex(id)).then((v) => v?.row),
462
- at: async (resource, actor, seq) => (await this.#at(resource, actor, seq)).map(own),
463
- range: async (resource, actor, from, to) => {
464
- if (from > to)
465
- return [];
466
- const [r, a] = [hex(resource), hex(actor)];
467
- return (await this.#unitsWhere(IDBKeyRange.bound([r, a, pad(from)], [r, a, pad(to)]), "ras"))
468
- .sort(UNIT_ORDER)
469
- .map(own);
470
- },
471
- withStatus: async (resource, status) => (await this.#unitsWhere(IDBKeyRange.only([hex(resource), status]), "rst"))
472
- .sort(UNIT_ORDER)
473
- .map(own),
474
- acceptedAt: async (resource, actor, seq) => copyOf((await this.#at(resource, actor, seq)).find((u) => u.accepted)?.unitId),
475
- recordSeen: (unit) => this.#tx(["units"], "readwrite", async (tx) => {
476
- const store = tx.objectStore("units");
477
- const old = (await req(store.get(hex(unit.unitId))));
478
- if (old !== undefined && !bytesEqual(old.row.bytes, unit.bytes))
479
- throw new LfcpError("INVALID_STRUCTURE", `Data Unit ${hex(unit.unitId)} is stored with other bytes`);
480
- if (old === undefined)
481
- await req(store.put(unitValue({ ...pickUnit(unit), status: "seen", detail: null, accepted: false }), hex(unit.unitId)));
482
- const all = (await req(store
483
- .index("ras")
484
- .getAll(IDBKeyRange.only([hex(unit.resourceId), hex(unit.actor), pad(unit.actorSeq)]))));
485
- const unitIds = all.map((v) => own(v.row.unitId)).sort(byHex);
486
- return Object.freeze({ unitIds: Object.freeze(unitIds), firstSeen: old === undefined });
487
- }),
488
- };
489
- keyPackages = {
490
- get: (id) => this.#get("keyPackages", hex(id)).then((v) => v?.row),
491
- list: async (resource, filter = {}) => (await this.#byResource("keyPackages", resource))
492
- .filter((k) => (filter.epoch === undefined || k.dataEpoch === filter.epoch) &&
493
- (filter.recipient === undefined || bytesEqual(k.recipient, filter.recipient)))
494
- .sort((a, b) => byBig(a.dataEpoch, b.dataEpoch) || byHex(a.packageId, b.packageId))
495
- .map(own),
496
- };
497
- snapshots = {
498
- get: (id) => this.#get("snapshots", hex(id)).then((v) => v?.row),
499
- list: async (resource, filter = {}) => (await this.#byResource("snapshots", resource))
500
- .filter((x) => filter.epoch === undefined || x.dataEpoch === filter.epoch)
501
- .sort((a, b) => byBig(a.dataEpoch, b.dataEpoch) ||
502
- byHex(a.publisher, b.publisher) ||
503
- byBig(a.snapshotSeq, b.snapshotSeq))
504
- .map(own),
505
- };
506
- resources = {
507
- get: (resource) => this.#get("resources", hex(resource)),
508
- list: () => this.#tx(["resources"], "readonly", async (tx) => (await req(tx.objectStore("resources").getAll()))
509
- .sort((a, b) => byHex(a.resourceId, b.resourceId))
510
- .map(own)),
511
- route: (resource) => this.#get("routes", hex(resource)),
512
- };
513
- outbound = {
514
- list: (resource) => this.#tx(["outbound"], "readonly", async (tx) => (await req(tx.objectStore("outbound").index("n").getAll()))
515
- .filter((v) => resource === undefined || v.r === hex(resource))
516
- .map((v) => own(v.row))),
517
- get: (id) => this.#get("outbound", hex(id)).then((v) => v?.row),
518
- };
519
- profileState = {
520
- checkpoint: (resource) => this.#get("checkpoints", hex(resource)),
521
- };
522
- syncState = {
523
- get: (resource) => this.#get("syncStates", hex(resource)),
524
- };
525
- localMarks = {
526
- get: (key) => this.#get("meta", `${MARK}${key}`),
527
- list: (prefix) => this.#tx(["meta"], "readonly", async (tx) => {
528
- const range = IDBKeyRange.bound(`${MARK}${prefix}`, `${MARK}${prefix}${HIGH}`);
529
- const store = tx.objectStore("meta");
530
- const [keys, values] = await Promise.all([
531
- req(store.getAllKeys(range)),
532
- req(store.getAll(range)),
533
- ]);
534
- return keys.map((k, i) => ({
535
- key: k.slice(MARK.length),
536
- value: values[i],
537
- }));
538
- }),
539
- };
540
- }
541
- //# sourceMappingURL=idb.js.map
package/dist/index.d.ts DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * Durable portable storage for LFCP clients (LFCP-059): LfcpStorage on
3
- * IndexedDB, for browsers, Electron and mobile WebViews. The application
4
- * names the database (one per client install) and keeps secrets in its own
5
- * SecretStore.
6
- */
7
- export declare const PACKAGE = "@openlfcp/storage-idb";
8
- export { IdbLfcpStorage, type IdbStorageOptions, type ReservedSequence } from "./idb.js";
9
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * Durable portable storage for LFCP clients (LFCP-059): LfcpStorage on
3
- * IndexedDB, for browsers, Electron and mobile WebViews. The application
4
- * names the database (one per client install) and keeps secrets in its own
5
- * SecretStore.
6
- */
7
- export const PACKAGE = "@openlfcp/storage-idb";
8
- export { IdbLfcpStorage } from "./idb.js";
9
- //# sourceMappingURL=index.js.map