js-bao 0.12.0-alpha.6 → 0.12.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.cjs CHANGED
@@ -2494,16 +2494,18 @@ function refusedWritesOf(entry) {
2494
2494
  seq: entry.seq
2495
2495
  }));
2496
2496
  }
2497
- var RECORD_LOG_DEFAULT_EPOCH_ENTRIES, RECORD_LOG_ENTRY_MAX_BYTES, RECORD_LOG_FRAME_TARGET_BYTES, RECORD_LOG_SEND_WINDOW, RECORD_LOG_TICK_MS, RECORD_LOG_UP_BATCH_MAX, RECORD_LOG_FIRST_EPOCH, RecordLogStaleReceiptError, RecordLogStaleReplacementError, RecordLogWriteRefusedError;
2497
+ var RECORD_LOG_DEFAULT_EPOCH_ENTRIES, RECORD_LOG_ENTRY_MAX_BYTES, RECORD_LOG_FRAME_TARGET_BYTES, RECORD_LOG_FRAMES_IN_FLIGHT, RECORD_LOG_FRAME_WAIT_MS, RECORD_LOG_TICK_MS, RECORD_LOG_UP_BATCH_MAX, RECORD_LOG_FRAME_ENTRIES_MAX, RECORD_LOG_FIRST_EPOCH, RecordLogStaleReceiptError, RecordLogStaleReplacementError, RecordLogWriteRefusedError;
2498
2498
  var init_types = __esm({
2499
2499
  "src/record-log-client/types.ts"() {
2500
2500
  "use strict";
2501
2501
  RECORD_LOG_DEFAULT_EPOCH_ENTRIES = 1e4;
2502
2502
  RECORD_LOG_ENTRY_MAX_BYTES = 1024 * 1024;
2503
2503
  RECORD_LOG_FRAME_TARGET_BYTES = 64 * 1024;
2504
- RECORD_LOG_SEND_WINDOW = 8;
2504
+ RECORD_LOG_FRAMES_IN_FLIGHT = 1;
2505
+ RECORD_LOG_FRAME_WAIT_MS = 2e3;
2505
2506
  RECORD_LOG_TICK_MS = 5;
2506
2507
  RECORD_LOG_UP_BATCH_MAX = 200;
2508
+ RECORD_LOG_FRAME_ENTRIES_MAX = 64;
2507
2509
  RECORD_LOG_FIRST_EPOCH = 1;
2508
2510
  RecordLogStaleReceiptError = class _RecordLogStaleReceiptError extends Error {
2509
2511
  /**
@@ -8737,9 +8739,11 @@ __export(browser_exports, {
8737
8739
  RECORD_LOG_DRAIN_WAIT_MS: () => RECORD_LOG_DRAIN_WAIT_MS,
8738
8740
  RECORD_LOG_ENTRY_MAX_BYTES: () => RECORD_LOG_ENTRY_MAX_BYTES,
8739
8741
  RECORD_LOG_FIRST_EPOCH: () => RECORD_LOG_FIRST_EPOCH,
8742
+ RECORD_LOG_FRAMES_IN_FLIGHT: () => RECORD_LOG_FRAMES_IN_FLIGHT,
8743
+ RECORD_LOG_FRAME_ENTRIES_MAX: () => RECORD_LOG_FRAME_ENTRIES_MAX,
8740
8744
  RECORD_LOG_FRAME_TARGET_BYTES: () => RECORD_LOG_FRAME_TARGET_BYTES,
8745
+ RECORD_LOG_FRAME_WAIT_MS: () => RECORD_LOG_FRAME_WAIT_MS,
8741
8746
  RECORD_LOG_RESULT_GAP_MS: () => RECORD_LOG_RESULT_GAP_MS,
8742
- RECORD_LOG_SEND_WINDOW: () => RECORD_LOG_SEND_WINDOW,
8743
8747
  RECORD_LOG_STORE_CHANNEL: () => RECORD_LOG_STORE_CHANNEL,
8744
8748
  RECORD_LOG_TEARDOWN_DRAIN_BOUND_MS: () => RECORD_LOG_TEARDOWN_DRAIN_BOUND_MS,
8745
8749
  RECORD_LOG_TICK_MS: () => RECORD_LOG_TICK_MS,
@@ -10593,6 +10597,16 @@ var RecordLogClient = class {
10593
10597
  get pending() {
10594
10598
  return this.tables.pending(this.writer);
10595
10599
  }
10600
+ /** One of a writer's unjudged entries, or `undefined`. */
10601
+ entryOf(writer, seq) {
10602
+ if (this.tables.pendingEntry) return this.tables.pendingEntry(writer, seq);
10603
+ return this.tables.pending(writer).find((p) => p.seq === seq);
10604
+ }
10605
+ /** A writer's unjudged entries at or below `seq`, in `seq` order. */
10606
+ entriesThrough(writer, seq) {
10607
+ if (this.tables.pendingThrough) return this.tables.pendingThrough(writer, seq);
10608
+ return this.tables.pending(writer).filter((p) => p.seq <= seq);
10609
+ }
10596
10610
  /**
10597
10611
  * Write an entry: the next `seq`, the open epoch, the cursor. It joins the
10598
10612
  * pending log; the store applies it to the view as a guess.
@@ -10662,7 +10676,7 @@ var RecordLogClient = class {
10662
10676
  }
10663
10677
  /** The room has moved past a withdrawn entry's sequence number. */
10664
10678
  settled(seq) {
10665
- const entry = this.tables.pending(this.writer).find((p) => p.seq === seq);
10679
+ const entry = this.entryOf(this.writer, seq);
10666
10680
  if (entry?.void) this.tables.deletePending(this.writer, seq);
10667
10681
  return { touched: [] };
10668
10682
  }
@@ -10677,7 +10691,7 @@ var RecordLogClient = class {
10677
10691
  receive(item) {
10678
10692
  const out = { touched: [], acknowledged: [], settled: [] };
10679
10693
  if ("void" in item) {
10680
- const entry = this.tables.pending(this.writer).find((p) => p.seq === item.seq);
10694
+ const entry = this.entryOf(this.writer, item.seq);
10681
10695
  if (entry) {
10682
10696
  this.tables.deletePending(this.writer, item.seq);
10683
10697
  out.settled.push({ writer: this.writer, seq: item.seq });
@@ -10686,7 +10700,7 @@ var RecordLogClient = class {
10686
10700
  return out;
10687
10701
  }
10688
10702
  if ("duplicate" in item) {
10689
- const entry = this.tables.pending(this.writer).find((p) => p.seq === item.seq);
10703
+ const entry = this.entryOf(this.writer, item.seq);
10690
10704
  out.acknowledged.push({ writer: this.writer, seq: item.seq });
10691
10705
  if (entry) {
10692
10706
  this.tables.deletePending(this.writer, item.seq);
@@ -10696,7 +10710,7 @@ var RecordLogClient = class {
10696
10710
  return out;
10697
10711
  }
10698
10712
  if (item.w !== void 0 && item.seq !== void 0 && this.tables.isWriter(item.w)) {
10699
- const entry = this.tables.pending(item.w).find((p) => p.seq === item.seq);
10713
+ const entry = this.entryOf(item.w, item.seq);
10700
10714
  out.acknowledged.push({ writer: item.w, seq: item.seq });
10701
10715
  if (entry && !entry.void && entry.acked === void 0) {
10702
10716
  this.tables.putPending(item.w, { ...entry, acked: item.l, ...item.alias ? { alias: item.alias } : {} });
@@ -10729,7 +10743,7 @@ var RecordLogClient = class {
10729
10743
  */
10730
10744
  settleAcknowledged(out) {
10731
10745
  for (const { writer, seq } of this.tables.acknowledgedThrough(this.tables.cursor())) {
10732
- const entry = this.tables.pending(writer).find((p) => p.seq === seq);
10746
+ const entry = this.entryOf(writer, seq);
10733
10747
  if (!entry) continue;
10734
10748
  this.tables.deletePending(writer, seq);
10735
10749
  out.settled.push({ writer, seq });
@@ -10744,8 +10758,7 @@ var RecordLogClient = class {
10744
10758
  * wrote since.
10745
10759
  */
10746
10760
  confirmEntry(writer, seq, alias, out) {
10747
- for (const p of this.tables.pending(writer)) {
10748
- if (p.seq > seq) continue;
10761
+ for (const p of this.entriesThrough(writer, seq)) {
10749
10762
  this.tables.deletePending(writer, p.seq);
10750
10763
  out.settled.push({ writer, seq: p.seq });
10751
10764
  if (!p.void) out.touched.push(...keysOf(p));
@@ -11208,6 +11221,12 @@ var MemoryRecordLogTables = class {
11208
11221
  pending(writer) {
11209
11222
  return [...this.writers.get(writer)?.pending ?? []];
11210
11223
  }
11224
+ pendingEntry(writer, seq) {
11225
+ return this.writers.get(writer)?.pending.find((p) => p.seq === seq);
11226
+ }
11227
+ pendingThrough(writer, seq) {
11228
+ return (this.writers.get(writer)?.pending ?? []).filter((p) => p.seq <= seq);
11229
+ }
11211
11230
  putPending(writer, entry) {
11212
11231
  const w = this.writerOf(writer);
11213
11232
  const at = w.pending.findIndex((p) => p.seq === entry.seq);
@@ -11332,6 +11351,10 @@ function recordLogClientDDL() {
11332
11351
  PRIMARY KEY (doc_id, writer, seq)
11333
11352
  )`,
11334
11353
  `CREATE INDEX IF NOT EXISTS record_log_pending_acked ON record_log_pending(doc_id, acked) WHERE acked IS NOT NULL`,
11354
+ // A write numbers its entry after the highest `ord` and counts its
11355
+ // epoch's unwithdrawn entries; neither reads the entries.
11356
+ `CREATE INDEX IF NOT EXISTS record_log_pending_by_ord ON record_log_pending(doc_id, ord)`,
11357
+ `CREATE INDEX IF NOT EXISTS record_log_pending_by_epoch ON record_log_pending(doc_id, e) WHERE void = 0`,
11335
11358
  `CREATE TABLE IF NOT EXISTS record_log_pending_keys (
11336
11359
  doc_id TEXT NOT NULL,
11337
11360
  writer TEXT NOT NULL,
@@ -11341,7 +11364,10 @@ function recordLogClientDDL() {
11341
11364
  resolved INTEGER NOT NULL DEFAULT 0,
11342
11365
  PRIMARY KEY (doc_id, writer, seq, _type, _id)
11343
11366
  )`,
11344
- `CREATE INDEX IF NOT EXISTS record_log_pending_keys_by_record ON record_log_pending_keys(doc_id, _type, _id)`,
11367
+ // The entries touching a record, answered from the index alone: without
11368
+ // `writer, seq` in it SQLite reads the keys' primary key by document.
11369
+ `DROP INDEX IF EXISTS record_log_pending_keys_by_record`,
11370
+ `CREATE INDEX IF NOT EXISTS record_log_pending_keys_by_record_entry ON record_log_pending_keys(doc_id, _type, _id, writer, seq)`,
11345
11371
  `CREATE TABLE IF NOT EXISTS record_log_pending_values (
11346
11372
  doc_id TEXT NOT NULL,
11347
11373
  writer TEXT NOT NULL,
@@ -11445,6 +11471,11 @@ function queryRow(row, spec) {
11445
11471
  function rowsOf(sql, query, ...bindings) {
11446
11472
  return Array.from(sql.exec(query, ...bindings));
11447
11473
  }
11474
+ function pendingRow(row) {
11475
+ const entry = JSON.parse(String(row.entry));
11476
+ if (row.acked !== null && row.acked !== void 0) entry.acked = Number(row.acked);
11477
+ return entry;
11478
+ }
11448
11479
  var SqlRecordLogTables = class {
11449
11480
  constructor(sql, docId) {
11450
11481
  this.sql = sql;
@@ -11456,6 +11487,11 @@ var SqlRecordLogTables = class {
11456
11487
  heldWarned = false;
11457
11488
  /** Records of entries changed or removed since the last {@link takeDirty}, resolutions included. */
11458
11489
  dirty = [];
11490
+ /**
11491
+ * Inside {@link countingEpochs}, each epoch's unwithdrawn pending entries
11492
+ * once counted, kept as entries are added; `null` outside it.
11493
+ */
11494
+ epochPending = null;
11459
11495
  state() {
11460
11496
  return rowsOf(this.sql, `SELECT * FROM record_log_state WHERE doc_id = ?`, this.docId)[0] ?? null;
11461
11497
  }
@@ -11680,16 +11716,32 @@ var SqlRecordLogTables = class {
11680
11716
  `SELECT entry, acked FROM record_log_pending WHERE doc_id = ? AND writer = ? ORDER BY seq`,
11681
11717
  this.docId,
11682
11718
  writer
11683
- ).map((row) => {
11684
- const entry = JSON.parse(String(row.entry));
11685
- if (row.acked !== null && row.acked !== void 0) entry.acked = Number(row.acked);
11686
- return entry;
11687
- });
11719
+ ).map(pendingRow);
11720
+ }
11721
+ pendingEntry(writer, seq) {
11722
+ const [row] = rowsOf(
11723
+ this.sql,
11724
+ `SELECT entry, acked FROM record_log_pending WHERE doc_id = ? AND writer = ? AND seq = ?`,
11725
+ this.docId,
11726
+ writer,
11727
+ seq
11728
+ );
11729
+ return row ? pendingRow(row) : void 0;
11730
+ }
11731
+ pendingThrough(writer, seq) {
11732
+ return rowsOf(
11733
+ this.sql,
11734
+ `SELECT entry, acked FROM record_log_pending WHERE doc_id = ? AND writer = ? AND seq <= ? ORDER BY seq`,
11735
+ this.docId,
11736
+ writer,
11737
+ seq
11738
+ ).map(pendingRow);
11688
11739
  }
11689
11740
  putPending(writer, entry) {
11690
11741
  const { acked, ...stored } = entry;
11691
11742
  const exists = rowsOf(this.sql, `SELECT 1 FROM record_log_pending WHERE doc_id = ? AND writer = ? AND seq = ?`, this.docId, writer, entry.seq).length > 0;
11692
11743
  if (exists) {
11744
+ this.epochPending?.clear();
11693
11745
  this.markDirty(writer, entry.seq);
11694
11746
  this.sql.exec(
11695
11747
  `UPDATE record_log_pending SET entry = ?, void = ?, acked = ?, e = ? WHERE doc_id = ? AND writer = ? AND seq = ?`,
@@ -11721,6 +11773,8 @@ var SqlRecordLogTables = class {
11721
11773
  entry.void ? 1 : 0,
11722
11774
  acked ?? null
11723
11775
  );
11776
+ const counted = this.epochPending?.get(entry.e);
11777
+ if (!entry.void && counted !== void 0) this.epochPending.set(entry.e, counted + 1);
11724
11778
  }
11725
11779
  this.putValues(writer, entry);
11726
11780
  for (const key of keysOf(entry)) {
@@ -11761,6 +11815,7 @@ var SqlRecordLogTables = class {
11761
11815
  }
11762
11816
  }
11763
11817
  deletePending(writer, seq) {
11818
+ this.epochPending?.clear();
11764
11819
  this.markDirty(writer, seq);
11765
11820
  this.sql.exec(`DELETE FROM record_log_pending WHERE doc_id = ? AND writer = ? AND seq = ?`, this.docId, writer, seq);
11766
11821
  this.sql.exec(`DELETE FROM record_log_pending_keys WHERE doc_id = ? AND writer = ? AND seq = ?`, this.docId, writer, seq);
@@ -11810,13 +11865,30 @@ var SqlRecordLogTables = class {
11810
11865
  );
11811
11866
  }
11812
11867
  pendingInEpoch(e) {
11868
+ const counted = this.epochPending?.get(e);
11869
+ if (counted !== void 0) return counted;
11813
11870
  const [row] = rowsOf(
11814
11871
  this.sql,
11815
11872
  `SELECT COUNT(*) AS n FROM record_log_pending WHERE doc_id = ? AND void = 0 AND e = ?`,
11816
11873
  this.docId,
11817
11874
  e
11818
11875
  );
11819
- return Number(row?.n ?? 0);
11876
+ const n = Number(row?.n ?? 0);
11877
+ this.epochPending?.set(e, n);
11878
+ return n;
11879
+ }
11880
+ /**
11881
+ * Run `fn` counting each epoch's pending entries once: a commit's writes
11882
+ * each check the budget, and inside one transaction only they change the
11883
+ * count. Any other change to the pending log counts again.
11884
+ */
11885
+ countingEpochs(fn) {
11886
+ this.epochPending = /* @__PURE__ */ new Map();
11887
+ try {
11888
+ return fn();
11889
+ } finally {
11890
+ this.epochPending = null;
11891
+ }
11820
11892
  }
11821
11893
  };
11822
11894
  function modelsOf(keys) {
@@ -12441,31 +12513,33 @@ var RecordLogStore = class {
12441
12513
  * epochs this client may write into are full; nothing of the command lands.
12442
12514
  */
12443
12515
  commitEntries(command) {
12444
- return this.sql.transaction(() => {
12445
- const client = this.client(command.writer);
12446
- const results = [];
12447
- const refreshed = [];
12448
- for (const write of command.writes) {
12449
- const stale = (write.expect ?? []).some(
12450
- ({ model, id, revision }) => recordLogRowRevision(this.read(model, id)) !== revision
12451
- );
12452
- if (stale) {
12453
- results.push({ stale: true });
12454
- continue;
12455
- }
12456
- client.budgetCheck(this.docId);
12457
- const [m, ...also] = write.mutations;
12458
- const entry = client.write(m, {
12459
- also,
12460
- ...write.upsertOn !== void 0 ? { upsertOn: write.upsertOn } : {},
12461
- ts: command.ts ?? Date.now()
12462
- });
12463
- refreshed.push(...this.refreshView([...keysOf(entry), ...this.tables.takeDirty()]));
12464
- results.push({ entry });
12465
- }
12466
- const changed = this.settleView([], command.project ?? [], refreshed);
12467
- return { results, changed, cursor: this.tables.cursor() };
12468
- });
12516
+ return this.sql.transaction(
12517
+ () => this.tables.countingEpochs(() => {
12518
+ const client = this.client(command.writer);
12519
+ const results = [];
12520
+ const refreshed = [];
12521
+ for (const write of command.writes) {
12522
+ const stale = (write.expect ?? []).some(
12523
+ ({ model, id, revision }) => recordLogRowRevision(this.read(model, id)) !== revision
12524
+ );
12525
+ if (stale) {
12526
+ results.push({ stale: true });
12527
+ continue;
12528
+ }
12529
+ client.budgetCheck(this.docId);
12530
+ const [m, ...also] = write.mutations;
12531
+ const entry = client.write(m, {
12532
+ also,
12533
+ ...write.upsertOn !== void 0 ? { upsertOn: write.upsertOn } : {},
12534
+ ts: command.ts ?? Date.now()
12535
+ });
12536
+ refreshed.push(...this.refreshView([...keysOf(entry), ...this.tables.takeDirty()]));
12537
+ results.push({ entry });
12538
+ }
12539
+ const changed = this.settleView([], command.project ?? [], refreshed);
12540
+ return { results, changed, cursor: this.tables.cursor() };
12541
+ })
12542
+ );
12469
12543
  }
12470
12544
  /** Apply what the room relayed, item by item. */
12471
12545
  receive(command) {
@@ -16294,20 +16368,23 @@ var RecordLogSender = class {
16294
16368
  constructor(transport, options = {}) {
16295
16369
  this.transport = transport;
16296
16370
  this.tickMs = options.tickMs ?? RECORD_LOG_TICK_MS;
16297
- this.window = options.window ?? RECORD_LOG_SEND_WINDOW;
16298
16371
  this.frameTargetBytes = options.frameTargetBytes ?? RECORD_LOG_FRAME_TARGET_BYTES;
16299
- this.batchMax = options.batchMax ?? RECORD_LOG_UP_BATCH_MAX;
16372
+ this.batchMax = options.batchMax ?? RECORD_LOG_FRAME_ENTRIES_MAX;
16373
+ this.frameWaitMs = options.frameWaitMs ?? RECORD_LOG_FRAME_WAIT_MS;
16300
16374
  this.clock = options.clock ?? platformClock;
16301
16375
  }
16302
16376
  queue = [];
16303
16377
  inFlight = /* @__PURE__ */ new Set();
16378
+ /** The frames sent and not yet answered in full, oldest first: the numbers each still owes. */
16379
+ frames = [];
16304
16380
  timer = null;
16381
+ /** Runs out when the oldest frame has waited {@link frameWaitMs}. */
16382
+ frameTimer = null;
16305
16383
  tickMs;
16306
16384
  frameTargetBytes;
16307
16385
  batchMax;
16386
+ frameWaitMs;
16308
16387
  clock;
16309
- /** How many unacknowledged entries may be in flight; a bulk-saving app may widen it. */
16310
- window;
16311
16388
  /** Entries queued and not yet sent. */
16312
16389
  queued() {
16313
16390
  return this.queue.filter((q) => q.kind === "entry").length;
@@ -16316,6 +16393,10 @@ var RecordLogSender = class {
16316
16393
  inFlightCount() {
16317
16394
  return this.inFlight.size;
16318
16395
  }
16396
+ /** `record.entries` frames that hold the next back: sent, not answered in full, and within their wait. */
16397
+ framesInFlight() {
16398
+ return this.frames.length;
16399
+ }
16319
16400
  /** Queue an entry to send; one already queued or in flight is not queued twice. */
16320
16401
  enqueue(entry) {
16321
16402
  if (this.inFlight.has(entry.seq)) return;
@@ -16336,10 +16417,12 @@ var RecordLogSender = class {
16336
16417
  }
16337
16418
  /**
16338
16419
  * The room is missing entries from `expected` on: every entry in flight
16339
- * from there is sent again, in `seq` order, ahead of what is queued.
16420
+ * from there is sent again, in `seq` order, ahead of what is queued. A
16421
+ * frame no longer owes them; one that owed nothing else is done.
16340
16422
  */
16341
16423
  resendFrom(expected, entries) {
16342
16424
  for (const seq of [...this.inFlight]) if (seq >= expected) this.inFlight.delete(seq);
16425
+ this.forget((seq) => seq >= expected);
16343
16426
  const again = entries.filter((entry) => entry.seq >= expected).filter((entry) => !this.queue.some((q) => q.kind === "entry" && q.entry.seq === entry.seq)).sort((a, b) => a.seq - b.seq).map((entry) => ({ kind: "entry", entry, bytes: recordLogJsonBytes(entry) }));
16344
16427
  this.queue.unshift(...again);
16345
16428
  if (this.queue.length) this.schedule();
@@ -16349,7 +16432,7 @@ var RecordLogSender = class {
16349
16432
  * entry it already judged only when it arrives again. Every entry in
16350
16433
  * flight goes back to the head of the queue in `seq` order, ahead of what
16351
16434
  * was queued after it, in its form among `unjudged`; one no longer there
16352
- * is dropped.
16435
+ * is dropped. No frame is in flight any more.
16353
16436
  */
16354
16437
  requeueInFlight(unjudged) {
16355
16438
  const bySeq = new Map(unjudged.map((entry) => [entry.seq, entry]));
@@ -16360,6 +16443,7 @@ var RecordLogSender = class {
16360
16443
  again.push({ kind: "entry", entry, bytes: recordLogJsonBytes(entry) });
16361
16444
  }
16362
16445
  this.inFlight.clear();
16446
+ this.clearFrames();
16363
16447
  this.queue.unshift(...again);
16364
16448
  if (this.queue.length) this.schedule();
16365
16449
  }
@@ -16368,14 +16452,21 @@ var RecordLogSender = class {
16368
16452
  this.queue.push({ kind: "control", frame });
16369
16453
  this.schedule();
16370
16454
  }
16371
- /** The room has judged `seq`: its slot in the window is free. */
16455
+ /**
16456
+ * The room has judged `seq` (a result, `void`, `duplicate` or a refusal):
16457
+ * its frame no longer owes it, and a frame answered in full lets the next
16458
+ * leave at the next tick.
16459
+ */
16372
16460
  acknowledged(seq) {
16373
- if (this.inFlight.delete(seq) && this.queue.length) this.schedule();
16461
+ const answered = this.inFlight.delete(seq);
16462
+ this.forget((owed) => owed === seq);
16463
+ if (answered && this.queue.length) this.schedule();
16374
16464
  }
16375
16465
  /** Forget everything queued and in flight: the connection that would carry it is gone. */
16376
16466
  reset() {
16377
16467
  this.queue.length = 0;
16378
16468
  this.inFlight.clear();
16469
+ this.clearFrames();
16379
16470
  if (this.timer !== null) this.clock.clearTimeout(this.timer);
16380
16471
  this.timer = null;
16381
16472
  }
@@ -16390,7 +16481,39 @@ var RecordLogSender = class {
16390
16481
  this.flush();
16391
16482
  }, this.tickMs);
16392
16483
  }
16393
- /** Send what the window and the transport allow now. */
16484
+ /** The frames no longer owe the numbers `owed` picks; one that owes nothing is done. */
16485
+ forget(owed) {
16486
+ const oldest = this.frames[0];
16487
+ for (const frame of this.frames) for (const seq of [...frame]) if (owed(seq)) frame.delete(seq);
16488
+ this.frames = this.frames.filter((frame) => frame.size > 0);
16489
+ if (this.frames[0] !== oldest) {
16490
+ this.clearFrameTimer();
16491
+ this.armFrameTimer();
16492
+ }
16493
+ }
16494
+ clearFrames() {
16495
+ this.frames = [];
16496
+ this.clearFrameTimer();
16497
+ }
16498
+ clearFrameTimer() {
16499
+ if (this.frameTimer !== null) this.clock.clearTimeout(this.frameTimer);
16500
+ this.frameTimer = null;
16501
+ }
16502
+ /**
16503
+ * Time the oldest frame. Unanswered at the end of its wait, it stops
16504
+ * holding the next back; its numbers stay in flight, so a `record.gap`
16505
+ * still sends them again and their answers are still taken.
16506
+ */
16507
+ armFrameTimer() {
16508
+ if (this.frameTimer !== null || !this.frames.length) return;
16509
+ this.frameTimer = this.clock.setTimeout(() => {
16510
+ this.frameTimer = null;
16511
+ this.frames.shift();
16512
+ this.armFrameTimer();
16513
+ if (this.queue.length) this.schedule();
16514
+ }, this.frameWaitMs);
16515
+ }
16516
+ /** Send what the frame in flight and the transport allow now. */
16394
16517
  flush() {
16395
16518
  if (!this.transport.connected()) return;
16396
16519
  let batch = [];
@@ -16398,6 +16521,8 @@ var RecordLogSender = class {
16398
16521
  const send = () => {
16399
16522
  if (!batch.length) return;
16400
16523
  this.transport.send({ type: "record.entries", entries: batch });
16524
+ this.frames.push(new Set(batch.map((entry) => entry.seq)));
16525
+ this.armFrameTimer();
16401
16526
  batch = [];
16402
16527
  bytes = 0;
16403
16528
  };
@@ -16409,7 +16534,7 @@ var RecordLogSender = class {
16409
16534
  this.transport.send(head.frame);
16410
16535
  continue;
16411
16536
  }
16412
- if (this.inFlight.size >= this.window) break;
16537
+ if (!batch.length && this.frames.length >= RECORD_LOG_FRAMES_IN_FLIGHT) break;
16413
16538
  const fits = batch.length < this.batchMax && (batch.length === 0 || bytes + head.bytes <= this.frameTargetBytes);
16414
16539
  if (!fits) {
16415
16540
  send();
@@ -16777,6 +16902,7 @@ var RecordLogDocumentSession = class {
16777
16902
  await this.store.refreshMarks();
16778
16903
  if (generation !== this.generation) return;
16779
16904
  this.current = { writer, orphan: false };
16905
+ this.sender.reset();
16780
16906
  await this.sendOpen();
16781
16907
  return;
16782
16908
  }
@@ -17394,11 +17520,13 @@ var RecordLogDocumentSession = class {
17394
17520
  return;
17395
17521
  }
17396
17522
  const writer = this.current.writer;
17397
- const before = await this.store.pendingOf(writer);
17398
- const named = before.find((p) => p.seq === refusal.seq);
17399
17523
  const reply = await this.store.refused(refusal, this.host.projectionSpecs(), writer);
17400
17524
  const sender = this.senderOf(writer);
17401
17525
  const after = new Map((await this.store.pendingOf(writer)).map((p) => [p.seq, p]));
17526
+ if (after.get(refusal.seq)?.void) {
17527
+ await this.store.dispatch({ kind: "settled", writer, seq: refusal.seq });
17528
+ after.delete(refusal.seq);
17529
+ }
17402
17530
  for (const frame of reply.frames) sender?.enqueueControl(frame);
17403
17531
  const handed = reply.handedBack.filter((p) => !p.void);
17404
17532
  for (const p of reply.handedBack) {
@@ -17407,7 +17535,6 @@ var RecordLogDocumentSession = class {
17407
17535
  }
17408
17536
  sender?.acknowledged(refusal.seq);
17409
17537
  this.answered([refusal.seq, ...reply.handedBack.map((p) => p.seq)]);
17410
- if (named?.void) await this.store.dispatch({ kind: "settled", writer, seq: refusal.seq });
17411
17538
  if (!handed.length) {
17412
17539
  Logger.debug(`[record-log] ${this.docId}: refusal of ${refusal.seq} (${refusal.code}) withdrew nothing new`);
17413
17540
  return;
@@ -17513,6 +17640,7 @@ var RecordLogDocumentSession = class {
17513
17640
  if (!current.orphan) {
17514
17641
  this.store.renumbered(renumbered);
17515
17642
  await this.store.refreshMarks();
17643
+ this.sender.reset();
17516
17644
  }
17517
17645
  this.current = { writer, orphan: current.orphan };
17518
17646
  await this.sendOpen();