@minnowdb/core 0.9.1 → 0.10.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 (47) hide show
  1. package/dist/engine/auto-store.d.ts +52 -0
  2. package/dist/engine/auto-store.js +157 -0
  3. package/dist/engine/buffered-writer.d.ts +2 -0
  4. package/dist/engine/buffered-writer.js +15 -2
  5. package/dist/engine/client-audit-harness.js +123 -0
  6. package/dist/engine/client.d.ts +55 -6
  7. package/dist/engine/client.js +176 -46
  8. package/dist/engine/database.d.ts +15 -1
  9. package/dist/engine/database.js +1276 -251
  10. package/dist/engine/errors.d.ts +61 -2
  11. package/dist/engine/errors.js +116 -3
  12. package/dist/engine/index.d.ts +1 -0
  13. package/dist/engine/index.js +2 -0
  14. package/dist/engine/live.d.ts +24 -1
  15. package/dist/engine/live.js +33 -9
  16. package/dist/engine/scope-write-set.js +36 -0
  17. package/dist/engine/worker-auto.d.ts +1 -0
  18. package/dist/engine/worker-auto.js +3 -0
  19. package/dist/engine/worker-host.d.ts +2 -1
  20. package/dist/engine/worker-host.js +19 -1
  21. package/dist/engine/worker-server.d.ts +53 -1
  22. package/dist/engine/worker-server.js +122 -19
  23. package/dist/engine/worker-store-auto.js +36 -0
  24. package/dist/engine/worker-store-opfs.js +3 -2
  25. package/dist/engine/write-coordinator.js +44 -2
  26. package/dist/storage/indexeddb-audit-helpers.js +269 -0
  27. package/dist/storage/indexeddb.js +599 -374
  28. package/dist/storage/opfs/coordination-helpers.js +54 -0
  29. package/dist/storage/opfs/index.d.ts +1 -1
  30. package/dist/storage/opfs/index.js +3 -2
  31. package/dist/storage/opfs/leader.js +243 -17
  32. package/dist/storage/opfs/power-loss-model.js +62 -0
  33. package/dist/storage/opfs/rpc.js +24 -43
  34. package/dist/storage/opfs/store.d.ts +32 -0
  35. package/dist/storage/opfs/store.js +531 -65
  36. package/dist/storage/toolkit/record-core.js +67 -38
  37. package/dist/storage/toolkit/wal.js +16 -0
  38. package/dist/storage/toolkit/wire.d.ts +1 -1
  39. package/dist/storage/toolkit/wire.js +4 -4
  40. package/dist/storage/types.d.ts +31 -10
  41. package/dist/storage/types.js +27 -16
  42. package/dist/testing/opfs-shim.js +14 -6
  43. package/dist/transactions/index.d.ts +19 -0
  44. package/dist/transactions/index.js +99 -25
  45. package/dist/worker-protocol/index.d.ts +50 -2
  46. package/dist/worker-protocol/index.js +106 -4
  47. package/package.json +7 -2
