@rocicorp/zero 1.11.0-canary.21 → 1.11.0-canary.23

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 (109) hide show
  1. package/out/zero/package.js +1 -1
  2. package/out/zero/package.js.map +1 -1
  3. package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
  4. package/out/zero-cache/src/config/normalize.js +1 -2
  5. package/out/zero-cache/src/config/normalize.js.map +1 -1
  6. package/out/zero-cache/src/config/zero-config.d.ts +0 -22
  7. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  8. package/out/zero-cache/src/config/zero-config.js +38 -42
  9. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  10. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  11. package/out/zero-cache/src/server/change-streamer.js +90 -73
  12. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  13. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.d.ts.map +1 -1
  14. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js +1 -1
  15. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
  16. package/out/zero-cache/src/services/change-source/common/init-cleanup.d.ts +24 -0
  17. package/out/zero-cache/src/services/change-source/common/init-cleanup.d.ts.map +1 -0
  18. package/out/zero-cache/src/services/change-source/common/init-cleanup.js +44 -0
  19. package/out/zero-cache/src/services/change-source/common/init-cleanup.js.map +1 -0
  20. package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts +49 -1
  21. package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts.map +1 -1
  22. package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -1
  23. package/out/zero-cache/src/services/change-source/custom/change-source.d.ts +1 -1
  24. package/out/zero-cache/src/services/change-source/custom/change-source.d.ts.map +1 -1
  25. package/out/zero-cache/src/services/change-source/custom/change-source.js +7 -3
  26. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  27. package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts +10 -5
  28. package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts.map +1 -1
  29. package/out/zero-cache/src/services/change-source/pg/change-source-init.js +65 -21
  30. package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
  31. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  32. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  33. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +2 -1
  34. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  35. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +3 -1
  36. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  37. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts +31 -0
  38. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts.map +1 -1
  39. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +100 -2
  40. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
  41. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.d.ts +6 -0
  42. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.d.ts.map +1 -1
  43. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js +8 -0
  44. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js.map +1 -1
  45. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts.map +1 -1
  46. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js +3 -16
  47. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
  48. package/out/zero-cache/src/services/change-streamer/change-log-initializer.d.ts +17 -0
  49. package/out/zero-cache/src/services/change-streamer/change-log-initializer.d.ts.map +1 -1
  50. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js +44 -1
  51. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js.map +1 -1
  52. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +7 -0
  53. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
  54. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +27 -8
  55. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  56. package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts +57 -0
  57. package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts.map +1 -1
  58. package/out/zero-cache/src/services/change-streamer/schema/tables.js +103 -23
  59. package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
  60. package/out/zero-cache/src/services/change-streamer/storer.d.ts +86 -1
  61. package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
  62. package/out/zero-cache/src/services/change-streamer/storer.js +272 -77
  63. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  64. package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts +7 -5
  65. package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts.map +1 -1
  66. package/out/zero-cache/src/services/change-streamer/upstream-acker.js +9 -5
  67. package/out/zero-cache/src/services/change-streamer/upstream-acker.js.map +1 -1
  68. package/out/zero-cache/src/services/litestream/commands.d.ts +3 -11
  69. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  70. package/out/zero-cache/src/services/litestream/commands.js +29 -23
  71. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  72. package/out/zero-cache/src/services/litestream/config-v5.yml +0 -7
  73. package/out/zero-cache/src/services/profz.d.ts.map +1 -1
  74. package/out/zero-cache/src/services/profz.js +19 -1
  75. package/out/zero-cache/src/services/profz.js.map +1 -1
  76. package/out/zero-cache/src/services/replicator/replication-status.js +1 -1
  77. package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
  78. package/out/zero-cache/src/services/view-syncer/advancement-metrics.d.ts +26 -0
  79. package/out/zero-cache/src/services/view-syncer/advancement-metrics.d.ts.map +1 -0
  80. package/out/zero-cache/src/services/view-syncer/advancement-metrics.js +26 -0
  81. package/out/zero-cache/src/services/view-syncer/advancement-metrics.js.map +1 -0
  82. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +10 -2
  83. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  84. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +102 -122
  85. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  86. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +8 -9
  87. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
  88. package/out/zero-cache/src/services/view-syncer/snapshotter.js +15 -3
  89. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  90. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  91. package/out/zero-cache/src/services/view-syncer/view-syncer.js +50 -28
  92. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  93. package/out/zero-client/src/client/version.js +1 -1
  94. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  95. package/out/zero-client/src/client/zero.js +2 -1
  96. package/out/zero-client/src/client/zero.js.map +1 -1
  97. package/out/zero-react/src/use-query.js +12 -3
  98. package/out/zero-react/src/use-query.js.map +1 -1
  99. package/out/zql/src/ivm/array-view.d.ts.map +1 -1
  100. package/out/zql/src/ivm/array-view.js +21 -12
  101. package/out/zql/src/ivm/array-view.js.map +1 -1
  102. package/out/zqlite/src/db.d.ts.map +1 -1
  103. package/out/zqlite/src/db.js +34 -6
  104. package/out/zqlite/src/db.js.map +1 -1
  105. package/package.json +1 -1
  106. package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.d.ts +0 -34
  107. package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.d.ts.map +0 -1
  108. package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.js +0 -184
  109. package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.js.map +0 -1
