@minnowdb/core 0.6.9 → 0.7.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.
Files changed (103) hide show
  1. package/dist/block-format/block.js +254 -325
  2. package/dist/block-format/checksum.js +30 -52
  3. package/dist/block-format/codecs.js +112 -125
  4. package/dist/block-format/column.js +187 -205
  5. package/dist/block-format/index.js +5 -1
  6. package/dist/block-format/physical.js +371 -387
  7. package/dist/block-format/types.js +4 -1
  8. package/dist/block-format/unicode.js +30 -39
  9. package/dist/date-value.d.ts +5 -0
  10. package/dist/date-value.js +71 -41
  11. package/dist/engine/artifact-cache.js +106 -116
  12. package/dist/engine/batch.js +36 -44
  13. package/dist/engine/buffered-writer.js +156 -158
  14. package/dist/engine/byte-estimates.js +20 -22
  15. package/dist/engine/cache-limits.js +24 -25
  16. package/dist/engine/cancellation.js +5 -3
  17. package/dist/engine/catalog.js +64 -73
  18. package/dist/engine/client.js +715 -780
  19. package/dist/engine/database.js +15952 -18465
  20. package/dist/engine/defaults.js +104 -111
  21. package/dist/engine/errors.js +113 -128
  22. package/dist/engine/fts.js +233 -280
  23. package/dist/engine/group-index.js +287 -321
  24. package/dist/engine/index.js +23 -12
  25. package/dist/engine/join-index.js +177 -186
  26. package/dist/engine/keyed-live.js +168 -173
  27. package/dist/engine/live-api.js +0 -1
  28. package/dist/engine/live-equal.js +32 -37
  29. package/dist/engine/live.js +603 -647
  30. package/dist/engine/memory.js +109 -111
  31. package/dist/engine/optimizer.d.ts +19 -1
  32. package/dist/engine/optimizer.js +2642 -2515
  33. package/dist/engine/point-read.d.ts +4 -2
  34. package/dist/engine/point-read.js +121 -171
  35. package/dist/engine/query-api.js +12 -3
  36. package/dist/engine/query-cache.js +61 -69
  37. package/dist/engine/query.d.ts +45 -0
  38. package/dist/engine/query.js +8506 -9375
  39. package/dist/engine/result-wire.js +221 -252
  40. package/dist/engine/schema-wire.js +97 -112
  41. package/dist/engine/schema.js +1099 -1254
  42. package/dist/engine/sort-keys.js +315 -389
  43. package/dist/engine/sql-domains.d.ts +7 -1
  44. package/dist/engine/sql-domains.js +648 -734
  45. package/dist/engine/sql-driver.js +0 -1
  46. package/dist/engine/sql-functions.d.ts +11 -0
  47. package/dist/engine/sql-functions.js +1092 -0
  48. package/dist/engine/sql-json.js +199 -218
  49. package/dist/engine/sql-semantics.d.ts +25 -4
  50. package/dist/engine/sql-semantics.js +579 -513
  51. package/dist/engine/typed-live.js +271 -293
  52. package/dist/engine/vector.d.ts +7 -1
  53. package/dist/engine/vector.js +4513 -4580
  54. package/dist/engine/worker-host.d.ts +8 -63
  55. package/dist/engine/worker-host.js +28 -1130
  56. package/dist/engine/worker-indexeddb.d.ts +1 -0
  57. package/dist/engine/worker-indexeddb.js +3 -0
  58. package/dist/engine/worker-memory.d.ts +1 -0
  59. package/dist/engine/worker-memory.js +3 -0
  60. package/dist/engine/worker-opfs.d.ts +1 -0
  61. package/dist/engine/worker-opfs.js +3 -0
  62. package/dist/engine/worker-server.d.ts +93 -0
  63. package/dist/engine/worker-server.js +999 -0
  64. package/dist/engine/worker-store-indexeddb.d.ts +2 -0
  65. package/dist/engine/worker-store-indexeddb.js +11 -0
  66. package/dist/engine/worker-store-memory.d.ts +2 -0
  67. package/dist/engine/worker-store-memory.js +6 -0
  68. package/dist/engine/worker-store-opfs.d.ts +2 -0
  69. package/dist/engine/worker-store-opfs.js +9 -0
  70. package/dist/engine/worker.js +0 -12
  71. package/dist/engine/write-block-planner.js +93 -100
  72. package/dist/plan/index.js +15 -16
  73. package/dist/plan/model.d.ts +9 -1
  74. package/dist/plan/model.js +20 -27
  75. package/dist/storage/index.js +0 -12
  76. package/dist/storage/indexeddb.js +11776 -13171
  77. package/dist/storage/memory.js +1198 -1241
  78. package/dist/storage/opfs/files.js +238 -295
  79. package/dist/storage/opfs/index.js +9 -3
  80. package/dist/storage/opfs/leader.js +4386 -4856
  81. package/dist/storage/opfs/rpc.js +223 -259
  82. package/dist/storage/opfs/snapshot-ledger.js +219 -260
  83. package/dist/storage/opfs/store.js +884 -1003
  84. package/dist/storage/persistence.js +59 -74
  85. package/dist/storage/snapshot-stream.js +788 -826
  86. package/dist/storage/snapshot.js +36 -37
  87. package/dist/storage/toolkit/extents.js +444 -520
  88. package/dist/storage/toolkit/index.js +28 -29
  89. package/dist/storage/toolkit/record-core.js +5792 -6377
  90. package/dist/storage/toolkit/sync-file.js +34 -42
  91. package/dist/storage/toolkit/wal.js +92 -127
  92. package/dist/storage/toolkit/wire.js +232 -256
  93. package/dist/storage/types.js +2952 -3228
  94. package/dist/testing/block-store-conformance.js +1556 -1629
  95. package/dist/testing/index.js +310 -291
  96. package/dist/testing/opfs-shim.js +280 -312
  97. package/dist/testing/simulator.js +496 -534
  98. package/dist/testing/sqllogictest.js +425 -365
  99. package/dist/transactions/index.js +1464 -1712
  100. package/dist/worker-protocol/index.js +70 -69
  101. package/package.json +20 -2
  102. package/postgres-feature-profile.json +15 -5
  103. package/sql-feature-matrix.json +252 -2
@@ -1,1047 +1,928 @@
1
- import { assertStorageBulkReadItems, assertTempRunPageBatchLimits, OpfsUncertainOutcomeError, StorageCorruptionError, StorageFormatVersionError, validateStorageDatabaseName, } from "../types.js";
1
+ import { assertStorageBulkReadItems, assertTempRunPageBatchLimits, OpfsUncertainOutcomeError, StorageCorruptionError, StorageFormatVersionError, validateStorageDatabaseName } from "../types.js";
2
2
  import { validateTempRunPage, validateTempRunPageIdentity } from "../toolkit/record-core.js";
3
3
  import { OpfsTree, encodeSegment, isDomError } from "./files.js";
4
4
  import { LOG_FORMAT_VERSION } from "../toolkit/wire.js";
5
5
  import { OpfsLeader } from "./leader.js";
6
- import { rehydrateStoreError, estimateRpcValueBytes, fingerprintStoreRequest, MAX_OPFS_RPC_MESSAGE_BYTES, parseStoreRpcMessage, serializeStoreError, } from "./rpc.js";
7
- /** How long a follower waits for a leader's answer before assuming it is gone. */
8
- const RPC_TIMEOUT_MS = 1_000;
9
- /** How long a ping waits for a leadership announcement. */
6
+ import { rehydrateStoreError, estimateRpcValueBytes, fingerprintStoreRequest, MAX_OPFS_RPC_MESSAGE_BYTES, parseStoreRpcMessage, serializeStoreError } from "./rpc.js";
7
+ const RPC_TIMEOUT_MS = 1e3;
10
8
  const DISCOVERY_WAIT_MS = 150;
11
- /** Attempts across the discover → call → elect loop before an operation gives up. */
12
9
  const DISPATCH_ATTEMPTS = 10;
13
- /** A leader yields to a foreground bidder at most this often. */
14
- const YIELD_COOLDOWN_MS = 3_000;
15
- /** Results remembered for retried requests whose acknowledgement was lost. */
10
+ const YIELD_COOLDOWN_MS = 3e3;
16
11
  const DEDUPE_CACHE_SIZE = 512;
17
- /** Requests admitted concurrently by either side of the follower protocol. */
18
12
  const RPC_IN_FLIGHT_LIMIT = 512;
19
13
  const RPC_IN_FLIGHT_MUTATION_BYTES = 128 * 1024 * 1024;
20
14
  const RPC_IN_FLIGHT_READ_BYTES = 256 * 1024 * 1024;
21
15
  const RPC_SETTLED_OUTCOME_BYTES = 128 * 1024 * 1024;
22
16
  const RPC_SERVER_ADMISSION_LIMIT = 1024;
23
17
  const RPC_SERVER_ADMISSION_BYTES = 256 * 1024 * 1024;
24
- /** Channels into requesters' inboxes a leader keeps open between answers. */
25
18
  const ANSWER_CHANNEL_CACHE_SIZE = 64;
26
19
  const BULK_READ_ARGUMENT_INDEX = {
27
- getBlocks: 0,
28
- getTransactions: 0,
29
- getExistingUniqueKeys: 1,
30
- hasManifestBlocks: 1,
20
+ getBlocks: 0,
21
+ getTransactions: 0,
22
+ getExistingUniqueKeys: 1,
23
+ hasManifestBlocks: 1
31
24
  };