@@ -2,12 +2,16 @@ import { assertStorageBulkReadItems, assertTempRunPageBatchLimits, OpfsCoordinat
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
- import { OpfsLeader } from "./leader.js";
6
- import { rehydrateStoreError, estimateRpcValueBytes, fingerprintStoreRequest, MAX_OPFS_RPC_MESSAGE_BYTES, parseStoreRpcMessage, serializeStoreError } from "./rpc.js";
5
+ import { OpfsLeader, OpfsLeaderClosedError } from "./leader.js";
6
+ import { rehydrateStoreError, estimateRpcValueBytes, fingerprintStoreRequest, MAX_OPFS_RPC_HOLD_MS, MAX_OPFS_RPC_MESSAGE_BYTES, parseStoreRpcMessage, serializeStoreError } from "./rpc.js";
7
7
  const RPC_TIMEOUT_MS = 1e3;
8
8
  const DISCOVERY_WAIT_MS = 150;
9
- const DISPATCH_ATTEMPTS = 10;
9
+ const DISPATCH_BUDGET_MS = 1e4;
10
10
  const YIELD_COOLDOWN_MS = 3e3;
11
+ const HANDOVER_GRACE_MS = 1500;
12
+ const HIDDEN_IDLE_RELEASE_MS = 15e3;
13
+ const MUTATION_PATIENCE_ROUNDS = 3;
14
+ const DECLINE_AFTER_CLOSE_MS = 1e3;
11
15
  const DEDUPE_CACHE_SIZE = 512;
12
16
  const RPC_IN_FLIGHT_LIMIT = 512;
13
17
  const RPC_IN_FLIGHT_MUTATION_BYTES = 128 * 1024 * 1024;
@@ -175,7 +179,14 @@ class OpfsBlockStore {
175
179
  #durability;
176
180
  #checkpointEntries;
177
181
  #cleanupLimitBytes;
182
+ #servedLedgerAgeMs;
183
+ #servedLedgerResultBytes;
178
184
  #rpcTimeoutMs;
185
+ #yieldCooldownMs;
186
+ #hiddenIdleReleaseMs;
187
+ #handoverGraceMs;
188
+ #dispatchBudgetMs;
189
+ #onDiagnostic;
179
190
  #instanceId = crypto.randomUUID();
180
191
  #channelName;
181
192
  #channel;
@@ -187,6 +198,8 @@ class OpfsBlockStore {
187
198
  #closed = false;
188
199
  #lastYieldAt = 0;
189
200
  #electing;
201
+ #recovering = false;
202
+ #waitHeardAt = 0;
190
203
  #pending = /* @__PURE__ */ new Map();
191
204
  #inFlightMutations = /* @__PURE__ */ new Map();
192
205
  #settledMutations = /* @__PURE__ */ new Map();
@@ -201,12 +214,27 @@ class OpfsBlockStore {
201
214
  #servedMutationGateForTests;
202
215
  #dropNextRpcResultForTests = false;
203
216
  #reacquireTimer;
217
+ #served = /* @__PURE__ */ new Map();
218
+ #keepaliveTimer;
219
+ #deferredBid;
220
+ #hiddenIdleTimer;
221
+ #inboxLingerTimer;
222
+ #lastActivityAt = Date.now();
223
+ #othersSeen = false;
224
+ #gracefulLeaders = /* @__PURE__ */ new Set();
204
225
  constructor(tree, options) {
205
226
  this.#tree = tree;
206
227
  this.#durability = options.durability ?? "strict";
207
228
  this.#checkpointEntries = options.checkpointEntries;
208
229
  this.#cleanupLimitBytes = options.cleanupLimitBytes;
230
+ this.#servedLedgerAgeMs = options.servedLedgerAgeMs;
231
+ this.#servedLedgerResultBytes = options.servedLedgerResultBytes;
209
232
  this.#rpcTimeoutMs = options.rpcTimeoutMs ?? RPC_TIMEOUT_MS;
233
+ this.#yieldCooldownMs = options.yieldCooldownMs ?? YIELD_COOLDOWN_MS;
234
+ this.#hiddenIdleReleaseMs = options.hiddenIdleReleaseMs ?? HIDDEN_IDLE_RELEASE_MS;
235
+ this.#handoverGraceMs = options.handoverGraceMs ?? HANDOVER_GRACE_MS;
236
+ this.#dispatchBudgetMs = options.dispatchBudgetMs ?? DISPATCH_BUDGET_MS;
237
+ this.#onDiagnostic = options.onDiagnostic;
210
238
  this.#channelName = `minnowdb-store:${options.name}`;
211
239
  this.liveQueryChannelName = `minnowdb-live:opfs:${options.name}`;
212
240
  }
@@ -246,11 +274,26 @@ class OpfsBlockStore {
246
274
  throw error;
247
275
  }
248
276
  }
249
- async #tryBecomeLeader() {
277
+ #diagnostic(error, context) {
278
+ try {
279
+ this.#onDiagnostic?.(error, context);
280
+ } catch {
281
+ }
282
+ }
283
+ #electInBackground(context, force = false) {
284
+ this.#tryBecomeLeader(force).catch((error) => {
285
+ this.#diagnostic(error, context);
286
+ });
287
+ }
288
+ async #tryBecomeLeader(force = false) {
250
289
  if (this.#closed)
251
290
  return false;
252
291
  if (this.#leader !== void 0)
253
292
  return true;
293
+ if (this.#yielding !== void 0)
294
+ return false;
295
+ if (!force && Date.now() - this.#lastYieldAt < this.#handoverGraceMs)
296
+ return false;
254
297
  if (this.#electing !== void 0)
255
298
  return this.#electing;
256
299
  const election = this.#elect();
@@ -272,10 +315,12 @@ class OpfsBlockStore {
272
315
  }
273
316
  let slotA;
274
317
  let slotB;
318
+ this.#recovering = true;
275
319
  try {
320
+ this.#post({ kind: "wait", leaderId: this.#instanceId });
276
321
  slotA = await this.#openWithRetry(["checkpoint-a"]);
277
322
  slotB = await this.#openWithRetry(["checkpoint-b"]);
278
- this.#leader = await OpfsLeader.recover(this.#tree, this.#durability === "strict", { wal, slotA, slotB }, this.#checkpointEntries, this.#cleanupLimitBytes);
323
+ this.#leader = await OpfsLeader.recover(this.#tree, this.#durability === "strict", { wal, slotA, slotB }, this.#checkpointEntries, this.#cleanupLimitBytes, this.#onDiagnostic, this.#servedLedgerAgeMs, this.#servedLedgerResultBytes);
279
324
  } catch (error) {
280
325
  wal.close();
281
326
  slotA?.close();
@@ -283,19 +328,108 @@ class OpfsBlockStore {
283
328
  if (isLockContention(error))
284
329
  return false;
285
330
  throw error;
331
+ } finally {
332
+ this.#recovering = false;
286
333
  }
287
334
  if (this.#closed) {
288
335
  const leader = this.#leader;
289
336
  this.#leader = void 0;
290
- await leader.shutdown().catch(() => {
337
+ await leader.shutdown().catch((error) => {
338
+ this.#diagnostic(error, "opfs shutdown after close during election");
291
339
  leader.crash();
292
340
  });
293
341
  return false;
294
342
  }
295
343
  this.#knownLeader = this.#instanceId;
344
+ this.#leader.onBeforeCheckpoint = (expectedMs) => {
345
+ this.#holdServed(expectedMs);
346
+ };
296
347
  this.#post({ kind: "leader", leaderId: this.#instanceId });
348
+ this.#post({ kind: "state", leaderId: this.#instanceId, foreground: this.#foreground });
349
+ if (!this.#foreground)
350
+ this.#armHiddenIdleTimer();
297
351
  return true;
298
352
  }
353
+ #holdServed(expectedMs) {
354
+ if (this.#served.size === 0)
355
+ return;
356
+ const ms = Math.min(MAX_OPFS_RPC_HOLD_MS, expectedMs * 2 + this.#rpcTimeoutMs);
357
+ for (const [requestId, from] of this.#served)
358
+ this.#answer(from, { kind: "hold", requestId, ms });
359
+ }
360
+ #keepaliveSuppressedForTests = false;
361
+ _suppressKeepaliveForTests() {
362
+ this.#keepaliveSuppressedForTests = true;
363
+ this.#stopKeepalive();
364
+ }
365
+ #startKeepalive() {
366
+ if (this.#keepaliveTimer !== void 0 || this.#keepaliveSuppressedForTests)
367
+ return;
368
+ const timer = setInterval(() => {
369
+ if (this.#served.size === 0) {
370
+ clearInterval(timer);
371
+ if (this.#keepaliveTimer === timer)
372
+ this.#keepaliveTimer = void 0;
373
+ return;
374
+ }
375
+ for (const [requestId, from] of this.#served) {
376
+ this.#answer(from, { kind: "busy", requestId });
377
+ }
378
+ }, Math.max(1, Math.floor(this.#rpcTimeoutMs / 2)));
379
+ timer.unref?.();
380
+ this.#keepaliveTimer = timer;
381
+ }
382
+ #stopKeepalive() {
383
+ if (this.#keepaliveTimer === void 0)
384
+ return;
385
+ clearInterval(this.#keepaliveTimer);
386
+ this.#keepaliveTimer = void 0;
387
+ }
388
+ #armHiddenIdleTimer() {
389
+ this.#clearHiddenIdleTimer();
390
+ if (this.#foreground || this.#leader === void 0 || this.#closed)
391
+ return;
392
+ const elapsed = Date.now() - this.#lastActivityAt;
393
+ const timer = setTimeout(() => {
394
+ this.#hiddenIdleTimer = void 0;
395
+ if (this.#closed || this.#leader === void 0 || this.#foreground)
396
+ return;
397
+ if (this.#othersSeen && this.#served.size === 0 && this.#yielding === void 0 && Date.now() - this.#lastActivityAt >= this.#hiddenIdleReleaseMs) {
398
+ this.#demote().catch((error) => {
399
+ this.#diagnostic(error, "opfs idle release");
400
+ });
401
+ return;
402
+ }
403
+ this.#armHiddenIdleTimer();
404
+ }, Math.max(0, this.#hiddenIdleReleaseMs - elapsed));
405
+ timer.unref?.();
406
+ this.#hiddenIdleTimer = timer;
407
+ }
408
+ #clearHiddenIdleTimer() {
409
+ if (this.#hiddenIdleTimer === void 0)
410
+ return;
411
+ clearTimeout(this.#hiddenIdleTimer);
412
+ this.#hiddenIdleTimer = void 0;
413
+ }
414
+ async #demote() {
415
+ const leader = this.#leader;
416
+ if (leader === void 0)
417
+ return;
418
+ this.#leader = void 0;
419
+ this.#knownLeader = void 0;
420
+ const shutdown = this.#shutdownAfterMutations(leader).catch((error) => {
421
+ this.#diagnostic(error, "opfs idle release shutdown");
422
+ leader.crash();
423
+ });
424
+ this.#yielding = shutdown;
425
+ await shutdown;
426
+ if (this.#yielding === shutdown)
427
+ this.#yielding = void 0;
428
+ if (this.#closed)
429
+ return;
430
+ this.#closeAnswerChannels();
431
+ this.#post({ kind: "released", leaderId: this.#instanceId });
432
+ }
299
433
  async #openWithRetry(path) {
300
434
  for (let attempt = 0; ; attempt += 1) {
301
435
  try {
@@ -310,19 +444,66 @@ class OpfsBlockStore {
310
444
  }
311
445
  }
312
446
  setForeground(foreground) {
447
+ if (this.#closed)
448
+ return;
449
+ const changed = this.#foreground !== foreground;
313
450
  this.#foreground = foreground;
314
- if (foreground && this.#leader === void 0 && !this.#closed) {
451
+ if (this.#leader !== void 0) {
452
+ if (foreground) {
453
+ this.#clearHiddenIdleTimer();
454
+ this.#clearDeferredBid();
455
+ } else {
456
+ this.#lastActivityAt = Date.now();
457
+ this.#armHiddenIdleTimer();
458
+ }
459
+ if (changed)
460
+ this.#post({ kind: "state", leaderId: this.#instanceId, foreground });
461
+ return;
462
+ }
463
+ if (foreground)
315
464
  this.#post({ kind: "bid", bidderId: this.#instanceId, foreground: true });
465
+ }
466
+ #clearDeferredBid() {
467
+ if (this.#deferredBid === void 0)
468
+ return;
469
+ clearTimeout(this.#deferredBid.timer);
470
+ this.#deferredBid = void 0;
471
+ }
472
+ #considerBid(bidderId) {
473
+ if (this.#leader === void 0 || this.#foreground || this.#yielding !== void 0)
474
+ return;
475
+ const remaining = this.#yieldCooldownMs - (Date.now() - this.#lastYieldAt);
476
+ if (remaining <= 0) {
477
+ this.#clearDeferredBid();
478
+ this.#yieldLeadership(bidderId).catch((error) => {
479
+ this.#diagnostic(error, "opfs yield");
480
+ });
481
+ return;
316
482
  }
483
+ if (this.#deferredBid !== void 0) {
484
+ this.#deferredBid.bidderId = bidderId;
485
+ return;
486
+ }
487
+ const timer = setTimeout(() => {
488
+ const deferred = this.#deferredBid;
489
+ this.#deferredBid = void 0;
490
+ if (deferred !== void 0 && !this.#closed)
491
+ this.#considerBid(deferred.bidderId);
492
+ }, remaining);
493
+ timer.unref?.();
494
+ this.#deferredBid = { bidderId, timer };
317
495
  }
318
496
  async #yieldLeadership(to) {
319
497
  const leader = this.#leader;
320
498
  if (leader === void 0)
321
499
  return;
500
+ this.#clearHiddenIdleTimer();
501
+ this.#clearDeferredBid();
322
502
  this.#leader = void 0;
323
503
  this.#knownLeader = void 0;
324
504
  this.#lastYieldAt = Date.now();
325
- const shutdown = leader.shutdown().catch(() => {
505
+ const shutdown = this.#shutdownAfterMutations(leader).catch((error) => {
506
+ this.#diagnostic(error, "opfs yield shutdown");
326
507
  leader.crash();
327
508
  });
328
509
  this.#yielding = shutdown;
@@ -333,22 +514,40 @@ class OpfsBlockStore {
333
514
  return;
334
515
  this.#closeAnswerChannels();
335
516
  this.#post({ kind: "yield", to });
517
+ this.#post({ kind: "released", leaderId: this.#instanceId });
336
518
  if (this.#reacquireTimer !== void 0)
337
519
  clearTimeout(this.#reacquireTimer);
338
520
  this.#reacquireTimer = setTimeout(() => {
339
- if (this.#knownLeader === void 0 && !this.#closed)
340
- void this.#tryBecomeLeader();
341
- }, 1500);
521
+ if (this.#knownLeader === void 0 && !this.#closed) {
522
+ this.#electInBackground("opfs reacquire after yield", true);
523
+ }
524
+ }, this.#handoverGraceMs);
342
525
  }
343
526
  #yielding;
527
+ #shutdownAfterMutations(leader) {
528
+ return this.#withMutationTurn(() => leader.shutdown());
529
+ }
530
+ async #servedDrained() {
531
+ const deadline = Date.now() + DECLINE_AFTER_CLOSE_MS;
532
+ while (this.#served.size > 0 && Date.now() < deadline)
533
+ await sleep(5);
534
+ }
344
535
  #onMessage(message) {
345
- if (this.#closed)
536
+ if (this.#closed) {
537
+ if (message.kind === "op")
538
+ this.#decline(message);
346
539
  return;
540
+ }
347
541
  if (this.#coordinationPausedForTests)
348
542
  return;
543
+ this.#othersSeen = true;
349
544
  switch (message.kind) {
350
545
  case "op": {
351
- if (this.#leader !== void 0) {
546
+ if (this.#leader === void 0) {
547
+ this.#decline(message);
548
+ return;
549
+ }
550
+ {
352
551
  const requestBytes = estimateRpcValueBytes(message.args);
353
552
  if (this.#servedRequestCount >= RPC_SERVER_ADMISSION_LIMIT || this.#servedRequestBytes + requestBytes > RPC_SERVER_ADMISSION_BYTES) {
354
553
  this.#answer(message.from, {
@@ -361,13 +560,51 @@ class OpfsBlockStore {
361
560
  }
362
561
  this.#servedRequestCount += 1;
363
562
  this.#servedRequestBytes += requestBytes;
364
- void this.#serveOp(message).finally(() => {
563
+ this.#lastActivityAt = Date.now();
564
+ const alreadyServing = this.#served.has(message.requestId);
565
+ this.#served.set(message.requestId, message.from);
566
+ this.#startKeepalive();
567
+ void this.#serveOp(message).catch((error) => {
568
+ this.#diagnostic(error, `opfs served ${message.method}`);
569
+ this.#answer(message.from, {
570
+ kind: "result",
571
+ requestId: message.requestId,
572
+ ok: false,
573
+ error: serializeStoreError(error)
574
+ });
575
+ }).finally(() => {
365
576
  this.#servedRequestCount = Math.max(0, this.#servedRequestCount - 1);
366
577
  this.#servedRequestBytes = Math.max(0, this.#servedRequestBytes - requestBytes);
578
+ if (!alreadyServing)
579
+ this.#served.delete(message.requestId);
580
+ this.#lastActivityAt = Date.now();
367
581
  });
368
582
  }
369
583
  return;
370
584
  }
585
+ case "declined": {
586
+ const pending = this.#takePending(message.requestId);
587
+ if (pending === void 0)
588
+ return;
589
+ clearTimeout(pending.timer);
590
+ pending.reject(RPC_DECLINED);
591
+ return;
592
+ }
593
+ case "uncertain": {
594
+ const pending = this.#takePending(message.requestId);
595
+ if (pending === void 0)
596
+ return;
597
+ clearTimeout(pending.timer);
598
+ pending.reject(new OpfsUncertainOutcomeError(pending.message.method));
599
+ return;
600
+ }
601
+ case "hold": {
602
+ const pending = this.#pending.get(message.requestId);
603
+ if (pending === void 0)
604
+ return;
605
+ this.#armPendingTimer(pending, Math.max(message.ms, this.#rpcTimeoutMs));
606
+ return;
607
+ }
371
608
  case "result": {
372
609
  const pending = this.#takePending(message.requestId);
373
610
  if (pending === void 0)
@@ -383,26 +620,28 @@ class OpfsBlockStore {
383
620
  const pending = this.#pending.get(message.requestId);
384
621
  if (pending === void 0)
385
622
  return;
386
- clearTimeout(pending.timer);
387
- pending.timer = setTimeout(() => {
388
- const expired = this.#takePending(message.requestId);
389
- expired?.reject(RPC_TIMED_OUT);
390
- }, this.#rpcTimeoutMs);
623
+ this.#armPendingTimer(pending, this.#rpcTimeoutMs);
391
624
  return;
392
625
  }
393
626
  case "leader": {
394
627
  this.#knownLeader = message.leaderId;
628
+ this.#gracefulLeaders.delete(message.leaderId);
395
629
  if (this.#reacquireTimer !== void 0) {
396
630
  clearTimeout(this.#reacquireTimer);
397
631
  this.#reacquireTimer = void 0;
398
632
  }
399
- for (const [requestId, pending] of this.#pending) {
400
- if (pending.sentTo === message.leaderId)
633
+ for (const pending of this.#pending.values()) {
634
+ if (pending.sentTo === message.leaderId) {
635
+ if (pending.awaitingPing)
636
+ this.#armPendingTimer(pending, this.#rpcTimeoutMs);
401
637
  continue;
402
- if (!READ_METHODS.has(pending.message.method)) {
403
- this.#takePending(requestId);
404
- clearTimeout(pending.timer);
405
- pending.reject(new OpfsUncertainOutcomeError(pending.message.method));
638
+ }
639
+ if (pending.sentTo === void 0) {
640
+ pending.sentTo = message.leaderId;
641
+ this.#send(message.leaderId, pending.message);
642
+ continue;
643
+ }
644
+ if (!READ_METHODS.has(pending.message.method) && this.#gracefulLeaders.has(pending.sentTo)) {
406
645
  continue;
407
646
  }
408
647
  pending.sentTo = message.leaderId;
@@ -413,30 +652,81 @@ class OpfsBlockStore {
413
652
  }
414
653
  return;
415
654
  }
655
+ case "state": {
656
+ if (!message.foreground && this.#foreground && this.#leader === void 0 && message.leaderId !== this.#instanceId) {
657
+ this.#post({ kind: "bid", bidderId: this.#instanceId, foreground: true });
658
+ }
659
+ return;
660
+ }
416
661
  case "ping": {
417
662
  if (this.#leader !== void 0) {
418
663
  this.#post({ kind: "leader", leaderId: this.#instanceId });
664
+ } else if (this.#recovering || this.#yielding !== void 0) {
665
+ this.#post({ kind: "wait", leaderId: this.#instanceId });
419
666
  }
420
667
  return;
421
668
  }
669
+ case "wait": {
670
+ if (message.leaderId !== this.#instanceId)
671
+ this.#waitHeardAt = Date.now();
672
+ return;
673
+ }
422
674
  case "bid": {
423
- if (this.#leader !== void 0 && !this.#foreground && message.foreground && message.bidderId !== this.#instanceId && Date.now() - this.#lastYieldAt > YIELD_COOLDOWN_MS) {
424
- void this.#yieldLeadership(message.bidderId);
675
+ if (message.foreground && message.bidderId !== this.#instanceId) {
676
+ this.#considerBid(message.bidderId);
425
677
  }
426
678
  return;
427
679
  }
428
680
  case "yield": {
429
- if (message.to === this.#instanceId)
430
- void this.#tryBecomeLeader();
681
+ if (message.to === this.#instanceId) {
682
+ this.#knownLeader = void 0;
683
+ this.#electInBackground("opfs election after yield", true);
684
+ }
431
685
  return;
432
686
  }
433
687
  case "released": {
434
688
  if (this.#knownLeader === message.leaderId)
435
689
  this.#knownLeader = void 0;
690
+ this.#gracefulLeaders.add(message.leaderId);
691
+ if (this.#gracefulLeaders.size > 16) {
692
+ const [oldest] = this.#gracefulLeaders;
693
+ if (oldest !== void 0)
694
+ this.#gracefulLeaders.delete(oldest);
695
+ }
436
696
  return;
437
697
  }
438
698
  }
439
699
  }
700
+ #decline(message) {
701
+ this.#postOnce(message.from, {
702
+ kind: "declined",
703
+ requestId: message.requestId,
704
+ reason: "not-leader"
705
+ });
706
+ }
707
+ #armPendingTimer(pending, ms) {
708
+ clearTimeout(pending.timer);
709
+ pending.awaitingPing = false;
710
+ pending.timer = setTimeout(() => {
711
+ this.#onPendingTimeout(pending.message.requestId);
712
+ }, ms);
713
+ }
714
+ #onPendingTimeout(requestId) {
715
+ const pending = this.#pending.get(requestId);
716
+ if (pending === void 0)
717
+ return;
718
+ if (READ_METHODS.has(pending.message.method) || pending.awaitingPing || pending.patienceRounds >= MUTATION_PATIENCE_ROUNDS || this.#channel === void 0) {
719
+ this.#takePending(requestId);
720
+ pending.reject(RPC_TIMED_OUT);
721
+ return;
722
+ }
723
+ pending.patienceRounds += 1;
724
+ pending.awaitingPing = true;
725
+ this.#post({ kind: "ping" });
726
+ pending.timer = setTimeout(() => {
727
+ this.#onPendingTimeout(requestId);
728
+ }, DISCOVERY_WAIT_MS);
729
+ }
440
730
  #takePending(requestId) {
441
731
  const pending = this.#pending.get(requestId);
442
732
  if (pending === void 0)
@@ -446,7 +736,7 @@ class OpfsBlockStore {
446
736
  return pending;
447
737
  }
448
738
  async #serveOp(message) {
449
- const requestKey = `${String(message.from.length)}:${message.from}${message.requestId}`;
739
+ const requestKey = servedRequestKey(message.from, message.requestId);
450
740
  if (this.#inFlightMutations.has(requestKey) || this.#settledMutations.has(requestKey)) {
451
741
  await this.#serveOpLocked(message, requestKey);
452
742
  return;
@@ -476,10 +766,38 @@ class OpfsBlockStore {
476
766
  let fingerprint;
477
767
  try {
478
768
  fingerprint = await fingerprintStoreRequest(message.method, message.args);
479
- } catch {
769
+ } catch (error) {
770
+ this.#answer(message.from, {
771
+ kind: "result",
772
+ requestId: message.requestId,
773
+ ok: false,
774
+ error: serializeStoreError(error)
775
+ });
480
776
  return;
481
777
  }
482
- const remembered = isRead ? void 0 : this.#settledMutations.get(requestKey);
778
+ let remembered = isRead ? void 0 : this.#settledMutations.get(requestKey);
779
+ const inFlight = isRead || remembered !== void 0 ? void 0 : this.#inFlightMutations.get(requestKey);
780
+ if (remembered === void 0 && inFlight === void 0 && !isRead) {
781
+ const logged = leader.servedOutcome(requestKey);
782
+ if (logged !== void 0) {
783
+ if (logged.method !== message.method || logged.signature !== fingerprint.signature) {
784
+ this.#rejectReusedRequestIdentity(message);
785
+ return;
786
+ }
787
+ if (!logged.settled || logged.withheld === true) {
788
+ this.#answer(message.from, { kind: "uncertain", requestId: message.requestId });
789
+ return;
790
+ }
791
+ const outcome = { ok: true, value: logged.result };
792
+ this.#rememberSettledMutation(requestKey, logged.method, { signature: logged.signature, retainedBytes: logged.requestBytes }, outcome);
793
+ remembered = this.#settledMutations.get(requestKey) ?? {
794
+ method: logged.method,
795
+ signature: logged.signature,
796
+ requestBytes: logged.requestBytes,
797
+ outcome
798
+ };
799
+ }
800
+ }
483
801
  let settled;
484
802
  if (remembered !== void 0) {
485
803
  if (!sameServedRequest(remembered, message, fingerprint.signature)) {
@@ -489,10 +807,7 @@ class OpfsBlockStore {
489
807
  this.#settledMutations.delete(requestKey);
490
808
  this.#settledMutations.set(requestKey, remembered);
491
809
  settled = remembered.outcome;
492
- } else if (!isRead && this.#inFlightMutations.has(requestKey)) {
493
- const inFlight = this.#inFlightMutations.get(requestKey);
494
- if (inFlight === void 0)
495
- throw new Error("In-flight RPC identity disappeared");
810
+ } else if (inFlight !== void 0) {
496
811
  if (!sameServedRequest(inFlight, message, fingerprint.signature)) {
497
812
  this.#rejectReusedRequestIdentity(message);
498
813
  return;
@@ -500,6 +815,10 @@ class OpfsBlockStore {
500
815
  this.#answer(message.from, { kind: "busy", requestId: message.requestId });
501
816
  settled = await inFlight.outcome;
502
817
  } else {
818
+ if (!isRead && message.sentAt < leader.servedCoverageSince) {
819
+ this.#answer(message.from, { kind: "uncertain", requestId: message.requestId });
820
+ return;
821
+ }
503
822
  if (!isRead && (this.#inFlightMutations.size >= RPC_IN_FLIGHT_LIMIT || this.#inFlightMutationBytes + fingerprint.retainedBytes > RPC_IN_FLIGHT_MUTATION_BYTES)) {
504
823
  this.#answer(message.from, {
505
824
  kind: "result",
@@ -521,11 +840,15 @@ class OpfsBlockStore {
521
840
  this.#readCapacityChanged = { promise, resolve };
522
841
  }
523
842
  await this.#readCapacityChanged.promise;
524
- if (this.#closed || this.#leader !== leader)
843
+ if (this.#closed || this.#leader !== leader) {
844
+ this.#decline(message);
525
845
  return;
846
+ }
526
847
  }
527
- if (this.#closed || this.#leader !== leader)
848
+ if (this.#closed || this.#leader !== leader) {
849
+ this.#decline(message);
528
850
  return;
851
+ }
529
852
  this.#inFlightReadBytes += reservation;
530
853
  try {
531
854
  settled = await this.#executeServedOpAfterGate(leader, message, true);
@@ -535,11 +858,17 @@ class OpfsBlockStore {
535
858
  }
536
859
  } else {
537
860
  this.#inFlightMutationBytes += fingerprint.retainedBytes;
538
- const execution = this.#executeServedOpAfterGate(leader, message, false);
861
+ const execution = this.#executeServedOpAfterGate(leader, message, false, {
862
+ key: requestKey,
863
+ method: message.method,
864
+ signature: fingerprint.signature,
865
+ requestBytes: fingerprint.retainedBytes,
866
+ sentAt: Math.min(message.sentAt, Date.now())
867
+ });
539
868
  const outcome = execution.then((result) => {
540
869
  this.#inFlightMutations.delete(requestKey);
541
870
  this.#inFlightMutationBytes = Math.max(0, this.#inFlightMutationBytes - fingerprint.retainedBytes);
542
- if (!this.#closed) {
871
+ if (!this.#closed && result !== DECLINED_OUTCOME) {
543
872
  this.#rememberSettledMutation(requestKey, message.method, fingerprint, result);
544
873
  }
545
874
  return result;
@@ -553,6 +882,8 @@ class OpfsBlockStore {
553
882
  settled = await outcome;
554
883
  }
555
884
  }
885
+ if (settled === DECLINED_OUTCOME)
886
+ return;
556
887
  if (this.#dropNextRpcResultForTests) {
557
888
  this.#dropNextRpcResultForTests = false;
558
889
  return;
@@ -601,20 +932,58 @@ class OpfsBlockStore {
601
932
  this.#readCapacityChanged = void 0;
602
933
  waiting?.resolve();
603
934
  }
604
- async #executeServedOpAfterGate(leader, message, isRead) {
935
+ async #executeServedOpAfterGate(leader, message, isRead, request) {
605
936
  const gate = this.#servedMutationGateForTests;
606
937
  if (!isRead && gate !== void 0)
607
938
  await gate;
608
- return this.#executeServedOp(leader, message);
939
+ return this.#executeServedOp(leader, message, request);
609
940
  }
610
- async #executeServedOp(leader, message) {
941
+ #leads(leader) {
942
+ return this.#leader === leader;
943
+ }
944
+ #mutationTail = Promise.resolve();
945
+ #withMutationTurn(run) {
946
+ const turn = this.#mutationTail.then(run);
947
+ this.#mutationTail = turn.then(() => void 0, () => void 0);
948
+ return turn;
949
+ }
950
+ async #executeServedOp(leader, message, request) {
951
+ if (leader.isClosed() || !this.#leads(leader)) {
952
+ this.#decline(message);
953
+ return DECLINED_OUTCOME;
954
+ }
611
955
  try {
612
956
  const method = leader[message.method];
613
957
  if (method === void 0) {
614
958
  return { ok: false, error: { name: "Error", message: "Unknown store operation" } };
615
959
  }
616
- return { ok: true, value: await method.apply(leader, message.args) };
960
+ if (request === void 0) {
961
+ return { ok: true, value: await method.apply(leader, message.args) };
962
+ }
963
+ return await this.#withMutationTurn(async () => {
964
+ if (leader.isClosed() || !this.#leads(leader)) {
965
+ this.#decline(message);
966
+ return DECLINED_OUTCOME;
967
+ }
968
+ leader.servingRequest = request;
969
+ try {
970
+ const value = await method.apply(leader, message.args);
971
+ if (leader.servingRequest !== request) {
972
+ await leader.completeServed(request.key, value).catch((error) => {
973
+ this.#diagnostic(error, `opfs served result for ${message.method}`);
974
+ });
975
+ }
976
+ return { ok: true, value };
977
+ } finally {
978
+ if (leader.servingRequest === request)
979
+ leader.servingRequest = void 0;
980
+ }
981
+ });
617
982
  } catch (error) {
983
+ if (leader.isClosed() && !this.#leads(leader)) {
984
+ this.#decline(message);
985
+ return DECLINED_OUTCOME;
986
+ }
618
987
  return { ok: false, error: serializeStoreError(error) };
619
988
  }
620
989
  }
@@ -663,9 +1032,20 @@ class OpfsBlockStore {
663
1032
  #closeChannels() {
664
1033
  this.#channel?.close();
665
1034
  this.#channel = void 0;
666
- this.#inbox?.close();
667
- this.#inbox = void 0;
668
1035
  this.#closeAnswerChannels();
1036
+ const inbox = this.#inbox;
1037
+ this.#inbox = void 0;
1038
+ if (inbox === void 0)
1039
+ return;
1040
+ if (this.#inboxLingerTimer !== void 0)
1041
+ clearTimeout(this.#inboxLingerTimer);
1042
+ const timer = setTimeout(() => {
1043
+ this.#inboxLingerTimer = void 0;
1044
+ inbox.close();
1045
+ }, DECLINE_AFTER_CLOSE_MS);
1046
+ timer.unref?.();
1047
+ inbox.unref?.();
1048
+ this.#inboxLingerTimer = timer;
669
1049
  }
670
1050
  #assertOpen() {
671
1051
  if (this.#closed)
@@ -674,14 +1054,46 @@ class OpfsBlockStore {
674
1054
  async #dispatch(method, args) {
675
1055
  this.#assertOpen();
676
1056
  const requestId = crypto.randomUUID();
677
- for (let attempt = 0; attempt < DISPATCH_ATTEMPTS; attempt += 1) {
1057
+ const sentAt = Date.now();
1058
+ const isRead = READ_METHODS.has(method);
1059
+ let sentRemotely = false;
1060
+ let mayHaveRun = false;
1061
+ this.#lastActivityAt = sentAt;
1062
+ while (Date.now() - Math.max(sentAt, this.#waitHeardAt) < this.#dispatchBudgetMs) {
678
1063
  this.#assertOpen();
679
1064
  const leader = this.#leader;
680
1065
  if (leader !== void 0) {
681
1066
  const bound = leader[method];
682
1067
  if (bound === void 0)
683
1068
  throw new Error(`Unknown store operation: ${method}`);
684
- return bound.apply(leader, args);
1069
+ try {
1070
+ if (isRead)
1071
+ return await bound.apply(leader, args);
1072
+ if (sentRemotely) {
1073
+ const key = servedRequestKey(this.#instanceId, requestId);
1074
+ const logged = leader.servedOutcome(key);
1075
+ if (logged !== void 0) {
1076
+ if (!logged.settled || logged.withheld === true) {
1077
+ throw new OpfsUncertainOutcomeError(method);
1078
+ }
1079
+ return logged.result;
1080
+ }
1081
+ if (sentAt < leader.servedCoverageSince)
1082
+ throw new OpfsUncertainOutcomeError(method);
1083
+ }
1084
+ return await this.#withMutationTurn(() => {
1085
+ if (leader.isClosed() || !this.#leads(leader))
1086
+ throw new OpfsLeaderClosedError();
1087
+ return bound.apply(leader, args);
1088
+ });
1089
+ } catch (error) {
1090
+ if (!this.#leads(leader) && (isRead || error instanceof OpfsLeaderClosedError)) {
1091
+ continue;
1092
+ }
1093
+ throw error;
1094
+ } finally {
1095
+ this.#lastActivityAt = Date.now();
1096
+ }
685
1097
  }
686
1098
  if (this.#channel === void 0) {
687
1099
  if (await this.#tryBecomeLeader())
@@ -699,23 +1111,26 @@ class OpfsBlockStore {
699
1111
  }
700
1112
  }
701
1113
  try {
702
- return await this.#rpc(requestId, method, args);
1114
+ sentRemotely = true;
1115
+ return await this.#rpc(requestId, method, args, sentAt);
703
1116
  } catch (error) {
704
- if (error === RPC_TIMED_OUT) {
1117
+ if (error === RPC_DECLINED || error === RPC_TIMED_OUT) {
1118
+ if (error === RPC_TIMED_OUT)
1119
+ mayHaveRun = true;
705
1120
  this.#knownLeader = void 0;
706
- if (!READ_METHODS.has(method))
707
- throw new OpfsUncertainOutcomeError(method);
708
1121
  continue;
709
1122
  }
710
1123
  throw error;
711
1124
  }
712
1125
  }
1126
+ if (!isRead && mayHaveRun)
1127
+ throw new OpfsUncertainOutcomeError(method);
713
1128
  throw new OpfsCoordinationError("leader-unavailable", method);
714
1129
  }
715
1130
  #leaderKnown() {
716
1131
  return this.#knownLeader !== void 0;
717
1132
  }
718
- #rpc(requestId, method, args) {
1133
+ #rpc(requestId, method, args, sentAt) {
719
1134
  let retainedBytes;
720
1135
  try {
721
1136
  retainedBytes = estimateRpcValueBytes(args);
@@ -726,10 +1141,16 @@ class OpfsBlockStore {
726
1141
  return Promise.reject(new OpfsCoordinationError("follower-queue-full", method));
727
1142
  }
728
1143
  return new Promise((resolve, reject) => {
729
- const message = { kind: "op", requestId, from: this.#instanceId, method, args };
1144
+ const message = {
1145
+ kind: "op",
1146
+ requestId,
1147
+ from: this.#instanceId,
1148
+ method,
1149
+ args,
1150
+ sentAt
1151
+ };
730
1152
  const timer = setTimeout(() => {
731
- const expired = this.#takePending(requestId);
732
- expired?.reject(RPC_TIMED_OUT);
1153
+ this.#onPendingTimeout(requestId);
733
1154
  }, this.#rpcTimeoutMs);
734
1155
  const leaderId = this.#knownLeader;
735
1156
  this.#pending.set(requestId, {
@@ -738,7 +1159,9 @@ class OpfsBlockStore {
738
1159
  sentTo: leaderId,
739
1160
  resolve,
740
1161
  reject,
741
- timer
1162
+ timer,
1163
+ patienceRounds: 0,
1164
+ awaitingPing: false
742
1165
  });
743
1166
  this.#pendingRpcBytes += retainedBytes;
744
1167
  if (leaderId !== void 0)
@@ -777,8 +1200,7 @@ class OpfsBlockStore {
777
1200
  if (this.#closed)
778
1201
  return;
779
1202
  this.#closed = true;
780
- if (this.#reacquireTimer !== void 0)
781
- clearTimeout(this.#reacquireTimer);
1203
+ this.#clearCoordinationTimers();
782
1204
  for (const pending of this.#pending.values()) {
783
1205
  clearTimeout(pending.timer);
784
1206
  pending.reject(new Error("This OPFS store connection is closed"));
@@ -797,18 +1219,37 @@ class OpfsBlockStore {
797
1219
  const leader = this.#leader;
798
1220
  this.#leader = void 0;
799
1221
  if (leader !== void 0) {
800
- void leader.shutdown().catch(() => {
1222
+ void this.#shutdownAfterMutations(leader).catch((error) => {
1223
+ this.#diagnostic(error, "opfs close shutdown");
801
1224
  leader.crash();
802
- }).then(() => {
1225
+ }).then(() => this.#servedDrained()).then(() => {
1226
+ this.#stopKeepalive();
803
1227
  this.#post({ kind: "released", leaderId: this.#instanceId });
804
1228
  this.#closeChannels();
805
1229
  this.#releaseWhenHandlesClose();
806
1230
  });
807
1231
  return;
808
1232
  }
1233
+ if (this.#yielding !== void 0) {
1234
+ void this.#yielding.then(() => this.#servedDrained()).then(() => {
1235
+ this.#stopKeepalive();
1236
+ this.#post({ kind: "released", leaderId: this.#instanceId });
1237
+ this.#closeChannels();
1238
+ this.#releaseWhenHandlesClose();
1239
+ });
1240
+ return;
1241
+ }
1242
+ this.#stopKeepalive();
809
1243
  this.#closeChannels();
810
1244
  this.#releaseWhenHandlesClose();
811
1245
  }
1246
+ #clearCoordinationTimers() {
1247
+ if (this.#reacquireTimer !== void 0)
1248
+ clearTimeout(this.#reacquireTimer);
1249
+ this.#reacquireTimer = void 0;
1250
+ this.#clearDeferredBid();
1251
+ this.#clearHiddenIdleTimer();
1252
+ }
812
1253
  #releaseWhenHandlesClose() {
813
1254
  const release = this.#releaseConnectionLock;
814
1255
  this.#releaseConnectionLock = void 0;
@@ -846,6 +1287,9 @@ class OpfsBlockStore {
846
1287
  release();
847
1288
  };
848
1289
  }
1290
+ _oldestPendingRequestIdForTests() {
1291
+ return this.#pending.keys().next().value;
1292
+ }
849
1293
  _resendOldestPendingForTests() {
850
1294
  const pending = this.#pending.values().next().value;
851
1295
  if (pending?.sentTo !== void 0)
@@ -863,8 +1307,9 @@ class OpfsBlockStore {
863
1307
  }
864
1308
  _crashForTests() {
865
1309
  this.#closed = true;
866
- if (this.#reacquireTimer !== void 0)
867
- clearTimeout(this.#reacquireTimer);
1310
+ this.#clearCoordinationTimers();
1311
+ this.#stopKeepalive();
1312
+ this.#served.clear();
868
1313
  for (const pending of this.#pending.values())
869
1314
  clearTimeout(pending.timer);
870
1315
  this.#pending.clear();
@@ -880,7 +1325,11 @@ class OpfsBlockStore {
880
1325
  this.#servedRequestBytes = 0;
881
1326
  this.#leader?.crash();
882
1327
  this.#leader = void 0;
883
- this.#closeChannels();
1328
+ this.#channel?.close();
1329
+ this.#channel = void 0;
1330
+ this.#inbox?.close();
1331
+ this.#inbox = void 0;
1332
+ this.#closeAnswerChannels();
884
1333
  this.#releaseWhenHandlesClose();
885
1334
  }
886
1335
  async #ensureFormatMarker() {
@@ -997,13 +1446,29 @@ async function holdConnectionLock(name) {
997
1446
  })).catch(reject);
998
1447
  });
999
1448
  }
1449
+ function servedRequestKey(from, requestId) {
1450
+ return `${String(from.length)}:${from}${requestId}`;
1451
+ }
1000
1452
  const RPC_TIMED_OUT = new Error("The leader did not answer in time");
1453
+ const RPC_DECLINED = new Error("The connection asked is not the leader");
1454
+ const DECLINED_OUTCOME = { ok: false };
1001
1455
  async function resolveDatabaseRoot(options) {
1002
1456
  const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
1003
1457
  const root = options.root ?? await navigator.storage.getDirectory();
1004
1458
  const namespace = await root.getDirectoryHandle("minnowdb", { create: true });
1005
1459
  return namespace.getDirectoryHandle(encodedName, { create: true });
1006
1460
  }
1461
+ async function opfsDatabaseExists(options) {
1462
+ const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
1463
+ try {
1464
+ const root = options.root ?? await navigator.storage.getDirectory();
1465
+ const namespace = await root.getDirectoryHandle("minnowdb");
1466
+ await namespace.getDirectoryHandle(encodedName);
1467
+ return true;
1468
+ } catch {
1469
+ return false;
1470
+ }
1471
+ }
1007
1472
  function isLockContention(error) {
1008
1473
  return isDomError(error, "NoModificationAllowedError") || isDomError(error, "InvalidStateError");
1009
1474
  }
@@ -1012,5 +1477,6 @@ function sleep(ms) {
1012
1477
  }
1013
1478
  export {
1014
1479
  OpfsBlockStore,
1015
- deleteOpfsDatabase
1480
+ deleteOpfsDatabase,
1481
+ opfsDatabaseExists
1016
1482
  };