@@ -2,6 +2,7 @@ import { assert, unreachable } from "../../../../shared/src/asserts.js";
2
2
  import { promiseVoid } from "../../../../shared/src/resolved-promises.js";
3
3
  import { AbortError } from "../../../../shared/src/abort-error.js";
4
4
  import { parse, valita_exports } from "../../../../shared/src/valita.js";
5
+ import { must } from "../../../../shared/src/must.js";
5
6
  import { BigIntJSON } from "../../../../shared/src/bigint-json.js";
6
7
  import { cdcSchema } from "../../types/shards.js";
7
8
  import { isDataChange, isSchemaChange } from "../change-source/protocol/current/data.js";
@@ -13,7 +14,7 @@ import "../../types/pg.js";
13
14
  import { READONLY, READ_COMMITTED } from "../../db/mode-enum.js";
14
15
  import { runTx } from "../../db/run-transaction.js";
15
16
  import { Queue } from "../../../../shared/src/queue.js";
16
- import { AutoResetSignal, markResetRequired } from "./schema/tables.js";
17
+ import { AutoResetSignal, markResetRequired, reinitializeChangeLog } from "./schema/tables.js";
17
18
  import { TransactionPool, sharedSnapshot } from "../../db/transaction-pool.js";
18
19
  import { orTimeout } from "../../types/timeout.js";
19
20
  import { reconstructWatermarkedChange, serializeChangeStreamDataWithChange } from "./change-log-codec.js";
@@ -66,6 +67,11 @@ var Storer = class {
66
67
  #replicaVersion;
67
68
  #onCommitted;
68
69
  #onFatal;
70
+ /**
71
+ * Set while waiting to assume ownership of the change log at its head
72
+ * (see {@link assumeOwnershipAtHead}).
73
+ */
74
+ #pendingOwnership = null;
69
75
  #queue = new Queue();
70
76
  #backPressureThresholdBytes;
71
77
  #statementTimeoutMs;
@@ -120,12 +126,128 @@ var Storer = class {
120
126
  }
121
127
  return result;
122
128
  }
