@minnowdb/core 0.9.0 → 0.10.0

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 (40) hide show
  1. package/dist/engine/auto-store.d.ts +40 -0
  2. package/dist/engine/auto-store.js +115 -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.d.ts +55 -6
  6. package/dist/engine/client.js +155 -40
  7. package/dist/engine/database.d.ts +15 -1
  8. package/dist/engine/database.js +1085 -227
  9. package/dist/engine/errors.d.ts +61 -2
  10. package/dist/engine/errors.js +116 -3
  11. package/dist/engine/index.d.ts +1 -0
  12. package/dist/engine/index.js +2 -0
  13. package/dist/engine/live.d.ts +24 -1
  14. package/dist/engine/live.js +33 -9
  15. package/dist/engine/scope-write-set.js +36 -0
  16. package/dist/engine/worker-auto.d.ts +1 -0
  17. package/dist/engine/worker-auto.js +3 -0
  18. package/dist/engine/worker-host.d.ts +2 -1
  19. package/dist/engine/worker-host.js +17 -1
  20. package/dist/engine/worker-server.d.ts +53 -1
  21. package/dist/engine/worker-server.js +118 -14
  22. package/dist/engine/worker-store-auto.js +36 -0
  23. package/dist/engine/worker-store-opfs.js +3 -2
  24. package/dist/engine/write-coordinator.js +24 -2
  25. package/dist/storage/indexeddb.js +494 -207
  26. package/dist/storage/opfs/leader.js +201 -14
  27. package/dist/storage/opfs/rpc.js +23 -43
  28. package/dist/storage/opfs/store.d.ts +20 -0
  29. package/dist/storage/opfs/store.js +501 -70
  30. package/dist/storage/toolkit/record-core.js +67 -38
  31. package/dist/storage/toolkit/wire.d.ts +1 -1
  32. package/dist/storage/toolkit/wire.js +1 -1
  33. package/dist/storage/types.d.ts +29 -8
  34. package/dist/storage/types.js +27 -16
  35. package/dist/testing/opfs-shim.js +14 -6
  36. package/dist/transactions/index.d.ts +12 -0
  37. package/dist/transactions/index.js +83 -23
  38. package/dist/worker-protocol/index.d.ts +50 -2
  39. package/dist/worker-protocol/index.js +106 -4
  40. package/package.json +7 -2
@@ -3,11 +3,15 @@ import { validateTempRunPage, validateTempRunPageIdentity } from "../toolkit/rec
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";
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
9
  const DISPATCH_ATTEMPTS = 10;
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,13 @@ 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
+ #onDiagnostic;
179
189
  #instanceId = crypto.randomUUID();
180
190
  #channelName;
181
191
  #channel;
@@ -201,12 +211,26 @@ class OpfsBlockStore {
201
211
  #servedMutationGateForTests;
202
212
  #dropNextRpcResultForTests = false;
203
213
  #reacquireTimer;
214
+ #served = /* @__PURE__ */ new Map();
215
+ #keepaliveTimer;
216
+ #deferredBid;
217
+ #hiddenIdleTimer;
218
+ #inboxLingerTimer;
219
+ #lastActivityAt = Date.now();
220
+ #othersSeen = false;
221
+ #gracefulLeaders = /* @__PURE__ */ new Set();
204
222
  constructor(tree, options) {
205
223
  this.#tree = tree;
206
224
  this.#durability = options.durability ?? "strict";
207
225
  this.#checkpointEntries = options.checkpointEntries;
208
226
  this.#cleanupLimitBytes = options.cleanupLimitBytes;
227
+ this.#servedLedgerAgeMs = options.servedLedgerAgeMs;
228
+ this.#servedLedgerResultBytes = options.servedLedgerResultBytes;
209
229
  this.#rpcTimeoutMs = options.rpcTimeoutMs ?? RPC_TIMEOUT_MS;
230
+ this.#yieldCooldownMs = options.yieldCooldownMs ?? YIELD_COOLDOWN_MS;
231
+ this.#hiddenIdleReleaseMs = options.hiddenIdleReleaseMs ?? HIDDEN_IDLE_RELEASE_MS;
232
+ this.#handoverGraceMs = options.handoverGraceMs ?? HANDOVER_GRACE_MS;
233
+ this.#onDiagnostic = options.onDiagnostic;
210
234
  this.#channelName = `minnowdb-store:${options.name}`;
211
235
  this.liveQueryChannelName = `minnowdb-live:opfs:${options.name}`;
212
236
  }
@@ -246,11 +270,26 @@ class OpfsBlockStore {
246
270
  throw error;
247
271
  }
248
272
  }