32
- /** Methods a follower may invoke on the leader. Temp-page IO is instance-local by design. */
33
- const RPC_METHODS = new Set([
34
- "getBlock",
35
- "getBlocks",
36
- "readManifestBlock",
37
- "hasManifestBlocks",
38
- "listManifestBlockPage",
39
- "listRetiredManifestBlockPage",
40
- "addTable",
41
- "getTable",
42
- "getTableByName",
43
- "listTables",
44
- "updateTable",
45
- "removeTable",
46
- "dropTable",
47
- "dropTableColumn",
48
- "removeFtsColumn",
49
- "writeFtsBase",
50
- "beginFtsBaseBuild",
51
- "renewFtsBaseBuild",
52
- "writeFtsBaseBuildChunk",
53
- "finishFtsBaseBuild",
54
- "abortFtsBaseBuild",
55
- "readFtsCandidates",
56
- "readFtsPostings",
57
- "getSegment",
58
- "listSegmentPage",
59
- "listTableSegmentPage",
60
- "removeAbortedSegment",
61
- "adoptAbortedSegment",
62
- "reserveRowIds",
63
- "reserveAutoIncrement",
64
- "getExistingUniqueKeys",
65
- "beginUniqueKeyBuild",
66
- "getUniqueKeyBuild",
67
- "renewUniqueKeyBuild",
68
- "appendUniqueKeyBuildChunk",
69
- "finishUniqueKeyBuild",
70
- "abortUniqueKeyBuild",
71
- "getCurrentManifest",
72
- "getCurrentManifestVersion",
73
- "getCatalogProbe",
74
- "getManifest",
75
- "listManifestPage",
76
- "createTransaction",
77
- "renewTransaction",
78
- "abortTransactionIfExpired",
79
- "beginTransaction",
80
- "getTransaction",
81
- "getTransactions",
82
- "listTransactionPage",
83
- "updateTransaction",
84
- "stageTransactionArtifacts",
85
- "rollbackTransactionArtifacts",
86
- "commitTransaction",
87
- "writeTransaction",
88
- "createLease",
89
- "getLease",
90
- "listLeases",
91
- "listExpiredLeasePage",
92
- "renewLease",
93
- "moveLease",
94
- "removeLeaseIfExpired",
95
- "removeLease",
96
- "createCompactionJob",
97
- "getCompactionJob",
98
- "listCompactionJobs",
99
- "listCompactionJobPage",
100
- "updateCompactionJob",
101
- "cancelCompactionJob",
102
- "removeCompactionJob",
103
- "createGarbageCollectionJob",
104
- "updateGarbageCollectionPlanning",
105
- "getGarbageCollectionJob",
106
- "listGarbageCollectionJobs",
107
- "listGarbageCollectionJobPage",
108
- "runGarbageCollectionStep",
109
- "removePrunedManifestRecords",
110
- "removeGarbageCollectionJob",
111
- "createTempOwner",
112
- "getTempOwner",
113
- "putTempRunPage",
114
- "putTempRunPages",
115
- "getTempRunPage",
116
- "removeTempRun",
117
- "renewTempOwner",
118
- "removeTempOwnerIfExpired",
119
- "removeTempOwner",
120
- "listTempOwnerIdsPage",
121
- "listExpiredTempOwnerPage",
122
- "getLogicalStorageBytes",
123
- "getStorageStats",
124
- "checkIntegrity",
125
- "beginSnapshotFrameExport",
126
- "readSnapshotExportFrame",
127
- "closeSnapshotFrameExport",
128
- "beginSnapshotFrameImport",
129
- "renewSnapshotFrameImport",
130
- "appendSnapshotImportFrames",
131
- "finishSnapshotFrameImport",
132
- "cancelSnapshotFrameImport",
25
+ const RPC_METHODS = /* @__PURE__ */ new Set([
26
+ "getBlock",
27
+ "getBlocks",
28
+ "readManifestBlock",
29
+ "hasManifestBlocks",
30
+ "listManifestBlockPage",
31
+ "listRetiredManifestBlockPage",
32
+ "addTable",
33
+ "getTable",
34
+ "getTableByName",
35
+ "listTables",
36
+ "updateTable",
37
+ "removeTable",
38
+ "dropTable",
39
+ "dropTableColumn",
40
+ "removeFtsColumn",
41
+ "writeFtsBase",
42
+ "beginFtsBaseBuild",
43
+ "renewFtsBaseBuild",
44
+ "writeFtsBaseBuildChunk",
45
+ "finishFtsBaseBuild",
46
+ "abortFtsBaseBuild",
47
+ "readFtsCandidates",
48
+ "readFtsPostings",
49
+ "getSegment",
50
+ "listSegmentPage",
51
+ "listTableSegmentPage",
52
+ "removeAbortedSegment",
53
+ "adoptAbortedSegment",
54
+ "reserveRowIds",
55
+ "reserveAutoIncrement",
56
+ "getExistingUniqueKeys",
57
+ "beginUniqueKeyBuild",
58
+ "getUniqueKeyBuild",
59
+ "renewUniqueKeyBuild",
60
+ "appendUniqueKeyBuildChunk",
61
+ "finishUniqueKeyBuild",
62
+ "abortUniqueKeyBuild",
63
+ "getCurrentManifest",
64
+ "getCurrentManifestVersion",
65
+ "getCatalogProbe",
66
+ "getManifest",
67
+ "listManifestPage",
68
+ "createTransaction",
69
+ "renewTransaction",
70
+ "abortTransactionIfExpired",
71
+ "beginTransaction",
72
+ "getTransaction",
73
+ "getTransactions",
74
+ "listTransactionPage",
75
+ "updateTransaction",
76
+ "stageTransactionArtifacts",
77
+ "rollbackTransactionArtifacts",
78
+ "commitTransaction",
79
+ "writeTransaction",
80
+ "createLease",
81
+ "getLease",
82
+ "listLeases",
83
+ "listExpiredLeasePage",
84
+ "renewLease",
85
+ "moveLease",
86
+ "removeLeaseIfExpired",
87
+ "removeLease",
88
+ "createCompactionJob",
89
+ "getCompactionJob",
90
+ "listCompactionJobs",
91
+ "listCompactionJobPage",
92
+ "updateCompactionJob",
93
+ "cancelCompactionJob",
94
+ "removeCompactionJob",
95
+ "createGarbageCollectionJob",
96
+ "updateGarbageCollectionPlanning",
97
+ "getGarbageCollectionJob",
98
+ "listGarbageCollectionJobs",
99
+ "listGarbageCollectionJobPage",
100
+ "runGarbageCollectionStep",
101
+ "removePrunedManifestRecords",
102
+ "removeGarbageCollectionJob",
103
+ "createTempOwner",
104
+ "getTempOwner",
105
+ "putTempRunPage",
106
+ "putTempRunPages",
107
+ "getTempRunPage",
108
+ "removeTempRun",
109
+ "renewTempOwner",
110
+ "removeTempOwnerIfExpired",
111
+ "removeTempOwner",
112
+ "listTempOwnerIdsPage",
113
+ "listExpiredTempOwnerPage",
114
+ "getLogicalStorageBytes",
115
+ "getStorageStats",
116
+ "checkIntegrity",
117
+ "beginSnapshotFrameExport",
118
+ "readSnapshotExportFrame",
119
+ "closeSnapshotFrameExport",
120
+ "beginSnapshotFrameImport",
121
+ "renewSnapshotFrameImport",
122
+ "appendSnapshotImportFrames",
123
+ "finishSnapshotFrameImport",
124
+ "cancelSnapshotFrameImport"
133
125
  ]);
134
- /**
135
- * Reads are idempotent — a lost-acknowledgement retry can simply run again — so only
136
- * mutations enter the dedupe cache, and read results (block bytes included) are never
137
- * retained on the leader.
138
- */
139
- const READ_METHODS = new Set([
140
- "getBlock",
141
- "getBlocks",
142
- "readManifestBlock",
143
- "hasManifestBlocks",
144
- "listManifestBlockPage",
145
- "listRetiredManifestBlockPage",
146
- "getTable",
147
- "getTableByName",
148
- "listTables",
149
- "readFtsCandidates",
150
- "readFtsPostings",
151
- "getSegment",
152
- "listSegmentPage",
153
- "listTableSegmentPage",
154
- "getExistingUniqueKeys",
155
- "getUniqueKeyBuild",
156
- "getCurrentManifest",
157
- "getCurrentManifestVersion",
158
- "getCatalogProbe",
159
- "getManifest",
160
- "listManifestPage",
161
- "getTransaction",
162
- "getTransactions",
163
- "listTransactionPage",
164
- "getLease",
165
- "listLeases",
166
- "listExpiredLeasePage",
167
- "getCompactionJob",
168
- "listCompactionJobs",
169
- "listCompactionJobPage",
170
- "getGarbageCollectionJob",
171
- "listGarbageCollectionJobs",
172
- "listGarbageCollectionJobPage",
173
- "getTempOwner",
174
- "getTempRunPage",
175
- "listTempOwnerIdsPage",
176
- "listExpiredTempOwnerPage",
177
- "getLogicalStorageBytes",
178
- "getStorageStats",
179
- "checkIntegrity",
180
- "readSnapshotExportFrame",
126
+ const READ_METHODS = /* @__PURE__ */ new Set([
127
+ "getBlock",
128
+ "getBlocks",
129
+ "readManifestBlock",
130
+ "hasManifestBlocks",
131
+ "listManifestBlockPage",
132
+ "listRetiredManifestBlockPage",
133
+ "getTable",
134
+ "getTableByName",
135
+ "listTables",
136
+ "readFtsCandidates",
137
+ "readFtsPostings",
138
+ "getSegment",
139
+ "listSegmentPage",
140
+ "listTableSegmentPage",
141
+ "getExistingUniqueKeys",
142
+ "getUniqueKeyBuild",
143
+ "getCurrentManifest",
144
+ "getCurrentManifestVersion",
145
+ "getCatalogProbe",
146
+ "getManifest",
147
+ "listManifestPage",
148
+ "getTransaction",
149
+ "getTransactions",
150
+ "listTransactionPage",
151
+ "getLease",
152
+ "listLeases",
153
+ "listExpiredLeasePage",
154
+ "getCompactionJob",
155
+ "listCompactionJobs",
156
+ "listCompactionJobPage",
157
+ "getGarbageCollectionJob",
158
+ "listGarbageCollectionJobs",
159
+ "listGarbageCollectionJobPage",
160
+ "getTempOwner",
161
+ "getTempRunPage",
162
+ "listTempOwnerIdsPage",
163
+ "listExpiredTempOwnerPage",
164
+ "getLogicalStorageBytes",
165
+ "getStorageStats",
166
+ "checkIntegrity",
167
+ "readSnapshotExportFrame"
181
168
  ]);
182
169
  function sameServedRequest(remembered, message, signature) {
183
- return remembered.method === message.method && remembered.signature === signature;
170
+ return remembered.method === message.method && remembered.signature === signature;
184
171
  }
185
- // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
186
- export class OpfsBlockStore {
187
- liveQueryChannelName;
188
- #tree;
189
- #durability;
190
- #checkpointEntries;
191
- #cleanupLimitBytes;
192
- #rpcTimeoutMs;
193
- #instanceId = crypto.randomUUID();
194
- #channelName;
195
- /** The database-wide channel: leadership traffic every connection listens to. */
196
- #channel;
197
- /** This connection's own channel: operations when leading, results when following. */
198
- #inbox;
199
- /** While leading: open channels into requesters' inboxes, by instance id. */
200
- #answerChannels = new Map();
201
- #leader;
202
- #knownLeader;
203
- #foreground = false;
204
- #closed = false;
205
- #lastYieldAt = 0;
206
- #electing;
207
- #pending = new Map();
208
- #inFlightMutations = new Map();
209
- #settledMutations = new Map();
210
- #servedRequestLocks = new Map();
211
- #inFlightMutationBytes = 0;
212
- #inFlightReadBytes = 0;
213
- #settledMutationBytes = 0;
214
- #pendingRpcBytes = 0;
215
- #servedRequestCount = 0;
216
- #servedRequestBytes = 0;
217
- #servedMutationGateForTests;
218
- #dropNextRpcResultForTests = false;
219
- #reacquireTimer;
220
- constructor(tree, options) {
221
- this.#tree = tree;
222
- this.#durability = options.durability ?? "strict";
223
- this.#checkpointEntries = options.checkpointEntries;
224
- this.#cleanupLimitBytes = options.cleanupLimitBytes;
225
- this.#rpcTimeoutMs = options.rpcTimeoutMs ?? RPC_TIMEOUT_MS;
226
- this.#channelName = `minnowdb-store:${options.name}`;
227
- this.liveQueryChannelName = `minnowdb-live:opfs:${options.name}`;
228
- }
229
- static async open(options) {
230
- const tree = new OpfsTree(await resolveDatabaseRoot(options));
231
- const store = new OpfsBlockStore(tree, options);
232
- await store.#ensureFormatMarker();
233
- if (typeof BroadcastChannel === "function") {
234
- const onMessage = (event) => {
235
- const message = parseStoreRpcMessage(event.data, RPC_METHODS);
236
- if (message !== undefined)
237
- store.#onMessage(message);
238
- };
239
- const channel = new BroadcastChannel(store.#channelName);
240
- channel.onmessage = onMessage;
241
- const inbox = new BroadcastChannel(store.#inboxName(store.#instanceId));
242
- inbox.onmessage = onMessage;
243
- store.#channel = channel;
244
- store.#inbox = inbox;
245
- }
246
- await store.#tryBecomeLeader();
247
- return store;
248
- }
249
- // ---------------------------------------------------------------------------------------
250
- // Leadership.
251
- // ---------------------------------------------------------------------------------------
252
- async #tryBecomeLeader() {
253
- if (this.#closed)
254
- return false;
255
- if (this.#leader !== undefined)
256
- return true;
257
- if (this.#electing !== undefined)
258
- return this.#electing;
259
- const election = this.#elect();
260
- this.#electing = election;
261
- try {
262
- return await election;
263
- }
264
- finally {
265
- this.#electing = undefined;
266
- }
267
- }
268
- async #elect() {
269
- let wal;
270
- try {
271
- wal = await this.#tree.openHandle(["wal"], { create: true });
272
- }
273
- catch (error) {
274
- if (isLockContention(error))
275
- return false;
276
- throw error;
277
- }
278
- let slotA;
279
- let slotB;
280
- try {
281
- // A dying ex-leader releases all its handles at once, but a graceful demotion closes
282
- // them in sequence; the brief retry covers the gap.
283
- slotA = await this.#openWithRetry(["checkpoint-a"]);
284
- slotB = await this.#openWithRetry(["checkpoint-b"]);
285
- this.#leader = await OpfsLeader.recover(this.#tree, this.#durability === "strict", { wal, slotA, slotB }, this.#checkpointEntries, this.#cleanupLimitBytes);
286
- }
287
- catch (error) {
288
- wal.close();
289
- slotA?.close();
290
- slotB?.close();
291
- if (isLockContention(error))
292
- return false;
293
- throw error;
294
- }
295
- if (this.#closed) {
296
- // close() ran while this election was in flight; a leader installed now would hold the
297
- // browser's file lock with no owner to ever release it.
298
- const leader = this.#leader;
299
- this.#leader = undefined;
300
- await leader.shutdown().catch(() => {
301
- leader.crash();
302
- });
303
- return false;
304
- }
305
- this.#knownLeader = this.#instanceId;
306
- this.#post({ kind: "leader", leaderId: this.#instanceId });
307
- return true;
308
- }
309
- async #openWithRetry(path) {
310
- for (let attempt = 0;; attempt += 1) {
311
- try {
312
- return await this.#tree.openHandle(path, { create: true });
313
- }
314
- catch (error) {
315
- if (isLockContention(error) && attempt < 40) {
316
- await sleep(5 + Math.random() * 10);
317
- continue;
318
- }
319
- throw error;
320
- }
321
- }
322
- }
323
- /** Marks this connection as the one the user is looking at — a leadership preference. */
324
- setForeground(foreground) {
325
- this.#foreground = foreground;
326
- if (foreground && this.#leader === undefined && !this.#closed) {
327
- this.#post({ kind: "bid", bidderId: this.#instanceId, foreground: true });
328
- }
329
- }
330
- async #yieldLeadership(to) {
331
- const leader = this.#leader;
332
- if (leader === undefined)
333
- return;
334
- this.#leader = undefined;
335
- this.#knownLeader = undefined;
336
- this.#lastYieldAt = Date.now();
337
- try {
338
- await leader.shutdown();
339
- }
340
- catch {
341
- // Whatever failed, the handles must not outlive the leadership; crash-close is
342
- // idempotent and releases them.
343
- leader.crash();
344
- }
345
- // An open channel into a follower's inbox would hear the next leader's answers to it.
346
- this.#closeAnswerChannels();
347
- this.#post({ kind: "yield", to });
348
- // If the bidder loses the race or vanishes, someone must still hold the database.
349
- if (this.#reacquireTimer !== undefined)
350
- clearTimeout(this.#reacquireTimer);
351
- this.#reacquireTimer = setTimeout(() => {
352
- if (this.#knownLeader === undefined && !this.#closed)
353
- void this.#tryBecomeLeader();
354
- }, 1_500);
355
- }
356
- #onMessage(message) {
357
- if (this.#closed)
358
- return;
359
- switch (message.kind) {
360
- case "op": {
361
- if (this.#leader !== undefined) {
362
- const requestBytes = estimateRpcValueBytes(message.args);
363
- if (this.#servedRequestCount >= RPC_SERVER_ADMISSION_LIMIT ||
364
- this.#servedRequestBytes + requestBytes > RPC_SERVER_ADMISSION_BYTES) {
365
- this.#answer(message.from, {
366
- kind: "result",
367
- requestId: message.requestId,
368
- ok: false,
369
- error: { name: "Error", message: "The OPFS leader RPC queue is full" },
370
- });
371
- return;
372
- }
373
- this.#servedRequestCount += 1;
374
- this.#servedRequestBytes += requestBytes;
375
- void this.#serveOp(message).finally(() => {
376
- this.#servedRequestCount = Math.max(0, this.#servedRequestCount - 1);
377
- this.#servedRequestBytes = Math.max(0, this.#servedRequestBytes - requestBytes);
378
- });
379
- }
380
- return;
381
- }
382
- case "result": {
383
- const pending = this.#takePending(message.requestId);
384
- if (pending === undefined)
385
- return;
386
- clearTimeout(pending.timer);
387
- if (message.ok)
388
- pending.resolve(message.value);
389
- else
390
- pending.reject(rehydrateStoreError(message.error));
391
- return;
392
- }
393
- case "busy": {
394
- const pending = this.#pending.get(message.requestId);
395
- if (pending === undefined)
396
- return;
397
- clearTimeout(pending.timer);
398
- pending.timer = setTimeout(() => {
399
- const expired = this.#takePending(message.requestId);
400
- expired?.reject(RPC_TIMED_OUT);
401
- }, this.#rpcTimeoutMs);
402
- return;
403
- }
404
- case "leader": {
405
- this.#knownLeader = message.leaderId;
406
- if (this.#reacquireTimer !== undefined) {
407
- clearTimeout(this.#reacquireTimer);
408
- this.#reacquireTimer = undefined;
409
- }
410
- // Reads can move to the new leader. A mutation may already be durable on the old one;
411
- // never execute it twice merely because its acknowledgement was lost.
412
- for (const [requestId, pending] of this.#pending) {
413
- if (pending.sentTo === message.leaderId)
414
- continue;
415
- if (!READ_METHODS.has(pending.message.method)) {
416
- this.#takePending(requestId);
417
- clearTimeout(pending.timer);
418
- pending.reject(new OpfsUncertainOutcomeError(pending.message.method));
419
- continue;
420
- }
421
- pending.sentTo = message.leaderId;
422
- this.#send(message.leaderId, pending.message);
423
- }
424
- // The freshly announced leader may be background while we are what the user sees.
425
- if (this.#foreground && this.#leader === undefined) {
426
- this.#post({ kind: "bid", bidderId: this.#instanceId, foreground: true });
427
- }
428
- return;
429
- }
430
- case "ping": {
431
- if (this.#leader !== undefined) {
432
- this.#post({ kind: "leader", leaderId: this.#instanceId });
433
- }
434
- return;
435
- }
436
- case "bid": {
437
- if (this.#leader !== undefined &&
438
- !this.#foreground &&
439
- message.foreground &&
440
- message.bidderId !== this.#instanceId &&
441
- Date.now() - this.#lastYieldAt > YIELD_COOLDOWN_MS) {
442
- void this.#yieldLeadership(message.bidderId);
443
- }
444
- return;
445
- }
446
- case "yield": {
447
- if (message.to === this.#instanceId)
448
- void this.#tryBecomeLeader();
449
- return;
450
- }
451
- case "released": {
452
- if (this.#knownLeader === message.leaderId)
453
- this.#knownLeader = undefined;
454
- return;
455
- }
456
- }
457
- }
458
- #takePending(requestId) {
459
- const pending = this.#pending.get(requestId);
460
- if (pending === undefined)
461
- return undefined;
462
- this.#pending.delete(requestId);
463
- this.#pendingRpcBytes -= pending.retainedBytes;
464
- return pending;
465
- }
466
- async #serveOp(message) {
467
- const requestKey = `${String(message.from.length)}:${message.from}${message.requestId}`;
468
- // Once an identity is admitted, duplicates can compare their fingerprint immediately and
469
- // either attach to the exact execution or fail closed. The short lock below exists only for
470
- // the pre-admission fingerprint race between two first deliveries.
471
- if (this.#inFlightMutations.has(requestKey) || this.#settledMutations.has(requestKey)) {
472
- await this.#serveOpLocked(message, requestKey);
473
- return;
474
- }
475
- const previous = this.#servedRequestLocks.get(requestKey) ?? Promise.resolve();
476
- let release;
477
- const hold = new Promise((resolve) => {
478
- release = resolve;
479
- });
480
- const tail = previous.then(() => hold);
481
- this.#servedRequestLocks.set(requestKey, tail);
482
- await previous;
483
- try {
484
- await this.#serveOpLocked(message, requestKey);
485
- }
486
- finally {
487
- release();
488
- if (this.#servedRequestLocks.get(requestKey) === tail) {
489
- this.#servedRequestLocks.delete(requestKey);
490
- }
491
- }
172
+ class OpfsBlockStore {
173
+ liveQueryChannelName;
174
+ #tree;
175
+ #durability;
176
+ #checkpointEntries;
177
+ #cleanupLimitBytes;
178
+ #rpcTimeoutMs;
179
+ #instanceId = crypto.randomUUID();
180
+ #channelName;
181
+ #channel;
182
+ #inbox;
183
+ #answerChannels = /* @__PURE__ */ new Map();
184
+ #leader;
185
+ #knownLeader;
186
+ #foreground = false;
187
+ #closed = false;
188
+ #lastYieldAt = 0;
189
+ #electing;
190
+ #pending = /* @__PURE__ */ new Map();
191
+ #inFlightMutations = /* @__PURE__ */ new Map();
192
+ #settledMutations = /* @__PURE__ */ new Map();
193
+ #servedRequestLocks = /* @__PURE__ */ new Map();
194
+ #inFlightMutationBytes = 0;
195
+ #inFlightReadBytes = 0;
196
+ #settledMutationBytes = 0;
197
+ #pendingRpcBytes = 0;
198
+ #servedRequestCount = 0;
199
+ #servedRequestBytes = 0;
200
+ #servedMutationGateForTests;
201
+ #dropNextRpcResultForTests = false;
202
+ #reacquireTimer;
203
+ constructor(tree, options) {
204
+ this.#tree = tree;
205
+ this.#durability = options.durability ?? "strict";
206
+ this.#checkpointEntries = options.checkpointEntries;
207
+ this.#cleanupLimitBytes = options.cleanupLimitBytes;
208
+ this.#rpcTimeoutMs = options.rpcTimeoutMs ?? RPC_TIMEOUT_MS;
209
+ this.#channelName = `minnowdb-store:${options.name}`;
210
+ this.liveQueryChannelName = `minnowdb-live:opfs:${options.name}`;
211
+ }
212
+ static async open(options) {
213
+ const tree = new OpfsTree(await resolveDatabaseRoot(options));
214
+ const store = new OpfsBlockStore(tree, options);
215
+ await store.#ensureFormatMarker();
216
+ if (typeof BroadcastChannel === "function") {
217
+ const onMessage = (event) => {
218
+ const message = parseStoreRpcMessage(event.data, RPC_METHODS);
219
+ if (message !== void 0)
220
+ store.#onMessage(message);
221
+ };
222
+ const channel = new BroadcastChannel(store.#channelName);
223
+ channel.onmessage = onMessage;
224
+ const inbox = new BroadcastChannel(store.#inboxName(store.#instanceId));
225
+ inbox.onmessage = onMessage;
226
+ store.#channel = channel;
227
+ store.#inbox = inbox;
228
+ }
229
+ await store.#tryBecomeLeader();
230
+ return store;
231
+ }
232
+ async #tryBecomeLeader() {
233
+ if (this.#closed)
234
+ return false;
235
+ if (this.#leader !== void 0)
236
+ return true;
237
+ if (this.#electing !== void 0)
238
+ return this.#electing;
239
+ const election = this.#elect();
240
+ this.#electing = election;
241
+ try {
242
+ return await election;
243
+ } finally {
244
+ this.#electing = void 0;
492
245
  }
493
- async #serveOpLocked(message, requestKey) {
494
- const leader = this.#leader;
495
- if (leader === undefined || !RPC_METHODS.has(message.method))
496
- return;
497
- const isRead = READ_METHODS.has(message.method);
498
- let fingerprint;
499
- try {
500
- fingerprint = await fingerprintStoreRequest(message.method, message.args);
501
- }
502
- catch {
503
- return;
504
- }
505
- const remembered = isRead ? undefined : this.#settledMutations.get(requestKey);
506
- let settled;
507
- if (remembered !== undefined) {
508
- if (!sameServedRequest(remembered, message, fingerprint.signature)) {
509
- this.#rejectReusedRequestIdentity(message);
510
- return;
511
- }
512
- // Refresh bounded settled outcomes as an LRU. This is the lost-ack retry path.
513
- this.#settledMutations.delete(requestKey);
514
- this.#settledMutations.set(requestKey, remembered);
515
- settled = remembered.outcome;
516
- }
517
- else if (!isRead && this.#inFlightMutations.has(requestKey)) {
518
- const inFlight = this.#inFlightMutations.get(requestKey);
519
- if (inFlight === undefined)
520
- throw new Error("In-flight RPC identity disappeared");
521
- if (!sameServedRequest(inFlight, message, fingerprint.signature)) {
522
- this.#rejectReusedRequestIdentity(message);
523
- return;
524
- }
525
- // Never evict an in-flight mutation. A duplicate attaches to the one execution and resets
526
- // the requester's patience while it remains queued or running.
527
- this.#answer(message.from, { kind: "busy", requestId: message.requestId });
528
- settled = await inFlight.outcome;
529
- }
530
- else {
531
- if (!isRead &&
532
- (this.#inFlightMutations.size >= RPC_IN_FLIGHT_LIMIT ||
533
- this.#inFlightMutationBytes + fingerprint.retainedBytes > RPC_IN_FLIGHT_MUTATION_BYTES)) {
534
- this.#answer(message.from, {
535
- kind: "result",
536
- requestId: message.requestId,
537
- ok: false,
538
- error: { name: "Error", message: "The OPFS leader mutation queue is full" },
539
- });
540
- return;
541
- }
542
- if (isRead) {
543
- const reservation = Math.max(fingerprint.retainedBytes, MAX_OPFS_RPC_MESSAGE_BYTES);
544
- if (this.#inFlightReadBytes + reservation > RPC_IN_FLIGHT_READ_BYTES) {
545
- this.#answer(message.from, {
546
- kind: "result",
547
- requestId: message.requestId,
548
- ok: false,
549
- error: { name: "Error", message: "The OPFS leader read queue is full" },
550
- });
551
- return;
552
- }
553
- this.#inFlightReadBytes += reservation;
554
- try {
555
- settled = await this.#executeServedOpAfterGate(leader, message, true);
556
- }
557
- finally {
558
- this.#inFlightReadBytes = Math.max(0, this.#inFlightReadBytes - reservation);
559
- }
560
- }
561
- else {
562
- this.#inFlightMutationBytes += fingerprint.retainedBytes;
563
- const execution = this.#executeServedOpAfterGate(leader, message, false);
564
- const outcome = execution.then((result) => {
565
- this.#inFlightMutations.delete(requestKey);
566
- this.#inFlightMutationBytes = Math.max(0, this.#inFlightMutationBytes - fingerprint.retainedBytes);
567
- if (!this.#closed) {
568
- this.#rememberSettledMutation(requestKey, message.method, fingerprint, result);
569
- }
570
- return result;
571
- });
572
- this.#inFlightMutations.set(requestKey, {
573
- method: message.method,
574
- signature: fingerprint.signature,
575
- requestBytes: fingerprint.retainedBytes,
576
- outcome,
577
- });
578
- settled = await outcome;
579
- }
580
- }
581
- if (this.#dropNextRpcResultForTests) {
582
- this.#dropNextRpcResultForTests = false;
583
- return;
584
- }
585
- this.#answer(message.from, settled.ok
586
- ? { kind: "result", requestId: message.requestId, ok: true, value: settled.value }
587
- : {
588
- kind: "result",
589
- requestId: message.requestId,
590
- ok: false,
591
- error: settled.error ?? { name: "Error", message: "unknown" },
246
+ }
247
+ async #elect() {
248
+ let wal;
249
+ try {
250
+ wal = await this.#tree.openHandle(["wal"], { create: true });
251
+ } catch (error) {
252
+ if (isLockContention(error))
253
+ return false;
254
+ throw error;
255
+ }
256
+ let slotA;
257
+ let slotB;
258
+ try {
259
+ slotA = await this.#openWithRetry(["checkpoint-a"]);
260
+ slotB = await this.#openWithRetry(["checkpoint-b"]);
261
+ this.#leader = await OpfsLeader.recover(this.#tree, this.#durability === "strict", { wal, slotA, slotB }, this.#checkpointEntries, this.#cleanupLimitBytes);
262
+ } catch (error) {
263
+ wal.close();
264
+ slotA?.close();
265
+ slotB?.close();
266
+ if (isLockContention(error))
267
+ return false;
268
+ throw error;
269
+ }
270
+ if (this.#closed) {
271
+ const leader = this.#leader;
272
+ this.#leader = void 0;
273
+ await leader.shutdown().catch(() => {
274
+ leader.crash();
275
+ });
276
+ return false;
277
+ }
278
+ this.#knownLeader = this.#instanceId;
279
+ this.#post({ kind: "leader", leaderId: this.#instanceId });
280
+ return true;
281
+ }
282
+ async #openWithRetry(path) {
283
+ for (let attempt = 0; ; attempt += 1) {
284
+ try {
285
+ return await this.#tree.openHandle(path, { create: true });
286
+ } catch (error) {
287
+ if (isLockContention(error) && attempt < 40) {
288
+ await sleep(5 + Math.random() * 10);
289
+ continue;
290
+ }
291
+ throw error;
292
+ }
293
+ }
294
+ }
295
+ setForeground(foreground) {
296
+ this.#foreground = foreground;
297
+ if (foreground && this.#leader === void 0 && !this.#closed) {
298
+ this.#post({ kind: "bid", bidderId: this.#instanceId, foreground: true });
299
+ }
300
+ }
301
+ async #yieldLeadership(to) {
302
+ const leader = this.#leader;
303
+ if (leader === void 0)
304
+ return;
305
+ this.#leader = void 0;
306
+ this.#knownLeader = void 0;
307
+ this.#lastYieldAt = Date.now();
308
+ try {
309
+ await leader.shutdown();
310
+ } catch {
311
+ leader.crash();
312
+ }
313
+ this.#closeAnswerChannels();
314
+ this.#post({ kind: "yield", to });
315
+ if (this.#reacquireTimer !== void 0)
316
+ clearTimeout(this.#reacquireTimer);
317
+ this.#reacquireTimer = setTimeout(() => {
318
+ if (this.#knownLeader === void 0 && !this.#closed)
319
+ void this.#tryBecomeLeader();
320
+ }, 1500);
321
+ }
322
+ #onMessage(message) {
323
+ if (this.#closed)
324
+ return;
325
+ switch (message.kind) {
326
+ case "op": {
327
+ if (this.#leader !== void 0) {
328
+ const requestBytes = estimateRpcValueBytes(message.args);
329
+ if (this.#servedRequestCount >= RPC_SERVER_ADMISSION_LIMIT || this.#servedRequestBytes + requestBytes > RPC_SERVER_ADMISSION_BYTES) {
330
+ this.#answer(message.from, {
331
+ kind: "result",
332
+ requestId: message.requestId,
333
+ ok: false,
334
+ error: { name: "Error", message: "The OPFS leader RPC queue is full" }
592
335
  });
593
- }
594
- #rememberSettledMutation(requestKey, method, fingerprint, outcome) {
595
- const outcomeBytes = estimateRpcValueBytes(outcome);
596
- while (this.#settledMutations.size >= DEDUPE_CACHE_SIZE ||
597
- this.#settledMutationBytes + outcomeBytes > RPC_SETTLED_OUTCOME_BYTES) {
598
- const oldest = this.#settledMutations.keys().next().value;
599
- if (oldest === undefined)
600
- break;
601
- const removed = this.#settledMutations.get(oldest);
602
- this.#settledMutations.delete(oldest);
603
- this.#settledMutationBytes -= removed?.outcomeBytes ?? 0;
604
- }
605
- if (outcomeBytes > RPC_SETTLED_OUTCOME_BYTES)
606
336
  return;
607
- this.#settledMutations.set(requestKey, {
608
- method,
609
- signature: fingerprint.signature,
610
- requestBytes: fingerprint.retainedBytes,
611
- outcome,
612
- outcomeBytes,
613
- });
614
- this.#settledMutationBytes += outcomeBytes;
615
- }
616
- #rejectReusedRequestIdentity(message) {
337
+ }
338
+ this.#servedRequestCount += 1;
339
+ this.#servedRequestBytes += requestBytes;
340
+ void this.#serveOp(message).finally(() => {
341
+ this.#servedRequestCount = Math.max(0, this.#servedRequestCount - 1);
342
+ this.#servedRequestBytes = Math.max(0, this.#servedRequestBytes - requestBytes);
343
+ });
344
+ }
345
+ return;
346
+ }
347
+ case "result": {
348
+ const pending = this.#takePending(message.requestId);
349
+ if (pending === void 0)
350
+ return;
351
+ clearTimeout(pending.timer);
352
+ if (message.ok)
353
+ pending.resolve(message.value);
354
+ else
355
+ pending.reject(rehydrateStoreError(message.error));
356
+ return;
357
+ }
358
+ case "busy": {
359
+ const pending = this.#pending.get(message.requestId);
360
+ if (pending === void 0)
361
+ return;
362
+ clearTimeout(pending.timer);
363
+ pending.timer = setTimeout(() => {
364
+ const expired = this.#takePending(message.requestId);
365
+ expired?.reject(RPC_TIMED_OUT);
366
+ }, this.#rpcTimeoutMs);
367
+ return;
368
+ }
369
+ case "leader": {
370
+ this.#knownLeader = message.leaderId;
371
+ if (this.#reacquireTimer !== void 0) {
372
+ clearTimeout(this.#reacquireTimer);
373
+ this.#reacquireTimer = void 0;
374
+ }
375
+ for (const [requestId, pending] of this.#pending) {
376
+ if (pending.sentTo === message.leaderId)
377
+ continue;
378
+ if (!READ_METHODS.has(pending.message.method)) {
379
+ this.#takePending(requestId);
380
+ clearTimeout(pending.timer);
381
+ pending.reject(new OpfsUncertainOutcomeError(pending.message.method));
382
+ continue;
383
+ }
384
+ pending.sentTo = message.leaderId;
385
+ this.#send(message.leaderId, pending.message);
386
+ }
387
+ if (this.#foreground && this.#leader === void 0) {
388
+ this.#post({ kind: "bid", bidderId: this.#instanceId, foreground: true });
389
+ }
390
+ return;
391
+ }
392
+ case "ping": {
393
+ if (this.#leader !== void 0) {
394
+ this.#post({ kind: "leader", leaderId: this.#instanceId });
395
+ }
396
+ return;
397
+ }
398
+ case "bid": {
399
+ if (this.#leader !== void 0 && !this.#foreground && message.foreground && message.bidderId !== this.#instanceId && Date.now() - this.#lastYieldAt > YIELD_COOLDOWN_MS) {
400
+ void this.#yieldLeadership(message.bidderId);
401
+ }
402
+ return;
403
+ }
404
+ case "yield": {
405
+ if (message.to === this.#instanceId)
406
+ void this.#tryBecomeLeader();
407
+ return;
408
+ }
409
+ case "released": {
410
+ if (this.#knownLeader === message.leaderId)
411
+ this.#knownLeader = void 0;
412
+ return;
413
+ }
414
+ }
415
+ }
416
+ #takePending(requestId) {
417
+ const pending = this.#pending.get(requestId);
418
+ if (pending === void 0)
419
+ return void 0;
420
+ this.#pending.delete(requestId);
421
+ this.#pendingRpcBytes -= pending.retainedBytes;
422
+ return pending;
423
+ }
424
+ async #serveOp(message) {
425
+ const requestKey = `${String(message.from.length)}:${message.from}${message.requestId}`;
426
+ if (this.#inFlightMutations.has(requestKey) || this.#settledMutations.has(requestKey)) {
427
+ await this.#serveOpLocked(message, requestKey);
428
+ return;
429
+ }
430
+ const previous = this.#servedRequestLocks.get(requestKey) ?? Promise.resolve();
431
+ let release;
432
+ const hold = new Promise((resolve) => {
433
+ release = resolve;
434
+ });
435
+ const tail = previous.then(() => hold);
436
+ this.#servedRequestLocks.set(requestKey, tail);
437
+ await previous;
438
+ try {
439
+ await this.#serveOpLocked(message, requestKey);
440
+ } finally {
441
+ release();
442
+ if (this.#servedRequestLocks.get(requestKey) === tail) {
443
+ this.#servedRequestLocks.delete(requestKey);
444
+ }
445
+ }
446
+ }
447
+ async #serveOpLocked(message, requestKey) {
448
+ const leader = this.#leader;
449
+ if (leader === void 0 || !RPC_METHODS.has(message.method))
450
+ return;
451
+ const isRead = READ_METHODS.has(message.method);
452
+ let fingerprint;
453
+ try {
454
+ fingerprint = await fingerprintStoreRequest(message.method, message.args);
455
+ } catch {
456
+ return;
457
+ }
458
+ const remembered = isRead ? void 0 : this.#settledMutations.get(requestKey);
459
+ let settled;
460
+ if (remembered !== void 0) {
461
+ if (!sameServedRequest(remembered, message, fingerprint.signature)) {
462
+ this.#rejectReusedRequestIdentity(message);
463
+ return;
464
+ }
465
+ this.#settledMutations.delete(requestKey);
466
+ this.#settledMutations.set(requestKey, remembered);
467
+ settled = remembered.outcome;
468
+ } else if (!isRead && this.#inFlightMutations.has(requestKey)) {
469
+ const inFlight = this.#inFlightMutations.get(requestKey);
470
+ if (inFlight === void 0)
471
+ throw new Error("In-flight RPC identity disappeared");
472
+ if (!sameServedRequest(inFlight, message, fingerprint.signature)) {
473
+ this.#rejectReusedRequestIdentity(message);
474
+ return;
475
+ }
476
+ this.#answer(message.from, { kind: "busy", requestId: message.requestId });
477
+ settled = await inFlight.outcome;
478
+ } else {
479
+ if (!isRead && (this.#inFlightMutations.size >= RPC_IN_FLIGHT_LIMIT || this.#inFlightMutationBytes + fingerprint.retainedBytes > RPC_IN_FLIGHT_MUTATION_BYTES)) {
617
480
  this.#answer(message.from, {
481
+ kind: "result",
482
+ requestId: message.requestId,
483
+ ok: false,
484
+ error: { name: "Error", message: "The OPFS leader mutation queue is full" }
485
+ });
486
+ return;
487
+ }
488
+ if (isRead) {
489
+ const reservation = Math.max(fingerprint.retainedBytes, MAX_OPFS_RPC_MESSAGE_BYTES);
490
+ if (this.#inFlightReadBytes + reservation > RPC_IN_FLIGHT_READ_BYTES) {
491
+ this.#answer(message.from, {
618
492
  kind: "result",
619
493
  requestId: message.requestId,
620
494
  ok: false,
621
- error: {
622
- name: "Error",
623
- message: "The OPFS RPC request identity was reused with different contents",
624
- },
625
- });
626
- }
627
- async #executeServedOpAfterGate(leader, message, isRead) {
628
- const gate = this.#servedMutationGateForTests;
629
- if (!isRead && gate !== undefined)
630
- await gate;
631
- return this.#executeServedOp(leader, message);
632
- }
633
- async #executeServedOp(leader, message) {
634
- try {
635
- const method = leader[message.method];
636
- if (method === undefined) {
637
- return { ok: false, error: { name: "Error", message: "Unknown store operation" } };
638
- }
639
- return { ok: true, value: await method.apply(leader, message.args) };
640
- }
641
- catch (error) {
642
- return { ok: false, error: serializeStoreError(error) };
643
- }
644
- }
645
- /** Posts on the shared channel: leadership traffic, heard by every connection. */
646
- #post(message) {
647
- this.#channel?.postMessage(message);
648
- }
649
- /**
650
- * Posts an operation into the leader's inbox through a channel opened for this one message.
651
- * Every follower posts into the same inbox, and a `BroadcastChannel` hears whatever any other
652
- * object of its name posts — an outbound channel kept open would deliver every other
653
- * follower's operation (block bytes included) to this tab. Opened and closed around a single
654
- * message, nothing but the leader's inbox is ever listening on that name.
655
- */
656
- #send(leaderId, message) {
657
- // Once the channels are torn down nothing may be posted.
658
- if (this.#channel === undefined)
659
- return;
660
- this.#postOnce(leaderId, message);
661
- }
662
- /**
663
- * Posts an answer into the requester's inbox. Only the leader posts there, so the leader
664
- * keeps these channels open across requests and hears nothing through them. They close on
665
- * demotion: an ex-leader's open channel would hear the next leader's answers to that tab.
666
- */
667
- #answer(requesterId, message) {
668
- if (this.#channel === undefined)
669
- return;
670
- if (this.#leader === undefined) {
671
- // An answer finishing after demotion; do not reopen a channel that demotion just closed.
672
- this.#postOnce(requesterId, message);
673
- return;
495
+ error: { name: "Error", message: "The OPFS leader read queue is full" }
496
+ });
497
+ return;
674
498
  }