123
- async assumeOwnership(purgeLock) {
124
- const db = this.#db;
125
- const owner = this.#taskID;
129
+ #ownerAddress() {
126
130
  const ownerAddress = this.#discoveryAddress;
127
131
  const ownerProtocol = this.#discoveryProtocol;
128
- const addressWithProtocol = ownerProtocol === "ws" ? ownerAddress : `${ownerProtocol}://${ownerAddress}`;
132
+ return ownerProtocol === "ws" ? ownerAddress : `${ownerProtocol}://${ownerAddress}`;
133
+ }
134
+ /**
135
+ * Re-initializes a change log that is no longer maintained from the
136
+ * replica's state, taking ownership of it in the process (which is then
137
+ * assumed again at its new head, see {@link assumeOwnershipAtHead}).
138
+ * See {@link reinitializeChangeLog}.
139
+ */
140
+ reinitializeFromReplica(params) {
141
+ return reinitializeChangeLog(this.#lc, this.#db, this.#shard, {
142
+ ...params,
143
+ owner: this.#taskID,
144
+ ownerAddress: this.#ownerAddress()
145
+ }, this.#pendingOwnership?.purgeLock ?? void 0);
146
+ }
147
+ /**
148
+ * Prepares to assume ownership of the change log at its head, while it may
149
+ * still be owned and maintained by another change-streamer. This allows the
150
+ * previous owner to continue to serve subscribers while this change-source's
151
+ * upstream decodes the WAL to catch up to the head of the change log, which
152
+ * may be many minutes worth of transactions for large DBs.
153
+ *
154
+ * Once the change stream is started from the head of the change log:
155
+ *
156
+ * * Transactions that the change log already contains (i.e. stored by the
157
+ * previous owner) are not stored again, but are otherwise processed
158
+ * normally (e.g. for upstream acks, since they have been stored).
159
+ * * Ownership is assumed in the transaction that stores the first
160
+ * transaction past the head of the change log. The previous owner's
161
+ * (subsequent) transactions fail its ownership check, which makes this
162
+ * transaction the one immediately after the previous owner's last one.
163
+ * * Ownership is also assumed upon a {@link status} at or past the head of
164
+ * the change log (outside of a transaction), which covers an idle
165
+ * upstream: all transactions up to the status position have been
166
+ * received, and any past the head would have been stored.
167
+ *
168
+ * Subscribers are caught up from the change log as usual, as the stream
169
+ * (which is forwarded to them) starts from its head.
170
+ *
171
+ * Backfill transactions are synthesized by each change-streamer's change
172
+ * source, and thus never correspond to those stored by the previous owner.
173
+ * Until ownership is assumed, they are not stored, and no backfills are
174
+ * requested from the change log (see
175
+ * {@link getStartStreamInitializationParameters}). Once ownership is
176
+ * assumed, a restart of the change stream is requested if the change log has
177
+ * pending backfills or if a backfill transaction was skipped.
178
+ */
179
+ assumeOwnershipAtHead(takeover) {
180
+ this.#pendingOwnership = {
181
+ ...takeover,
182
+ head: "",
183
+ skippedBackfill: false,
184
+ start: performance.now()
185
+ };
186
+ }
187
+ /**
188
+ * Whether ownership of the change log is pending, i.e. after
189
+ * {@link assumeOwnershipAtHead} and before ownership is assumed.
190
+ */
191
+ get ownershipPending() {
192
+ return this.#pendingOwnership !== null;
193
+ }
194
+ /**
195
+ * Relays a status message received outside of a transaction, which is
196
+ * used to assume ownership of the change log on an idle upstream. A no-op
197
+ * unless ownership is pending.
198
+ */
199
+ status(watermark) {
200
+ if (this.#pendingOwnership) this.#queue.enqueue(["status", watermark]);
201
+ }
202
+ /**
203
+ * Assumes ownership if the change log contains nothing past `watermark`.
204
+ * Otherwise, updates the known head of the change log.
205
+ *
206
+ * @param ifBefore When set, ownership is only assumed if the head of the
207
+ * change log is before (rather than at or before) the `watermark`,
208
+ * i.e. when the watermark is that of a transaction to be stored.
209
+ * @returns The state of the change log at which ownership is assumed (in
210
+ * `tx`), or `null` if ownership was not assumed.
211
+ */
212
+ async #tryAssumeOwnership(tx, watermark, ifBefore) {
213
+ const pending = must(this.#pendingOwnership);
214
+ const [state] = await tx.processReadTask((sql) => sql`
215
+ SELECT "lastWatermark" FROM ${this.#cdc("replicationState")} FOR UPDATE`);
216
+ const head = state.lastWatermark;
217
+ pending.head = head > pending.head ? head : pending.head;
218
+ if (ifBefore ? head >= watermark : head > watermark) return null;
219
+ const owner = this.#taskID;
220
+ const ownerAddress = this.#ownerAddress();
221
+ tx.process((sql) => [sql`UPDATE ${this.#cdc("replicationState")} SET ${sql({
222
+ owner,
223
+ ownerAddress
224
+ })}`]);
225
+ const [{ backfilling }] = await tx.processReadTask((sql) => sql`
226
+ SELECT EXISTS (SELECT 1 FROM ${this.#cdc("backfilling")}) AS "backfilling"`);
227
+ this.#lc.info?.(`assuming ownership at ${ownerAddress} after change log head ${head}`);
228
+ return {
229
+ head,
230
+ backfilling
231
+ };
232
+ }
233
+ #ownershipAssumed({ head, backfilling }) {
234
+ const { purgeLock, requestRestart, skippedBackfill, start } = must(this.#pendingOwnership);
235
+ this.#pendingOwnership = null;
236
+ this.#lc.info?.(`assumed ownership of the change log at ${head} after ${(performance.now() - start).toFixed(2)} ms`);
237
+ purgeLock?.release();
238
+ if (backfilling || skippedBackfill) requestRestart(`resuming backfills after assuming ownership`);
239
+ }
240
+ /**
241
+ * Assumes ownership of the change log immediately, regardless of its head.
242
+ * This is used for in tests.
243
+ *
244
+ * A production change-streamer instead takes over the change log at its head
245
+ * (see {@link assumeOwnershipAtHead}).
246
+ */
247
+ async assumeOwnership() {
248
+ const db = this.#db;
249
+ const owner = this.#taskID;
250
+ const addressWithProtocol = this.#ownerAddress();
129
251
  this.#lc.info?.(`assuming ownership at ${addressWithProtocol}`);
130
252
  const start = performance.now();
131
253
  await this.#withTimeout("assume-ownership", db`UPDATE ${this.#cdc("replicationState")} SET ${db({
@@ -134,7 +256,6 @@ var Storer = class {
134
256
  })}`);
135
257
  const elapsed = (performance.now() - start).toFixed(2);
136
258
  this.#lc.info?.(`assumed ownership at ${addressWithProtocol} (${elapsed} ms)`);
137
- if (purgeLock) purgeLock.release();
138
259
  }
139
260
  /**
140
261
  * The whole of what a stream connection starts from: where to resume, what to
@@ -180,7 +301,7 @@ var Storer = class {
180
301
  ], { mode: READONLY }));
181
302
  return {
182
303
  lastWatermark,
183
- backfillRequests: parse(result, backfillRequestsSchema),
304
+ backfillRequests: this.#pendingOwnership ? [] : parse(result, backfillRequestsSchema),
184
305
  cookies: {
185
306
  tableMetadata: [...tableMetadata],
186
307
  backfilling: [...backfilling]
@@ -404,88 +525,152 @@ var Storer = class {
404
525
  else await this.#startCatchup([subscriber]);
405
526
  continue;
406
527
  }
528
+ case "status": {
529
+ const pending = this.#pendingOwnership;
530
+ if (pending && !tx && msg[1] >= pending.head) {
531
+ const pool = new TransactionPool(this.#lc, { mode: READ_COMMITTED });
532
+ pool.run(this.#inserter);
533
+ const assumed = await this.#tryAssumeOwnership(pool, msg[1], false);
534
+ if (assumed === null) {
535
+ pool.abort();
536
+ await pool.done();
537
+ } else {
538
+ pool.setDone();
539
+ await pool.done();
540
+ this.#ownershipAssumed(assumed);
541
+ }
542
+ }
543
+ continue;
544
+ }
407
545
  case "abort":
408
- if (tx) {
546
+ if (tx && !tx.skipped) {
409
547
  tx.pool.abort();
410
548
  await tx.pool.done();
411
- tx = null;
412
549
  }
550
+ tx = null;
413
551
  continue;
414
552
  }
415
- const [_, watermark, json, storedChange, change] = msg;
416
- const tag = change?.tag;
417
- this.#approximateQueuedBytes -= json.length;
418
- if (tag === "begin") {
419
- assert(!tx, "received BEGIN in the middle of a transaction");
420
- const { promise, resolve, reject } = resolver();
421
- promise.catch(() => {});
422
- tx = {
423
- pool: new TransactionPool(this.#lc.withContext("watermark", watermark), { mode: READ_COMMITTED }),
424
- preCommitWatermark: watermark,
425
- pos: 0,
426
- startingReplicationState: promise,
427
- ack: !change.skipAck,
428
- batch: [],
429
- lastFlush: void 0
430
- };
431
- tx.pool.run(this.#inserter);
432
- tx.pool.process((tx) => {
433
- tx`
434
- SELECT "owner" FROM ${this.#cdc("replicationState")} FOR UPDATE`.then(([result]) => resolve(result), reject);
435
- return [];
436
- });
437
- } else {
438
- assert(tx, () => `received change outside of transaction: ${json}`);
439
- tx.pos++;
440
- }
441
- const entry = {
442
- watermark: tag === "commit" ? watermark : tx.preCommitWatermark,
443
- precommit: tag === "commit" ? tx.preCommitWatermark : null,
444
- pos: tx.pos,
445
- change: storedChange
446
- };
447
- if (change !== null && isSchemaChange(change)) {
448
- await this.#flushChangeLog(tx);
449
- tx.lastFlush = tx.pool.process((sql) => [sql`INSERT INTO ${this.#cdc("changeLog")} ${sql(entry)}`, ...this.#trackBackfillMetadata(sql, change)]);
450
- } else {
451
- tx.batch.push(entry);
452
- if (tx.batch.length >= this.#changeLogBatchSize) {
453
- const prevFlush = tx.lastFlush;
454
- this.#flushChangeLog(tx);
455
- await prevFlush;
456
- }
457
- }
553
+ this.#approximateQueuedBytes -= msg[2].length;
554
+ tx = await this.#processChange(tx, msg);
458
555
  this.#maybeReleaseBackPressure();
459
- if (tag === "commit") {
460
- this.#flushChangeLog(tx);
461
- const { owner } = await tx.startingReplicationState;
462
- if (owner !== this.#taskID) tx.pool.fail(new AbortError(`changeLog ownership has been assumed by ${owner}`));
463
- else {
464
- const lastWatermark = watermark;
465
- tx.pool.process((tx) => [tx`
466
- UPDATE ${this.#cdc("replicationState")} SET ${tx({ lastWatermark })}`]);
467
- tx.pool.setDone();
468
- }
469
- await tx.pool.done();
470
- if (tx.ack) this.#onCommitted([
471
- "commit",
472
- change,
473
- { watermark }
474
- ]);
475
- tx = null;
476
- await this.#startCatchup(catchupQueue.splice(0));
477
- } else if (tag === "rollback") {
478
- tx.pool.abort();
479
- await tx.pool.done();
480
- tx = null;
481
- await this.#startCatchup(catchupQueue.splice(0));
482
- }
556
+ if (tx === null) await this.#startCatchup(catchupQueue.splice(0));
483
557
  }