249
- async #tryBecomeLeader() {
273
+ #diagnostic(error, context) {
274
+ try {
275
+ this.#onDiagnostic?.(error, context);
276
+ } catch {
277
+ }
278
+ }
279
+ #electInBackground(context, force = false) {
280
+ this.#tryBecomeLeader(force).catch((error) => {
281
+ this.#diagnostic(error, context);
282
+ });
283
+ }
284
+ async #tryBecomeLeader(force = false) {
250
285
  if (this.#closed)
251
286
  return false;
252
287
  if (this.#leader !== void 0)
253
288
  return true;
289
+ if (this.#yielding !== void 0)
290
+ return false;
291
+ if (!force && Date.now() - this.#lastYieldAt < this.#handoverGraceMs)
292
+ return false;
254
293
  if (this.#electing !== void 0)
255
294
  return this.#electing;
256
295
  const election = this.#elect();
@@ -275,7 +314,7 @@ class OpfsBlockStore {
275
314
  try {
276
315
  slotA = await this.#openWithRetry(["checkpoint-a"]);
277
316
  slotB = await this.#openWithRetry(["checkpoint-b"]);
278
- this.#leader = await OpfsLeader.recover(this.#tree, this.#durability === "strict", { wal, slotA, slotB }, this.#checkpointEntries, this.#cleanupLimitBytes);
317
+ this.#leader = await OpfsLeader.recover(this.#tree, this.#durability === "strict", { wal, slotA, slotB }, this.#checkpointEntries, this.#cleanupLimitBytes, this.#onDiagnostic, this.#servedLedgerAgeMs, this.#servedLedgerResultBytes);
279
318
  } catch (error) {
280
319
  wal.close();
281
320
  slotA?.close();
@@ -287,15 +326,102 @@ class OpfsBlockStore {
287
326
  if (this.#closed) {
288
327
  const leader = this.#leader;
289
328
  this.#leader = void 0;
290
- await leader.shutdown().catch(() => {
329
+ await leader.shutdown().catch((error) => {
330
+ this.#diagnostic(error, "opfs shutdown after close during election");
291
331
  leader.crash();
292
332
  });
293
333
  return false;
294
334
  }
295
335
  this.#knownLeader = this.#instanceId;
336
+ this.#leader.onBeforeCheckpoint = (expectedMs) => {
337
+ this.#holdServed(expectedMs);
338
+ };
296
339
  this.#post({ kind: "leader", leaderId: this.#instanceId });
340
+ this.#post({ kind: "state", leaderId: this.#instanceId, foreground: this.#foreground });
341
+ if (!this.#foreground)
342
+ this.#armHiddenIdleTimer();
297
343
  return true;
298
344
  }
345
+ #holdServed(expectedMs) {
346
+ if (this.#served.size === 0)
347
+ return;
348
+ const ms = Math.min(MAX_OPFS_RPC_HOLD_MS, expectedMs * 2 + this.#rpcTimeoutMs);
349
+ for (const [requestId, from] of this.#served)
350
+ this.#answer(from, { kind: "hold", requestId, ms });
351
+ }
352
+ #keepaliveSuppressedForTests = false;
353
+ _suppressKeepaliveForTests() {
354
+ this.#keepaliveSuppressedForTests = true;
355
+ this.#stopKeepalive();
356
+ }
357
+ #startKeepalive() {
358
+ if (this.#keepaliveTimer !== void 0 || this.#keepaliveSuppressedForTests)
359
+ return;
360
+ const timer = setInterval(() => {
361
+ if (this.#served.size === 0) {
362
+ clearInterval(timer);
363
+ if (this.#keepaliveTimer === timer)
364
+ this.#keepaliveTimer = void 0;
365
+ return;
366
+ }
367
+ for (const [requestId, from] of this.#served) {
368
+ this.#answer(from, { kind: "busy", requestId });
369
+ }
370
+ }, Math.max(1, Math.floor(this.#rpcTimeoutMs / 2)));
371
+ timer.unref?.();
372
+ this.#keepaliveTimer = timer;
373
+ }
374
+ #stopKeepalive() {
375
+ if (this.#keepaliveTimer === void 0)
376
+ return;
377
+ clearInterval(this.#keepaliveTimer);
378
+ this.#keepaliveTimer = void 0;
379
+ }
380
+ #armHiddenIdleTimer() {
381
+ this.#clearHiddenIdleTimer();
382
+ if (this.#foreground || this.#leader === void 0 || this.#closed)
383
+ return;
384
+ const elapsed = Date.now() - this.#lastActivityAt;
385
+ const timer = setTimeout(() => {
386
+ this.#hiddenIdleTimer = void 0;
387
+ if (this.#closed || this.#leader === void 0 || this.#foreground)
388
+ return;
389
+ if (this.#othersSeen && this.#served.size === 0 && this.#yielding === void 0 && Date.now() - this.#lastActivityAt >= this.#hiddenIdleReleaseMs) {
390
+ this.#demote().catch((error) => {
391
+ this.#diagnostic(error, "opfs idle release");
392
+ });
393
+ return;
394
+ }
395
+ this.#armHiddenIdleTimer();
396
+ }, Math.max(0, this.#hiddenIdleReleaseMs - elapsed));
397
+ timer.unref?.();
398
+ this.#hiddenIdleTimer = timer;
399
+ }
400
+ #clearHiddenIdleTimer() {
401
+ if (this.#hiddenIdleTimer === void 0)
402
+ return;
403
+ clearTimeout(this.#hiddenIdleTimer);
404
+ this.#hiddenIdleTimer = void 0;
405
+ }
406
+ async #demote() {
407
+ const leader = this.#leader;
408
+ if (leader === void 0)
409
+ return;
410
+ this.#leader = void 0;
411
+ this.#knownLeader = void 0;
412
+ const shutdown = leader.shutdown().catch((error) => {
413
+ this.#diagnostic(error, "opfs idle release shutdown");
414
+ leader.crash();
415
+ });
416
+ this.#yielding = shutdown;
417
+ await shutdown;
418
+ if (this.#yielding === shutdown)
419
+ this.#yielding = void 0;
420
+ if (this.#closed)
421
+ return;
422
+ this.#closeAnswerChannels();
423
+ this.#post({ kind: "released", leaderId: this.#instanceId });
424
+ }
299
425
  async #openWithRetry(path) {
300
426
  for (let attempt = 0; ; attempt += 1) {
301
427
  try {
@@ -310,19 +436,66 @@ class OpfsBlockStore {
310
436
  }
311
437
  }
312
438
  setForeground(foreground) {
439
+ if (this.#closed)
440
+ return;
441
+ const changed = this.#foreground !== foreground;
313
442
  this.#foreground = foreground;
314
- if (foreground && this.#leader === void 0 && !this.#closed) {
443
+ if (this.#leader !== void 0) {
444
+ if (foreground) {
445
+ this.#clearHiddenIdleTimer();
446
+ this.#clearDeferredBid();
447
+ } else {
448
+ this.#lastActivityAt = Date.now();
449
+ this.#armHiddenIdleTimer();
450
+ }
451
+ if (changed)
452
+ this.#post({ kind: "state", leaderId: this.#instanceId, foreground });
453
+ return;
454
+ }
455
+ if (foreground)
315
456
  this.#post({ kind: "bid", bidderId: this.#instanceId, foreground: true });
457
+ }
458
+ #clearDeferredBid() {
459
+ if (this.#deferredBid === void 0)
460
+ return;
461
+ clearTimeout(this.#deferredBid.timer);
462
+ this.#deferredBid = void 0;
463
+ }
464
+ #considerBid(bidderId) {
465
+ if (this.#leader === void 0 || this.#foreground || this.#yielding !== void 0)
466
+ return;
467
+ const remaining = this.#yieldCooldownMs - (Date.now() - this.#lastYieldAt);
468
+ if (remaining <= 0) {
469
+ this.#clearDeferredBid();
470
+ this.#yieldLeadership(bidderId).catch((error) => {
471
+ this.#diagnostic(error, "opfs yield");
472
+ });
473
+ return;
474
+ }
475
+ if (this.#deferredBid !== void 0) {
476
+ this.#deferredBid.bidderId = bidderId;
477
+ return;
316
478
  }
479
+ const timer = setTimeout(() => {
480
+ const deferred = this.#deferredBid;
481
+ this.#deferredBid = void 0;
482
+ if (deferred !== void 0 && !this.#closed)
483
+ this.#considerBid(deferred.bidderId);
484
+ }, remaining);
485
+ timer.unref?.();
486
+ this.#deferredBid = { bidderId, timer };
317
487
  }
318
488
  async #yieldLeadership(to) {
319
489
  const leader = this.#leader;
320
490
  if (leader === void 0)
321
491
  return;
492
+ this.#clearHiddenIdleTimer();
493
+ this.#clearDeferredBid();
322
494
  this.#leader = void 0;
323
495
  this.#knownLeader = void 0;
324
496
  this.#lastYieldAt = Date.now();
325
- const shutdown = leader.shutdown().catch(() => {
497
+ const shutdown = leader.shutdown().catch((error) => {
498
+ this.#diagnostic(error, "opfs yield shutdown");
326
499
  leader.crash();
327
500
  });
328
501
  this.#yielding = shutdown;
@@ -333,22 +506,32 @@ class OpfsBlockStore {
333
506
  return;
334
507
  this.#closeAnswerChannels();
335
508
  this.#post({ kind: "yield", to });
509
+ this.#post({ kind: "released", leaderId: this.#instanceId });
336
510
  if (this.#reacquireTimer !== void 0)
337
511
  clearTimeout(this.#reacquireTimer);
338
512
  this.#reacquireTimer = setTimeout(() => {
339
- if (this.#knownLeader === void 0 && !this.#closed)
340
- void this.#tryBecomeLeader();
341
- }, 1500);
513
+ if (this.#knownLeader === void 0 && !this.#closed) {
514
+ this.#electInBackground("opfs reacquire after yield", true);
515
+ }
516
+ }, this.#handoverGraceMs);
342
517
  }
343
518
  #yielding;
344
519
  #onMessage(message) {
345
- if (this.#closed)
520
+ if (this.#closed) {
521
+ if (message.kind === "op")
522
+ this.#decline(message);
346
523
  return;
524
+ }
347
525
  if (this.#coordinationPausedForTests)
348
526
  return;
527
+ this.#othersSeen = true;
349
528
  switch (message.kind) {
350
529
  case "op": {
351
- if (this.#leader !== void 0) {
530
+ if (this.#leader === void 0) {
531
+ this.#decline(message);
532
+ return;
533
+ }
534
+ {
352
535
  const requestBytes = estimateRpcValueBytes(message.args);
353
536
  if (this.#servedRequestCount >= RPC_SERVER_ADMISSION_LIMIT || this.#servedRequestBytes + requestBytes > RPC_SERVER_ADMISSION_BYTES) {
354
537
  this.#answer(message.from, {
@@ -361,13 +544,51 @@ class OpfsBlockStore {
361
544
  }
362
545
  this.#servedRequestCount += 1;
363
546
  this.#servedRequestBytes += requestBytes;
364
- void this.#serveOp(message).finally(() => {
547
+ this.#lastActivityAt = Date.now();
548
+ const alreadyServing = this.#served.has(message.requestId);
549
+ this.#served.set(message.requestId, message.from);
550
+ this.#startKeepalive();
551
+ void this.#serveOp(message).catch((error) => {
552
+ this.#diagnostic(error, `opfs served ${message.method}`);
553
+ this.#answer(message.from, {
554
+ kind: "result",
555
+ requestId: message.requestId,
556
+ ok: false,
557
+ error: serializeStoreError(error)
558
+ });
559
+ }).finally(() => {
365
560
  this.#servedRequestCount = Math.max(0, this.#servedRequestCount - 1);
366
561
  this.#servedRequestBytes = Math.max(0, this.#servedRequestBytes - requestBytes);
562
+ if (!alreadyServing)
563
+ this.#served.delete(message.requestId);
564
+ this.#lastActivityAt = Date.now();
367
565
  });
368
566
  }
369
567
  return;
370
568
  }
569
+ case "declined": {
570
+ const pending = this.#takePending(message.requestId);
571
+ if (pending === void 0)
572
+ return;
573
+ clearTimeout(pending.timer);
574
+ pending.reject(RPC_DECLINED);
575
+ return;
576
+ }
577
+ case "uncertain": {
578
+ const pending = this.#takePending(message.requestId);
579
+ if (pending === void 0)
580
+ return;
581
+ clearTimeout(pending.timer);
582
+ pending.reject(new OpfsUncertainOutcomeError(pending.message.method));
583
+ return;
584
+ }
585
+ case "hold": {
586
+ const pending = this.#pending.get(message.requestId);
587
+ if (pending === void 0)
588
+ return;
589
+ this.#armPendingTimer(pending, Math.max(message.ms, this.#rpcTimeoutMs));
590
+ return;
591
+ }
371
592
  case "result": {
372
593
  const pending = this.#takePending(message.requestId);
373
594
  if (pending === void 0)
@@ -383,26 +604,28 @@ class OpfsBlockStore {
383
604
  const pending = this.#pending.get(message.requestId);
384
605
  if (pending === void 0)
385
606
  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);
607
+ this.#armPendingTimer(pending, this.#rpcTimeoutMs);
391
608
  return;
392
609
  }
393
610
  case "leader": {
394
611
  this.#knownLeader = message.leaderId;
612
+ this.#gracefulLeaders.delete(message.leaderId);
395
613
  if (this.#reacquireTimer !== void 0) {
396
614
  clearTimeout(this.#reacquireTimer);
397
615
  this.#reacquireTimer = void 0;
398
616
  }
399
- for (const [requestId, pending] of this.#pending) {
400
- if (pending.sentTo === message.leaderId)
617
+ for (const pending of this.#pending.values()) {
618
+ if (pending.sentTo === message.leaderId) {
619
+ if (pending.awaitingPing)
620
+ this.#armPendingTimer(pending, this.#rpcTimeoutMs);
401
621
  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));
622
+ }
623
+ if (pending.sentTo === void 0) {
624
+ pending.sentTo = message.leaderId;
625
+ this.#send(message.leaderId, pending.message);
626
+ continue;
627
+ }
628
+ if (!READ_METHODS.has(pending.message.method) && this.#gracefulLeaders.has(pending.sentTo)) {
406
629
  continue;
407
630
  }
408
631
  pending.sentTo = message.leaderId;
@@ -413,6 +636,12 @@ class OpfsBlockStore {
413
636
  }
414
637
  return;
415
638
  }
639
+ case "state": {
640
+ if (!message.foreground && this.#foreground && this.#leader === void 0 && message.leaderId !== this.#instanceId) {
641
+ this.#post({ kind: "bid", bidderId: this.#instanceId, foreground: true });
642
+ }
643
+ return;
644
+ }
416
645
  case "ping": {
417
646
  if (this.#leader !== void 0) {
418
647
  this.#post({ kind: "leader", leaderId: this.#instanceId });
@@ -420,23 +649,61 @@ class OpfsBlockStore {
420
649
  return;
421
650
  }
422
651
  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);
652
+ if (message.foreground && message.bidderId !== this.#instanceId) {
653
+ this.#considerBid(message.bidderId);
425
654
  }
426
655
  return;
427
656
  }
428
657
  case "yield": {
429
- if (message.to === this.#instanceId)
430
- void this.#tryBecomeLeader();
658
+ if (message.to === this.#instanceId) {
659
+ this.#knownLeader = void 0;
660
+ this.#electInBackground("opfs election after yield", true);
661
+ }
431
662
  return;
432
663
  }
433
664
  case "released": {
434
665
  if (this.#knownLeader === message.leaderId)
435
666
  this.#knownLeader = void 0;
667
+ this.#gracefulLeaders.add(message.leaderId);
668
+ if (this.#gracefulLeaders.size > 16) {
669
+ const [oldest] = this.#gracefulLeaders;
670
+ if (oldest !== void 0)
671
+ this.#gracefulLeaders.delete(oldest);
672
+ }
436
673
  return;
437
674
  }
438
675
  }
439
676
  }
677
+ #decline(message) {
678
+ this.#postOnce(message.from, {
679
+ kind: "declined",
680
+ requestId: message.requestId,
681
+ reason: "not-leader"
682
+ });
683
+ }
684
+ #armPendingTimer(pending, ms) {
685
+ clearTimeout(pending.timer);
686
+ pending.awaitingPing = false;
687
+ pending.timer = setTimeout(() => {
688
+ this.#onPendingTimeout(pending.message.requestId);
689
+ }, ms);
690
+ }
691
+ #onPendingTimeout(requestId) {
692
+ const pending = this.#pending.get(requestId);
693
+ if (pending === void 0)
694
+ return;
695
+ if (READ_METHODS.has(pending.message.method) || pending.awaitingPing || pending.patienceRounds >= MUTATION_PATIENCE_ROUNDS || this.#channel === void 0) {
696
+ this.#takePending(requestId);
697
+ pending.reject(RPC_TIMED_OUT);
698
+ return;
699
+ }
700
+ pending.patienceRounds += 1;
701
+ pending.awaitingPing = true;
702
+ this.#post({ kind: "ping" });
703
+ pending.timer = setTimeout(() => {
704
+ this.#onPendingTimeout(requestId);
705
+ }, DISCOVERY_WAIT_MS);
706
+ }
440
707
  #takePending(requestId) {
441
708
  const pending = this.#pending.get(requestId);
442
709
  if (pending === void 0)
@@ -446,7 +713,7 @@ class OpfsBlockStore {
446
713
  return pending;
447
714
  }
448
715
  async #serveOp(message) {
449
- const requestKey = `${String(message.from.length)}:${message.from}${message.requestId}`;
716
+ const requestKey = servedRequestKey(message.from, message.requestId);
450
717
  if (this.#inFlightMutations.has(requestKey) || this.#settledMutations.has(requestKey)) {
451
718
  await this.#serveOpLocked(message, requestKey);
452
719
  return;
@@ -476,10 +743,38 @@ class OpfsBlockStore {
476
743
  let fingerprint;
477
744
  try {
478
745
  fingerprint = await fingerprintStoreRequest(message.method, message.args);
479
- } catch {
746
+ } catch (error) {
747
+ this.#answer(message.from, {
748
+ kind: "result",
749
+ requestId: message.requestId,
750
+ ok: false,
751
+ error: serializeStoreError(error)
752
+ });
480
753
  return;
481
754
  }
482
- const remembered = isRead ? void 0 : this.#settledMutations.get(requestKey);
755
+ let remembered = isRead ? void 0 : this.#settledMutations.get(requestKey);
756
+ const inFlight = isRead || remembered !== void 0 ? void 0 : this.#inFlightMutations.get(requestKey);
757
+ if (remembered === void 0 && inFlight === void 0 && !isRead) {
758
+ const logged = leader.servedOutcome(requestKey);
759
+ if (logged !== void 0) {
760
+ if (logged.method !== message.method || logged.signature !== fingerprint.signature) {
761
+ this.#rejectReusedRequestIdentity(message);
762
+ return;
763
+ }
764
+ if (!logged.settled || logged.withheld === true) {
765
+ this.#answer(message.from, { kind: "uncertain", requestId: message.requestId });
766
+ return;
767
+ }
768
+ const outcome = { ok: true, value: logged.result };
769
+ this.#rememberSettledMutation(requestKey, logged.method, { signature: logged.signature, retainedBytes: logged.requestBytes }, outcome);
770
+ remembered = this.#settledMutations.get(requestKey) ?? {
771
+ method: logged.method,
772
+ signature: logged.signature,
773
+ requestBytes: logged.requestBytes,
774
+ outcome
775
+ };
776
+ }
777
+ }
483
778
  let settled;
484
779
  if (remembered !== void 0) {
485
780
  if (!sameServedRequest(remembered, message, fingerprint.signature)) {
@@ -489,10 +784,7 @@ class OpfsBlockStore {
489
784
  this.#settledMutations.delete(requestKey);
490
785
  this.#settledMutations.set(requestKey, remembered);
491
786
  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");
787
+ } else if (inFlight !== void 0) {
496
788
  if (!sameServedRequest(inFlight, message, fingerprint.signature)) {
497
789
  this.#rejectReusedRequestIdentity(message);
498
790
  return;
@@ -500,6 +792,10 @@ class OpfsBlockStore {
500
792
  this.#answer(message.from, { kind: "busy", requestId: message.requestId });
501
793
  settled = await inFlight.outcome;
502
794
  } else {
795
+ if (!isRead && message.sentAt < leader.servedCoverageSince) {
796
+ this.#answer(message.from, { kind: "uncertain", requestId: message.requestId });
797
+ return;
798
+ }
503
799
  if (!isRead && (this.#inFlightMutations.size >= RPC_IN_FLIGHT_LIMIT || this.#inFlightMutationBytes + fingerprint.retainedBytes > RPC_IN_FLIGHT_MUTATION_BYTES)) {
504
800
  this.#answer(message.from, {
505
801
  kind: "result",
@@ -521,11 +817,15 @@ class OpfsBlockStore {
521
817
  this.#readCapacityChanged = { promise, resolve };
522
818
  }
523
819
  await this.#readCapacityChanged.promise;
524
- if (this.#closed || this.#leader !== leader)
820
+ if (this.#closed || this.#leader !== leader) {
821
+ this.#decline(message);
525
822
  return;
823
+ }
526
824
  }
527
- if (this.#closed || this.#leader !== leader)
825
+ if (this.#closed || this.#leader !== leader) {
826
+ this.#decline(message);
528
827
  return;
828
+ }
529
829
  this.#inFlightReadBytes += reservation;
530
830
  try {
531
831
  settled = await this.#executeServedOpAfterGate(leader, message, true);
@@ -535,11 +835,17 @@ class OpfsBlockStore {
535
835
  }
536
836
  } else {
537
837
  this.#inFlightMutationBytes += fingerprint.retainedBytes;
538
- const execution = this.#executeServedOpAfterGate(leader, message, false);
838
+ const execution = this.#executeServedOpAfterGate(leader, message, false, {
839
+ key: requestKey,
840
+ method: message.method,
841
+ signature: fingerprint.signature,
842
+ requestBytes: fingerprint.retainedBytes,
843
+ sentAt: Math.min(message.sentAt, Date.now())
844
+ });
539
845
  const outcome = execution.then((result) => {
540
846
  this.#inFlightMutations.delete(requestKey);
541
847
  this.#inFlightMutationBytes = Math.max(0, this.#inFlightMutationBytes - fingerprint.retainedBytes);
542
- if (!this.#closed) {
848
+ if (!this.#closed && result !== DECLINED_OUTCOME) {
543
849
  this.#rememberSettledMutation(requestKey, message.method, fingerprint, result);
544
850
  }
545
851
  return result;
@@ -553,6 +859,8 @@ class OpfsBlockStore {
553
859
  settled = await outcome;
554
860
  }
555
861
  }
862
+ if (settled === DECLINED_OUTCOME)
863
+ return;
556
864
  if (this.#dropNextRpcResultForTests) {
557
865
  this.#dropNextRpcResultForTests = false;
558
866
  return;
@@ -601,20 +909,58 @@ class OpfsBlockStore {
601
909
  this.#readCapacityChanged = void 0;
602
910
  waiting?.resolve();
603
911
  }
604
- async #executeServedOpAfterGate(leader, message, isRead) {
912
+ async #executeServedOpAfterGate(leader, message, isRead, request) {
605
913
  const gate = this.#servedMutationGateForTests;
606
914
  if (!isRead && gate !== void 0)
607
915
  await gate;
608
- return this.#executeServedOp(leader, message);
916
+ return this.#executeServedOp(leader, message, request);
917
+ }
918
+ #leads(leader) {
919
+ return this.#leader === leader;
920
+ }
921
+ #mutationTail = Promise.resolve();
922
+ #withMutationTurn(run) {
923
+ const turn = this.#mutationTail.then(run);
924
+ this.#mutationTail = turn.then(() => void 0, () => void 0);
925
+ return turn;
609
926
  }
610
- async #executeServedOp(leader, message) {
927
+ async #executeServedOp(leader, message, request) {
928
+ if (leader.isClosed() || !this.#leads(leader)) {
929
+ this.#decline(message);
930
+ return DECLINED_OUTCOME;
931
+ }
611
932
  try {
612
933
  const method = leader[message.method];
613
934
  if (method === void 0) {
614
935
  return { ok: false, error: { name: "Error", message: "Unknown store operation" } };
615
936
  }
616
- return { ok: true, value: await method.apply(leader, message.args) };
937
+ if (request === void 0) {
938
+ return { ok: true, value: await method.apply(leader, message.args) };
939
+ }
940
+ return await this.#withMutationTurn(async () => {
941
+ if (leader.isClosed() || !this.#leads(leader)) {
942
+ this.#decline(message);
943
+ return DECLINED_OUTCOME;
944
+ }
945
+ leader.servingRequest = request;
946
+ try {
947
+ const value = await method.apply(leader, message.args);
948
+ if (leader.servingRequest !== request) {
949
+ await leader.completeServed(request.key, value).catch((error) => {
950
+ this.#diagnostic(error, `opfs served result for ${message.method}`);
951
+ });
952
+ }
953
+ return { ok: true, value };
954
+ } finally {
955
+ if (leader.servingRequest === request)
956
+ leader.servingRequest = void 0;
957
+ }
958
+ });
617
959
  } catch (error) {
960
+ if (leader.isClosed() && !this.#leads(leader)) {
961
+ this.#decline(message);
962
+ return DECLINED_OUTCOME;
963
+ }
618
964
  return { ok: false, error: serializeStoreError(error) };
619
965
  }
620
966
  }
@@ -663,9 +1009,20 @@ class OpfsBlockStore {
663
1009
  #closeChannels() {
664
1010
  this.#channel?.close();
665
1011
  this.#channel = void 0;
666
- this.#inbox?.close();
667
- this.#inbox = void 0;
668
1012
  this.#closeAnswerChannels();
1013
+ const inbox = this.#inbox;
1014
+ this.#inbox = void 0;
1015
+ if (inbox === void 0)
1016
+ return;
1017
+ if (this.#inboxLingerTimer !== void 0)
1018
+ clearTimeout(this.#inboxLingerTimer);
1019
+ const timer = setTimeout(() => {
1020
+ this.#inboxLingerTimer = void 0;
1021
+ inbox.close();
1022
+ }, DECLINE_AFTER_CLOSE_MS);
1023
+ timer.unref?.();
1024
+ inbox.unref?.();
1025
+ this.#inboxLingerTimer = timer;
669
1026
  }
670
1027
  #assertOpen() {
671
1028
  if (this.#closed)
@@ -674,6 +1031,11 @@ class OpfsBlockStore {
674
1031
  async #dispatch(method, args) {
675
1032
  this.#assertOpen();
676
1033
  const requestId = crypto.randomUUID();
1034
+ const sentAt = Date.now();
1035
+ const isRead = READ_METHODS.has(method);
1036
+ let sentRemotely = false;
1037
+ let mayHaveRun = false;
1038
+ this.#lastActivityAt = sentAt;
677
1039
  for (let attempt = 0; attempt < DISPATCH_ATTEMPTS; attempt += 1) {
678
1040
  this.#assertOpen();
679
1041
  const leader = this.#leader;
@@ -681,7 +1043,25 @@ class OpfsBlockStore {
681
1043
  const bound = leader[method];
682
1044
  if (bound === void 0)
683
1045
  throw new Error(`Unknown store operation: ${method}`);
684
- return bound.apply(leader, args);
1046
+ try {
1047
+ if (isRead)
1048
+ return await bound.apply(leader, args);
1049
+ if (sentRemotely) {
1050
+ const key = servedRequestKey(this.#instanceId, requestId);
1051
+ const logged = leader.servedOutcome(key);
1052
+ if (logged !== void 0) {
1053
+ if (!logged.settled || logged.withheld === true) {
1054
+ throw new OpfsUncertainOutcomeError(method);
1055
+ }
1056
+ return logged.result;
1057
+ }
1058
+ if (sentAt < leader.servedCoverageSince)
1059
+ throw new OpfsUncertainOutcomeError(method);
1060
+ }
1061
+ return await this.#withMutationTurn(() => bound.apply(leader, args));
1062
+ } finally {
1063
+ this.#lastActivityAt = Date.now();
1064
+ }
685
1065
  }
686
1066
  if (this.#channel === void 0) {
687
1067
  if (await this.#tryBecomeLeader())
@@ -699,23 +1079,26 @@ class OpfsBlockStore {
699
1079
  }
700
1080
  }
701
1081
  try {
702
- return await this.#rpc(requestId, method, args);
1082
+ sentRemotely = true;
1083
+ return await this.#rpc(requestId, method, args, sentAt);
703
1084
  } catch (error) {
704
- if (error === RPC_TIMED_OUT) {
1085
+ if (error === RPC_DECLINED || error === RPC_TIMED_OUT) {
1086
+ if (error === RPC_TIMED_OUT)
1087
+ mayHaveRun = true;
705
1088
  this.#knownLeader = void 0;
706
- if (!READ_METHODS.has(method))
707
- throw new OpfsUncertainOutcomeError(method);
708
1089
  continue;
709
1090
  }
710
1091
  throw error;
711
1092
  }
712
1093
  }
1094
+ if (!isRead && mayHaveRun)
1095
+ throw new OpfsUncertainOutcomeError(method);
713
1096
  throw new OpfsCoordinationError("leader-unavailable", method);
714
1097
  }
715
1098
  #leaderKnown() {
716
1099
  return this.#knownLeader !== void 0;
717
1100
  }
718
- #rpc(requestId, method, args) {
1101
+ #rpc(requestId, method, args, sentAt) {
719
1102
  let retainedBytes;
720
1103
  try {
721
1104
  retainedBytes = estimateRpcValueBytes(args);
@@ -726,10 +1109,16 @@ class OpfsBlockStore {
726
1109
  return Promise.reject(new OpfsCoordinationError("follower-queue-full", method));
727
1110
  }
728
1111
  return new Promise((resolve, reject) => {
729
- const message = { kind: "op", requestId, from: this.#instanceId, method, args };
1112
+ const message = {
1113
+ kind: "op",
1114
+ requestId,
1115
+ from: this.#instanceId,
1116
+ method,
1117
+ args,
1118
+ sentAt
1119
+ };
730
1120
  const timer = setTimeout(() => {
731
- const expired = this.#takePending(requestId);
732
- expired?.reject(RPC_TIMED_OUT);
1121
+ this.#onPendingTimeout(requestId);
733
1122
  }, this.#rpcTimeoutMs);
734
1123
  const leaderId = this.#knownLeader;
735
1124
  this.#pending.set(requestId, {
@@ -738,7 +1127,9 @@ class OpfsBlockStore {
738
1127
  sentTo: leaderId,
739
1128
  resolve,
740
1129
  reject,
741
- timer
1130
+ timer,
1131
+ patienceRounds: 0,
1132
+ awaitingPing: false
742
1133
  });
743
1134
  this.#pendingRpcBytes += retainedBytes;
744
1135
  if (leaderId !== void 0)
@@ -777,8 +1168,7 @@ class OpfsBlockStore {
777
1168
  if (this.#closed)
778
1169
  return;
779
1170
  this.#closed = true;
780
- if (this.#reacquireTimer !== void 0)
781
- clearTimeout(this.#reacquireTimer);
1171
+ this.#clearCoordinationTimers();
782
1172
  for (const pending of this.#pending.values()) {
783
1173
  clearTimeout(pending.timer);
784
1174
  pending.reject(new Error("This OPFS store connection is closed"));
@@ -797,7 +1187,8 @@ class OpfsBlockStore {
797
1187
  const leader = this.#leader;
798
1188
  this.#leader = void 0;
799
1189
  if (leader !== void 0) {
800
- void leader.shutdown().catch(() => {
1190
+ void leader.shutdown().catch((error) => {
1191
+ this.#diagnostic(error, "opfs close shutdown");
801
1192
  leader.crash();
802
1193
  }).then(() => {
803
1194
  this.#post({ kind: "released", leaderId: this.#instanceId });
@@ -806,9 +1197,26 @@ class OpfsBlockStore {
806
1197
  });
807
1198
  return;
808
1199
  }
1200
+ if (this.#yielding !== void 0) {
1201
+ void this.#yielding.then(() => {
1202
+ this.#post({ kind: "released", leaderId: this.#instanceId });
1203
+ this.#closeChannels();
1204
+ this.#releaseWhenHandlesClose();
1205
+ });
1206
+ return;
1207
+ }
809
1208
  this.#closeChannels();
810
1209
  this.#releaseWhenHandlesClose();
811
1210
  }
1211
+ #clearCoordinationTimers() {
1212
+ if (this.#reacquireTimer !== void 0)
1213
+ clearTimeout(this.#reacquireTimer);
1214
+ this.#reacquireTimer = void 0;
1215
+ this.#stopKeepalive();
1216
+ this.#clearDeferredBid();
1217
+ this.#clearHiddenIdleTimer();
1218
+ this.#served.clear();
1219
+ }
812
1220
  #releaseWhenHandlesClose() {
813
1221
  const release = this.#releaseConnectionLock;
814
1222
  this.#releaseConnectionLock = void 0;
@@ -846,6 +1254,9 @@ class OpfsBlockStore {
846
1254
  release();
847
1255
  };
848
1256
  }
1257
+ _oldestPendingRequestIdForTests() {
1258
+ return this.#pending.keys().next().value;
1259
+ }
849
1260
  _resendOldestPendingForTests() {
850
1261
  const pending = this.#pending.values().next().value;
851
1262
  if (pending?.sentTo !== void 0)
@@ -863,8 +1274,7 @@ class OpfsBlockStore {
863
1274
  }
864
1275
  _crashForTests() {
865
1276
  this.#closed = true;
866
- if (this.#reacquireTimer !== void 0)
867
- clearTimeout(this.#reacquireTimer);
1277
+ this.#clearCoordinationTimers();
868
1278
  for (const pending of this.#pending.values())
869
1279
  clearTimeout(pending.timer);
870
1280
  this.#pending.clear();
@@ -880,7 +1290,11 @@ class OpfsBlockStore {
880
1290
  this.#servedRequestBytes = 0;
881
1291
  this.#leader?.crash();
882
1292
  this.#leader = void 0;
883
- this.#closeChannels();
1293
+ this.#channel?.close();
1294
+ this.#channel = void 0;
1295
+ this.#inbox?.close();
1296
+ this.#inbox = void 0;
1297
+ this.#closeAnswerChannels();
884
1298
  this.#releaseWhenHandlesClose();
885
1299
  }
886
1300
  async #ensureFormatMarker() {
@@ -954,23 +1368,35 @@ for (const method of RPC_METHODS) {
954
1368
  }
955
1369
  });
956
1370
  }
1371
+ const DELETE_LOCK_WAIT_MS = 1e3;
957
1372
  async function deleteOpfsDatabase(options) {
958
1373
  const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
959
1374
  const locks = globalThis.navigator?.locks;
960
1375
  if (locks === void 0)
961
1376
  throw new Error("Deleting an OPFS database requires Web Locks");
962
- await locks.request(connectionLockName(options.name), { mode: "exclusive", ifAvailable: true }, async (lock) => {
963
- if (lock === null)
1377
+ const abort = new AbortController();
1378
+ const timer = setTimeout(() => {
1379
+ abort.abort();
1380
+ }, DELETE_LOCK_WAIT_MS);
1381
+ try {
1382
+ await locks.request(connectionLockName(options.name), { mode: "exclusive", signal: abort.signal }, async () => {
1383
+ const root = options.root ?? await navigator.storage.getDirectory();
1384
+ try {
1385
+ const namespace = await root.getDirectoryHandle("minnowdb");
1386
+ await namespace.removeEntry(encodedName, { recursive: true });
1387
+ } catch (error) {
1388
+ if (!isDomError(error, "NotFoundError"))
1389
+ throw error;
1390
+ }
1391
+ });
1392
+ } catch (error) {
1393
+ if (abort.signal.aborted && isDomError(error, "AbortError")) {
964
1394
  throw new OpfsDatabaseInUseError(options.name);
965
- const root = options.root ?? await navigator.storage.getDirectory();
966
- try {
967
- const namespace = await root.getDirectoryHandle("minnowdb");
968
- await namespace.removeEntry(encodedName, { recursive: true });
969
- } catch (error) {
970
- if (!isDomError(error, "NotFoundError"))
971
- throw error;
972
1395
  }
973
- });
1396
+ throw error;
1397
+ } finally {
1398
+ clearTimeout(timer);
1399
+ }
974
1400
  }
975
1401
  function connectionLockName(name) {
976
1402
  return `minnowdb-opfs-connections:${name}`;
@@ -985,7 +1411,12 @@ async function holdConnectionLock(name) {
985
1411
  })).catch(reject);
986
1412
  });
987
1413
  }
1414
+ function servedRequestKey(from, requestId) {
1415
+ return `${String(from.length)}:${from}${requestId}`;
1416
+ }
988
1417
  const RPC_TIMED_OUT = new Error("The leader did not answer in time");
1418
+ const RPC_DECLINED = new Error("The connection asked is not the leader");
1419
+ const DECLINED_OUTCOME = { ok: false };
989
1420
  async function resolveDatabaseRoot(options) {
990
1421
  const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
991
1422
  const root = options.root ?? await navigator.storage.getDirectory();