675
- let outbound = this.#answerChannels.get(requesterId);
676
- if (outbound === undefined) {
677
- outbound = new BroadcastChannel(this.#inboxName(requesterId));
678
- this.#answerChannels.set(requesterId, outbound);
679
- if (this.#answerChannels.size > ANSWER_CHANNEL_CACHE_SIZE) {
680
- // Followers leave without a goodbye; the oldest entry is the likeliest to be gone.
681
- const [oldest] = this.#answerChannels;
682
- if (oldest !== undefined) {
683
- this.#answerChannels.delete(oldest[0]);
684
- oldest[1].close();
685
- }
686
- }
687
- }
688
- outbound.postMessage(message);
689
- }
690
- #closeAnswerChannels() {
691
- for (const outbound of this.#answerChannels.values())
692
- outbound.close();
693
- this.#answerChannels.clear();
694
- }
695
- /** Posts one message into an inbox through a channel that lives only for that message. */
696
- #postOnce(instanceId, message) {
697
- const outbound = new BroadcastChannel(this.#inboxName(instanceId));
698
- outbound.postMessage(message);
699
- outbound.close();
700
- }
701
- #inboxName(instanceId) {
702
- return `${this.#channelName}:${instanceId}`;
703
- }
704
- /** Closes every channel; nothing is posted or delivered after this. */
705
- #closeChannels() {
706
- this.#channel?.close();
707
- this.#channel = undefined;
708
- this.#inbox?.close();
709
- this.#inbox = undefined;
710
- this.#closeAnswerChannels();
711
- }
712
- // ---------------------------------------------------------------------------------------
713
- // Dispatch: local when leading, RPC when following, elect when leaderless.
714
- // ---------------------------------------------------------------------------------------
715
- #assertOpen() {
716
- if (this.#closed)
717
- throw new Error("This OPFS store connection is closed");
718
- }
719
- async #dispatch(method, args) {
720
- this.#assertOpen();
721
- const requestId = crypto.randomUUID();
722
- for (let attempt = 0; attempt < DISPATCH_ATTEMPTS; attempt += 1) {
723
- // Re-checked each attempt: the awaits below (elections, RPC round trips) give close()
724
- // every opportunity to run.
725
- this.#assertOpen();
726
- const leader = this.#leader;
727
- if (leader !== undefined) {
728
- const bound = leader[method];
729
- if (bound === undefined)
730
- throw new Error(`Unknown store operation: ${method}`);
731
- return bound.apply(leader, args);
732
- }
733
- if (this.#channel === undefined) {
734
- if (await this.#tryBecomeLeader())
735
- continue;
736
- throw new Error("Another connection holds this OPFS database, and BroadcastChannel is unavailable to reach it");
737
- }
738
- if (!this.#leaderKnown()) {
739
- this.#post({ kind: "ping" });
740
- await sleep(DISCOVERY_WAIT_MS);
741
- if (!this.#leaderKnown()) {
742
- if (await this.#tryBecomeLeader())
743
- continue;
744
- await sleep(20 + Math.random() * 50);
745
- continue;
746
- }
747
- }
748
- try {
749
- return await this.#rpc(requestId, method, args);
750
- }
751
- catch (error) {
752
- if (error === RPC_TIMED_OUT) {
753
- this.#knownLeader = undefined;
754
- if (!READ_METHODS.has(method))
755
- throw new OpfsUncertainOutcomeError(method);
756
- continue;
757
- }
758
- throw error;
759
- }
760
- }
761
- throw new Error("The OPFS store could not reach or become a leader");
762
- }
763
- /** The channel can change #knownLeader between any two awaits; a method defeats narrowing. */
764
- #leaderKnown() {
765
- return this.#knownLeader !== undefined;
766
- }
767
- #rpc(requestId, method, args) {
768
- let retainedBytes;
499
+ this.#inFlightReadBytes += reservation;
769
500
  try {
770
- retainedBytes = estimateRpcValueBytes(args);
771
- }
772
- catch (error) {
773
- return Promise.reject(error instanceof Error ? error : new Error(String(error)));
774
- }
775
- if (this.#pending.size >= RPC_IN_FLIGHT_LIMIT ||
776
- this.#pendingRpcBytes + retainedBytes > RPC_IN_FLIGHT_MUTATION_BYTES) {
777
- return Promise.reject(new Error("The OPFS follower request queue is full"));
778
- }
779
- return new Promise((resolve, reject) => {
780
- const message = { kind: "op", requestId, from: this.#instanceId, method, args };
781
- const timer = setTimeout(() => {
782
- const expired = this.#takePending(requestId);
783
- expired?.reject(RPC_TIMED_OUT);
784
- }, this.#rpcTimeoutMs);
785
- const leaderId = this.#knownLeader;
786
- this.#pending.set(requestId, {
787
- message,
788
- retainedBytes,
789
- sentTo: leaderId,
790
- resolve,
791
- reject,
792
- timer,
793
- });
794
- this.#pendingRpcBytes += retainedBytes;
795
- // The dispatch loop only gets here with a leader known; should it have slipped away in
796
- // between, the timeout (or the next leader's announcement, which re-sends) takes over.
797
- if (leaderId !== undefined)
798
- this.#send(leaderId, message);
501
+ settled = await this.#executeServedOpAfterGate(leader, message, true);
502
+ } finally {
503
+ this.#inFlightReadBytes = Math.max(0, this.#inFlightReadBytes - reservation);
504
+ }
505
+ } else {
506
+ this.#inFlightMutationBytes += fingerprint.retainedBytes;
507
+ const execution = this.#executeServedOpAfterGate(leader, message, false);
508
+ const outcome = execution.then((result) => {
509
+ this.#inFlightMutations.delete(requestKey);
510
+ this.#inFlightMutationBytes = Math.max(0, this.#inFlightMutationBytes - fingerprint.retainedBytes);
511
+ if (!this.#closed) {
512
+ this.#rememberSettledMutation(requestKey, message.method, fingerprint, result);
513
+ }
514
+ return result;
799
515
  });
800
- }
801
- // ---------------------------------------------------------------------------------------
802
- // The BlockStore surface.
803
- // ---------------------------------------------------------------------------------------
804
- async putTempRunPage(page) {
805
- validateTempRunPage(page);
806
- await this.#dispatch("putTempRunPage", [page]);
807
- }
808
- async putTempRunPages(pages) {
809
- // Spill pages are instance-local files; the batch is a convenience, not a round-trip win.
810
- assertTempRunPageBatchLimits(pages);
811
- for (const page of pages)
812
- validateTempRunPage(page);
813
- await this.#dispatch("putTempRunPages", [pages]);
814
- }
815
- async getTempRunPage(ownerId, runId, pageIndex) {
816
- validateTempRunPageIdentity(ownerId, runId, pageIndex);
817
- return (await this.#dispatch("getTempRunPage", [ownerId, runId, pageIndex]));
818
- }
819
- async removeTempRun(ownerId, runId) {
820
- validateTempRunPageIdentity(ownerId, runId, 0);
821
- await this.#dispatch("removeTempRun", [ownerId, runId]);
822
- }
823
- /** @internal Shared implementation installed for every ordinary RPC method below. */
824
- async _dispatchGenerated(method, args) {
825
- const bulkIndex = BULK_READ_ARGUMENT_INDEX[method];
826
- if (bulkIndex !== undefined) {
827
- const bulkItems = args[bulkIndex];
828
- if (!Array.isArray(bulkItems))
829
- throw new TypeError(`${method} items must be an array`);
830
- assertStorageBulkReadItems(bulkItems, `${method} request`);
831
- }
832
- return this.#dispatch(method, args);
833
- }
834
- close() {
835
- if (this.#closed)
836
- return;
837
- this.#closed = true;
838
- if (this.#reacquireTimer !== undefined)
839
- clearTimeout(this.#reacquireTimer);
840
- for (const pending of this.#pending.values()) {
841
- clearTimeout(pending.timer);
842
- pending.reject(new Error("This OPFS store connection is closed"));
843
- }
844
- this.#pending.clear();
845
- this.#inFlightMutations.clear();
846
- this.#settledMutations.clear();
847
- this.#servedRequestLocks.clear();
848
- this.#pendingRpcBytes = 0;
849
- this.#inFlightMutationBytes = 0;
850
- this.#inFlightReadBytes = 0;
851
- this.#settledMutationBytes = 0;
852
- this.#servedRequestCount = 0;
853
- this.#servedRequestBytes = 0;
854
- const leader = this.#leader;
855
- this.#leader = undefined;
856
- if (leader !== undefined) {
857
- void leader
858
- .shutdown()
859
- .catch(() => {
860
- leader.crash();
861
- })
862
- .then(() => {
863
- this.#post({ kind: "released", leaderId: this.#instanceId });
864
- this.#closeChannels();
865
- });
866
- return;
867
- }
868
- this.#closeChannels();
869
- }
870
- /** Test-only: whether this connection currently holds the database's handles. */
871
- _isLeaderForTests() {
872
- return this.#leader !== undefined;
873
- }
874
- /** Test-only: the id that names this connection's inbox channel. */
875
- _instanceIdForTests() {
876
- return this.#instanceId;
877
- }
878
- /** Test-only: simulates an acknowledgement lost after the served operation settles. */
879
- _dropNextRpcResultForTests() {
880
- this.#dropNextRpcResultForTests = true;
881
- }
882
- /** Test-only: holds newly admitted served mutations until the returned release is called. */
883
- _holdServedMutationsForTests() {
884
- if (this.#servedMutationGateForTests !== undefined) {
885
- throw new Error("Served mutations are already held");
886
- }
887
- let release;
888
- this.#servedMutationGateForTests = new Promise((resolve) => {
889
- release = resolve;
516
+ this.#inFlightMutations.set(requestKey, {
517
+ method: message.method,
518
+ signature: fingerprint.signature,
519
+ requestBytes: fingerprint.retainedBytes,
520
+ outcome
890
521
  });
891
- return () => {
892
- this.#servedMutationGateForTests = undefined;
893
- release();
894
- };
895
- }
896
- /** Test-only: retransmits the oldest request with its stable deduplication identity. */
897
- _resendOldestPendingForTests() {
898
- const pending = this.#pending.values().next().value;
899
- if (pending?.sentTo !== undefined)
900
- this.#send(pending.sentTo, pending.message);
901
- }
902
- /** Test-only counters that pin the connection's bounded RPC state and close-time cleanup. */
903
- _residentStateForTests() {
904
- return {
905
- answerChannels: this.#answerChannels.size,
906
- dedupeEntries: this.#settledMutations.size,
907
- inFlightMutations: this.#inFlightMutations.size,
908
- pendingRequests: this.#pending.size,
909
- retainedRpcBytes: this.#pendingRpcBytes +
910
- this.#inFlightMutationBytes +
911
- this.#inFlightReadBytes +
912
- this.#settledMutationBytes +
913
- this.#servedRequestBytes,
914
- closed: this.#closed,
915
- };
916
- }
917
- /** Test-only: what tab death looks like — locks release, nothing flushes, no goodbyes. */
918
- _crashForTests() {
919
- this.#closed = true;
920
- if (this.#reacquireTimer !== undefined)
921
- clearTimeout(this.#reacquireTimer);
922
- for (const pending of this.#pending.values())
923
- clearTimeout(pending.timer);
924
- this.#pending.clear();
925
- this.#inFlightMutations.clear();
926
- this.#settledMutations.clear();
927
- this.#servedRequestLocks.clear();
928
- this.#pendingRpcBytes = 0;
929
- this.#inFlightMutationBytes = 0;
930
- this.#inFlightReadBytes = 0;
931
- this.#settledMutationBytes = 0;
932
- this.#servedRequestCount = 0;
933
- this.#servedRequestBytes = 0;
934
- this.#leader?.crash();
935
- this.#leader = undefined;
522
+ settled = await outcome;
523
+ }
524
+ }
525
+ if (this.#dropNextRpcResultForTests) {
526
+ this.#dropNextRpcResultForTests = false;
527
+ return;
528
+ }
529
+ this.#answer(message.from, settled.ok ? { kind: "result", requestId: message.requestId, ok: true, value: settled.value } : {
530
+ kind: "result",
531
+ requestId: message.requestId,
532
+ ok: false,
533
+ error: settled.error ?? { name: "Error", message: "unknown" }
534
+ });
535
+ }
536
+ #rememberSettledMutation(requestKey, method, fingerprint, outcome) {
537
+ const outcomeBytes = estimateRpcValueBytes(outcome);
538
+ while (this.#settledMutations.size >= DEDUPE_CACHE_SIZE || this.#settledMutationBytes + outcomeBytes > RPC_SETTLED_OUTCOME_BYTES) {
539
+ const oldest = this.#settledMutations.keys().next().value;
540
+ if (oldest === void 0)
541
+ break;
542
+ const removed = this.#settledMutations.get(oldest);
543
+ this.#settledMutations.delete(oldest);
544
+ this.#settledMutationBytes -= removed?.outcomeBytes ?? 0;
545
+ }
546
+ if (outcomeBytes > RPC_SETTLED_OUTCOME_BYTES)
547
+ return;
548
+ this.#settledMutations.set(requestKey, {
549
+ method,
550
+ signature: fingerprint.signature,
551
+ requestBytes: fingerprint.retainedBytes,
552
+ outcome,
553
+ outcomeBytes
554
+ });
555
+ this.#settledMutationBytes += outcomeBytes;
556
+ }
557
+ #rejectReusedRequestIdentity(message) {
558
+ this.#answer(message.from, {
559
+ kind: "result",
560
+ requestId: message.requestId,
561
+ ok: false,
562
+ error: {
563
+ name: "Error",
564
+ message: "The OPFS RPC request identity was reused with different contents"
565
+ }
566
+ });
567
+ }
568
+ async #executeServedOpAfterGate(leader, message, isRead) {
569
+ const gate = this.#servedMutationGateForTests;
570
+ if (!isRead && gate !== void 0)
571
+ await gate;
572
+ return this.#executeServedOp(leader, message);
573
+ }
574
+ async #executeServedOp(leader, message) {
575
+ try {
576
+ const method = leader[message.method];
577
+ if (method === void 0) {
578
+ return { ok: false, error: { name: "Error", message: "Unknown store operation" } };
579
+ }
580
+ return { ok: true, value: await method.apply(leader, message.args) };
581
+ } catch (error) {
582
+ return { ok: false, error: serializeStoreError(error) };
583
+ }
584
+ }
585
+ #post(message) {
586
+ this.#channel?.postMessage(message);
587
+ }
588
+ #send(leaderId, message) {
589
+ if (this.#channel === void 0)
590
+ return;
591
+ this.#postOnce(leaderId, message);
592
+ }
593
+ #answer(requesterId, message) {
594
+ if (this.#channel === void 0)
595
+ return;
596
+ if (this.#leader === void 0) {
597
+ this.#postOnce(requesterId, message);
598
+ return;
599
+ }
600
+ let outbound = this.#answerChannels.get(requesterId);
601
+ if (outbound === void 0) {
602
+ outbound = new BroadcastChannel(this.#inboxName(requesterId));
603
+ this.#answerChannels.set(requesterId, outbound);
604
+ if (this.#answerChannels.size > ANSWER_CHANNEL_CACHE_SIZE) {
605
+ const [oldest] = this.#answerChannels;
606
+ if (oldest !== void 0) {
607
+ this.#answerChannels.delete(oldest[0]);
608
+ oldest[1].close();
609
+ }
610
+ }
611
+ }
612
+ outbound.postMessage(message);
613
+ }
614
+ #closeAnswerChannels() {
615
+ for (const outbound of this.#answerChannels.values())
616
+ outbound.close();
617
+ this.#answerChannels.clear();
618
+ }
619
+ #postOnce(instanceId, message) {
620
+ const outbound = new BroadcastChannel(this.#inboxName(instanceId));
621
+ outbound.postMessage(message);
622
+ outbound.close();
623
+ }
624
+ #inboxName(instanceId) {
625
+ return `${this.#channelName}:${instanceId}`;
626
+ }
627
+ #closeChannels() {
628
+ this.#channel?.close();
629
+ this.#channel = void 0;
630
+ this.#inbox?.close();
631
+ this.#inbox = void 0;
632
+ this.#closeAnswerChannels();
633
+ }
634
+ #assertOpen() {
635
+ if (this.#closed)
636
+ throw new Error("This OPFS store connection is closed");
637
+ }
638
+ async #dispatch(method, args) {
639
+ this.#assertOpen();
640
+ const requestId = crypto.randomUUID();
641
+ for (let attempt = 0; attempt < DISPATCH_ATTEMPTS; attempt += 1) {
642
+ this.#assertOpen();
643
+ const leader = this.#leader;
644
+ if (leader !== void 0) {
645
+ const bound = leader[method];
646
+ if (bound === void 0)
647
+ throw new Error(`Unknown store operation: ${method}`);
648
+ return bound.apply(leader, args);
649
+ }
650
+ if (this.#channel === void 0) {
651
+ if (await this.#tryBecomeLeader())
652
+ continue;
653
+ throw new Error("Another connection holds this OPFS database, and BroadcastChannel is unavailable to reach it");
654
+ }
655
+ if (!this.#leaderKnown()) {
656
+ this.#post({ kind: "ping" });
657
+ await sleep(DISCOVERY_WAIT_MS);
658
+ if (!this.#leaderKnown()) {
659
+ if (await this.#tryBecomeLeader())
660
+ continue;
661
+ await sleep(20 + Math.random() * 50);
662
+ continue;
663
+ }
664
+ }
665
+ try {
666
+ return await this.#rpc(requestId, method, args);
667
+ } catch (error) {
668
+ if (error === RPC_TIMED_OUT) {
669
+ this.#knownLeader = void 0;
670
+ if (!READ_METHODS.has(method))
671
+ throw new OpfsUncertainOutcomeError(method);
672
+ continue;
673
+ }
674
+ throw error;
675
+ }
676
+ }
677
+ throw new Error("The OPFS store could not reach or become a leader");
678
+ }
679
+ #leaderKnown() {
680
+ return this.#knownLeader !== void 0;
681
+ }
682
+ #rpc(requestId, method, args) {
683
+ let retainedBytes;
684
+ try {
685
+ retainedBytes = estimateRpcValueBytes(args);
686
+ } catch (error) {
687
+ return Promise.reject(error instanceof Error ? error : new Error(String(error)));
688
+ }
689
+ if (this.#pending.size >= RPC_IN_FLIGHT_LIMIT || this.#pendingRpcBytes + retainedBytes > RPC_IN_FLIGHT_MUTATION_BYTES) {
690
+ return Promise.reject(new Error("The OPFS follower request queue is full"));
691
+ }
692
+ return new Promise((resolve, reject) => {
693
+ const message = { kind: "op", requestId, from: this.#instanceId, method, args };
694
+ const timer = setTimeout(() => {
695
+ const expired = this.#takePending(requestId);
696
+ expired?.reject(RPC_TIMED_OUT);
697
+ }, this.#rpcTimeoutMs);
698
+ const leaderId = this.#knownLeader;
699
+ this.#pending.set(requestId, {
700
+ message,
701
+ retainedBytes,
702
+ sentTo: leaderId,
703
+ resolve,
704
+ reject,
705
+ timer
706
+ });
707
+ this.#pendingRpcBytes += retainedBytes;
708
+ if (leaderId !== void 0)
709
+ this.#send(leaderId, message);
710
+ });
711
+ }
712
+ async putTempRunPage(page) {
713
+ validateTempRunPage(page);
714
+ await this.#dispatch("putTempRunPage", [page]);
715
+ }
716
+ async putTempRunPages(pages) {
717
+ assertTempRunPageBatchLimits(pages);
718
+ for (const page of pages)
719
+ validateTempRunPage(page);
720
+ await this.#dispatch("putTempRunPages", [pages]);
721
+ }
722
+ async getTempRunPage(ownerId, runId, pageIndex) {
723
+ validateTempRunPageIdentity(ownerId, runId, pageIndex);
724
+ return await this.#dispatch("getTempRunPage", [ownerId, runId, pageIndex]);
725
+ }
726
+ async removeTempRun(ownerId, runId) {
727
+ validateTempRunPageIdentity(ownerId, runId, 0);
728
+ await this.#dispatch("removeTempRun", [ownerId, runId]);
729
+ }
730
+ async _dispatchGenerated(method, args) {
731
+ const bulkIndex = BULK_READ_ARGUMENT_INDEX[method];
732
+ if (bulkIndex !== void 0) {
733
+ const bulkItems = args[bulkIndex];
734
+ if (!Array.isArray(bulkItems))
735
+ throw new TypeError(`${method} items must be an array`);
736
+ assertStorageBulkReadItems(bulkItems, `${method} request`);
737
+ }
738
+ return this.#dispatch(method, args);
739
+ }
740
+ close() {
741
+ if (this.#closed)
742
+ return;
743
+ this.#closed = true;
744
+ if (this.#reacquireTimer !== void 0)
745
+ clearTimeout(this.#reacquireTimer);
746
+ for (const pending of this.#pending.values()) {
747
+ clearTimeout(pending.timer);
748
+ pending.reject(new Error("This OPFS store connection is closed"));
749
+ }
750
+ this.#pending.clear();
751
+ this.#inFlightMutations.clear();
752
+ this.#settledMutations.clear();
753
+ this.#servedRequestLocks.clear();
754
+ this.#pendingRpcBytes = 0;
755
+ this.#inFlightMutationBytes = 0;
756
+ this.#inFlightReadBytes = 0;
757
+ this.#settledMutationBytes = 0;
758
+ this.#servedRequestCount = 0;
759
+ this.#servedRequestBytes = 0;
760
+ const leader = this.#leader;
761
+ this.#leader = void 0;
762
+ if (leader !== void 0) {
763
+ void leader.shutdown().catch(() => {
764
+ leader.crash();
765
+ }).then(() => {
766
+ this.#post({ kind: "released", leaderId: this.#instanceId });
936
767
  this.#closeChannels();
937
- }
938
- async #ensureFormatMarker() {
939
- const existing = await this.#tree.readFile(["format.json"], {
940
- lockedMeansAbsent: true,
941
- maxBytes: 1024,
768
+ });
769
+ return;
770
+ }
771
+ this.#closeChannels();
772
+ }
773
+ _isLeaderForTests() {
774
+ return this.#leader !== void 0;
775
+ }
776
+ _instanceIdForTests() {
777
+ return this.#instanceId;
778
+ }
779
+ _dropNextRpcResultForTests() {
780
+ this.#dropNextRpcResultForTests = true;
781
+ }
782
+ _holdServedMutationsForTests() {
783
+ if (this.#servedMutationGateForTests !== void 0) {
784
+ throw new Error("Served mutations are already held");
785
+ }
786
+ let release;
787
+ this.#servedMutationGateForTests = new Promise((resolve) => {
788
+ release = resolve;
789
+ });
790
+ return () => {
791
+ this.#servedMutationGateForTests = void 0;
792
+ release();
793
+ };
794
+ }
795
+ _resendOldestPendingForTests() {
796
+ const pending = this.#pending.values().next().value;
797
+ if (pending?.sentTo !== void 0)
798
+ this.#send(pending.sentTo, pending.message);
799
+ }
800
+ _residentStateForTests() {
801
+ return {
802
+ answerChannels: this.#answerChannels.size,
803
+ dedupeEntries: this.#settledMutations.size,
804
+ inFlightMutations: this.#inFlightMutations.size,
805
+ pendingRequests: this.#pending.size,
806
+ retainedRpcBytes: this.#pendingRpcBytes + this.#inFlightMutationBytes + this.#inFlightReadBytes + this.#settledMutationBytes + this.#servedRequestBytes,
807
+ closed: this.#closed
808
+ };
809
+ }
810
+ _crashForTests() {
811
+ this.#closed = true;
812
+ if (this.#reacquireTimer !== void 0)
813
+ clearTimeout(this.#reacquireTimer);
814
+ for (const pending of this.#pending.values())
815
+ clearTimeout(pending.timer);
816
+ this.#pending.clear();
817
+ this.#inFlightMutations.clear();
818
+ this.#settledMutations.clear();
819
+ this.#servedRequestLocks.clear();
820
+ this.#pendingRpcBytes = 0;
821
+ this.#inFlightMutationBytes = 0;
822
+ this.#inFlightReadBytes = 0;
823
+ this.#settledMutationBytes = 0;
824
+ this.#servedRequestCount = 0;
825
+ this.#servedRequestBytes = 0;
826
+ this.#leader?.crash();
827
+ this.#leader = void 0;
828
+ this.#closeChannels();
829
+ }
830
+ async #ensureFormatMarker() {
831
+ const existing = await this.#tree.readFile(["format.json"], {
832
+ lockedMeansAbsent: true,
833
+ maxBytes: 1024
834
+ });
835
+ if (existing !== void 0) {
836
+ try {
837
+ this.#validateFormatMarker(existing);
838
+ return;
839
+ } catch (error) {
840
+ if (!(error instanceof SyntaxError))
841
+ throw error;
842
+ }
843
+ }
844
+ let artifact;
845
+ for await (const name of this.#tree.iterateNames([])) {
846
+ if (name !== "format.json") {
847
+ artifact = name;
848
+ break;
849
+ }
850
+ }
851
+ if (artifact !== void 0) {
852
+ throw new StorageCorruptionError("opfs", "format.json", `The OPFS format marker is ${existing === void 0 ? "missing" : "torn"}, but the database directory already contains storage artifacts (${artifact}). Refusing to guess their layout version.`);
853
+ }
854
+ const bytes = new TextEncoder().encode(JSON.stringify({ formatVersion: LOG_FORMAT_VERSION }));
855
+ try {
856
+ await this.#tree.writeFile(["format.json"], bytes, { flush: true });
857
+ } catch (error) {
858
+ if (!isLockContention(error))
859
+ throw error;
860
+ for (let attempt = 0; attempt < 40; attempt += 1) {
861
+ await sleep(5 + Math.random() * 10);
862
+ const published = await this.#tree.readFile(["format.json"], {
863
+ lockedMeansAbsent: true,
864
+ maxBytes: 1024
942
865
  });
943
- if (existing !== undefined) {
944
- try {
945
- this.#validateFormatMarker(existing);
946
- return;
947
- }
948
- catch (error) {
949
- if (!(error instanceof SyntaxError))
950
- throw error;
951
- // A torn marker from a crashed first open is repairable only while no database
952
- // artifacts exist. Never stamp the current version over an unversioned WAL.
953
- }
954
- }
955
- let artifact;
956
- for await (const name of this.#tree.iterateNames([])) {
957
- if (name !== "format.json") {
958
- artifact = name;
959
- break;
960
- }
961
- }
962
- if (artifact !== undefined) {
963
- throw new StorageCorruptionError("opfs", "format.json", `The OPFS format marker is ${existing === undefined ? "missing" : "torn"}, but the ` +
964
- `database directory already contains storage artifacts (${artifact}). ` +
965
- `Refusing to guess their layout version.`);
966
- }
967
- const bytes = new TextEncoder().encode(JSON.stringify({ formatVersion: LOG_FORMAT_VERSION }));
968
- try {
969
- await this.#tree.writeFile(["format.json"], bytes, { flush: true });
970
- }
971
- catch (error) {
972
- if (!isLockContention(error))
973
- throw error;
974
- // Another opener owns the marker write. Do not assume it writes this build's version:
975
- // wait until its handle closes, then validate the bytes it actually published.
976
- for (let attempt = 0; attempt < 40; attempt += 1) {
977
- await sleep(5 + Math.random() * 10);
978
- const published = await this.#tree.readFile(["format.json"], {
979
- lockedMeansAbsent: true,
980
- maxBytes: 1024,
981
- });
982
- if (published === undefined)
983
- continue;
984
- this.#validateFormatMarker(published);
985
- return;
986
- }
987
- throw new Error("A concurrent opener did not publish a readable OPFS format marker", {
988
- cause: error,
989
- });
990
- }
991
- }
992
- #validateFormatMarker(bytes) {
993
- const text = new TextDecoder().decode(bytes);
994
- const parsed = JSON.parse(text);
995
- if (typeof parsed !== "object" ||
996
- parsed === null ||
997
- Array.isArray(parsed) ||
998
- !Number.isSafeInteger(parsed.formatVersion)) {
999
- throw new StorageCorruptionError("opfs", "format.json", "The OPFS format marker is invalid: formatVersion must be a safe integer");
1000
- }
1001
- const formatVersion = parsed.formatVersion;
1002
- if (formatVersion !== LOG_FORMAT_VERSION) {
1003
- throw new StorageFormatVersionError("opfs", "format.json", formatVersion, LOG_FORMAT_VERSION, formatVersion < LOG_FORMAT_VERSION ? "older" : "newer");
1004
- }
1005
- if (Object.keys(parsed).length !== 1 ||
1006
- !Object.hasOwn(parsed, "formatVersion") ||
1007
- text !== JSON.stringify({ formatVersion })) {
1008
- throw new StorageCorruptionError("opfs", "format.json", "The OPFS format marker is not the canonical layout marker");
1009
- }
1010
- }
866
+ if (published === void 0)
867
+ continue;
868
+ this.#validateFormatMarker(published);
869
+ return;
870
+ }
871
+ throw new Error("A concurrent opener did not publish a readable OPFS format marker", {
872
+ cause: error
873
+ });
874
+ }
875
+ }
876
+ #validateFormatMarker(bytes) {
877
+ const text = new TextDecoder().decode(bytes);
878
+ const parsed = JSON.parse(text);
879
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || !Number.isSafeInteger(parsed.formatVersion)) {
880
+ throw new StorageCorruptionError("opfs", "format.json", "The OPFS format marker is invalid: formatVersion must be a safe integer");
881
+ }
882
+ const formatVersion = parsed.formatVersion;
883
+ if (formatVersion !== LOG_FORMAT_VERSION) {
884
+ throw new StorageFormatVersionError("opfs", "format.json", formatVersion, LOG_FORMAT_VERSION, formatVersion < LOG_FORMAT_VERSION ? "older" : "newer");
885
+ }
886
+ if (Object.keys(parsed).length !== 1 || !Object.hasOwn(parsed, "formatVersion") || text !== JSON.stringify({ formatVersion })) {
887
+ throw new StorageCorruptionError("opfs", "format.json", "The OPFS format marker is not the canonical layout marker");
888
+ }
889
+ }
1011
890
  }