558
+ this.#cancelQueueEntries(catchupQueue.splice(0).map((sub) => ["subscriber", sub]), void 0);
484
559
  } catch (e) {
485
560
  catchupQueue.forEach(({ subscriber }) => subscriber.fail(e));
486
561
  throw e;
487
562
  }
488
563
  }
564
+ /**
565
+ * Processes a change from the queue, i.e. stores it in the change log as
566
+ * part of the current transaction (or skips it, see
567
+ * {@link assumeOwnershipAtHead}).
568
+ *
569
+ * @returns The transaction that is open after the change, or `null` if the
570
+ * change ended (or was outside of) a transaction.
571
+ */
572
+ async #processChange(tx, msg) {
573
+ const [_, watermark, json, storedChange, change] = msg;
574
+ const tag = change?.tag;
575
+ if (tag === "begin") {
576
+ assert(!tx, "received BEGIN in the middle of a transaction");
577
+ const pending = this.#pendingOwnership;
578
+ if (pending) {
579
+ if (change.skipAck) {
580
+ pending.skippedBackfill = true;
581
+ return {
582
+ skipped: true,
583
+ ack: false
584
+ };
585
+ }
586
+ if (watermark <= pending.head) return {
587
+ skipped: true,
588
+ ack: true
589
+ };
590
+ }
591
+ const pool = new TransactionPool(this.#lc.withContext("watermark", watermark), { mode: READ_COMMITTED });
592
+ pool.run(this.#inserter);
593
+ const { promise, resolve, reject } = resolver();
594
+ promise.catch(() => {});
595
+ const assumed = pending ? await this.#tryAssumeOwnership(pool, watermark, true) : void 0;
596
+ if (assumed === null) {
597
+ pool.abort();
598
+ await pool.done();
599
+ return {
600
+ skipped: true,
601
+ ack: true
602
+ };
603
+ }
604
+ if (pending) resolve({ owner: this.#taskID });
605
+ else pool.process((tx) => {
606
+ tx`
607
+ SELECT "owner" FROM ${this.#cdc("replicationState")} FOR UPDATE`.then(([result]) => resolve(result), reject);
608
+ return [];
609
+ });
610
+ tx = {
611
+ pool,
612
+ preCommitWatermark: watermark,
613
+ pos: 0,
614
+ startingReplicationState: promise,
615
+ ack: !change.skipAck,
616
+ batch: [],
617
+ lastFlush: void 0,
618
+ ownershipAssumed: assumed
619
+ };
620
+ } else if (tx?.skipped) {
621
+ if (tag === "commit" && tx.ack) this.#onCommitted([
622
+ "commit",
623
+ change,
624
+ { watermark }
625
+ ]);
626
+ return tag === "commit" || tag === "rollback" ? null : tx;
627
+ } else {
628
+ assert(tx, () => `received change outside of transaction: ${json}`);
629
+ tx.pos++;
630
+ }
631
+ const entry = {
632
+ watermark: tag === "commit" ? watermark : tx.preCommitWatermark,
633
+ precommit: tag === "commit" ? tx.preCommitWatermark : null,
634
+ pos: tx.pos,
635
+ change: storedChange
636
+ };
637
+ if (change !== null && isSchemaChange(change)) {
638
+ await this.#flushChangeLog(tx);
639
+ tx.lastFlush = tx.pool.process((sql) => [sql`INSERT INTO ${this.#cdc("changeLog")} ${sql(entry)}`, ...this.#trackBackfillMetadata(sql, change)]);
640
+ } else {
641
+ tx.batch.push(entry);
642
+ if (tx.batch.length >= this.#changeLogBatchSize) {
643
+ const prevFlush = tx.lastFlush;
644
+ this.#flushChangeLog(tx);
645
+ await prevFlush;
646
+ }
647
+ }
648
+ if (tag === "commit") {
649
+ this.#flushChangeLog(tx);
650
+ const { owner } = await tx.startingReplicationState;
651
+ if (owner !== this.#taskID) tx.pool.fail(new AbortError(`changeLog ownership has been assumed by ${owner}`));
652
+ else {
653
+ const lastWatermark = watermark;
654
+ tx.pool.process((tx) => [tx`
655
+ UPDATE ${this.#cdc("replicationState")} SET ${tx({ lastWatermark })}`]);
656
+ tx.pool.setDone();
657
+ }
658
+ await tx.pool.done();
659
+ const { ownershipAssumed, ack } = tx;
660
+ tx = null;
661
+ if (ownershipAssumed) this.#ownershipAssumed(ownershipAssumed);
662
+ if (ack) this.#onCommitted([
663
+ "commit",
664
+ change,
665
+ { watermark }
666
+ ]);
667
+ } else if (tag === "rollback") {
668
+ tx.pool.abort();
669
+ await tx.pool.done();
670
+ tx = null;
671
+ }
672
+ return tx;
673
+ }
489
674
  async #startCatchup(subs) {
490
675
  const numCatchups = subs.length;
491
676
  if (numCatchups === 0) return;
@@ -720,8 +905,18 @@ var PurgeLocker = class {
720
905
  #cdc(table) {
721
906
  return this.#db(`${cdcSchema(this.#shard)}.${table}`);
722
907
  }
723
- async acquire() {
908
+ async acquire(slotWatermark) {
724
909
  const tx = new TransactionPool(this.#lc, { mode: READ_COMMITTED }).run(this.#db);
910
+ if (slotWatermark !== void 0) {
911
+ const [{ lastWatermark }] = await tx.processReadTask((sql) => sql`
912
+ SELECT "lastWatermark" FROM ${this.#cdc("replicationState")}`);
913
+ if (lastWatermark < slotWatermark) {
914
+ this.#lc.info?.(`changeLog@${lastWatermark} is behind the replication slot@${slotWatermark}`);
915
+ tx.setDone();
916
+ await tx.done();
917
+ return "behind-slot";
918
+ }
919
+ }
725
920
  const row = await tx.processReadTask((sql) => sql`
726
921
  SELECT watermark FROM ${this.#cdc("changeLog")}
727
922
  ORDER BY watermark, pos LIMIT 1