1012
891
  for (const method of RPC_METHODS) {
1013
- if (method in OpfsBlockStore.prototype)
1014
- continue;
1015
- Object.defineProperty(OpfsBlockStore.prototype, method, {
1016
- configurable: true,
1017
- value(...args) {
1018
- return this._dispatchGenerated(method, args);
1019
- },
1020
- });
892
+ if (method in OpfsBlockStore.prototype)
893
+ continue;
894
+ Object.defineProperty(OpfsBlockStore.prototype, method, {
895
+ configurable: true,
896
+ value(...args) {
897
+ return this._dispatchGenerated(method, args);
898
+ }
899
+ });
1021
900
  }
1022
- /** Removes every file of a database created by `OpfsBlockStore.open` under this name. */
1023
- export async function deleteOpfsDatabase(options) {
1024
- const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
1025
- const root = options.root ?? (await navigator.storage.getDirectory());
1026
- try {
1027
- const namespace = await root.getDirectoryHandle("minnowdb");
1028
- await namespace.removeEntry(encodedName, { recursive: true });
1029
- }
1030
- catch (error) {
1031
- if (!isDomError(error, "NotFoundError"))
1032
- throw error;
1033
- }
901
+ async function deleteOpfsDatabase(options) {
902
+ const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
903
+ const root = options.root ?? await navigator.storage.getDirectory();
904
+ try {
905
+ const namespace = await root.getDirectoryHandle("minnowdb");
906
+ await namespace.removeEntry(encodedName, { recursive: true });
907
+ } catch (error) {
908
+ if (!isDomError(error, "NotFoundError"))
909
+ throw error;
910
+ }
1034
911
  }
1035
912
  const RPC_TIMED_OUT = new Error("The leader did not answer in time");
1036
913
  async function resolveDatabaseRoot(options) {
1037
- const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
1038
- const root = options.root ?? (await navigator.storage.getDirectory());
1039
- const namespace = await root.getDirectoryHandle("minnowdb", { create: true });
1040
- return namespace.getDirectoryHandle(encodedName, { create: true });
914
+ const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
915
+ const root = options.root ?? await navigator.storage.getDirectory();
916
+ const namespace = await root.getDirectoryHandle("minnowdb", { create: true });
917
+ return namespace.getDirectoryHandle(encodedName, { create: true });
1041
918
  }
1042
919
  function isLockContention(error) {
1043
- return isDomError(error, "NoModificationAllowedError") || isDomError(error, "InvalidStateError");
920
+ return isDomError(error, "NoModificationAllowedError") || isDomError(error, "InvalidStateError");
1044
921
  }
1045
922
  function sleep(ms) {
1046
- return new Promise((resolve) => setTimeout(resolve, ms));
923
+ return new Promise((resolve) => setTimeout(resolve, ms));
1047
924
  }
925
+ export {
926
+ OpfsBlockStore,
927
+ deleteOpfsDatabase
928
+ };