@rocicorp/zero 1.0.0 → 1.0.1-canary.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 (75) hide show
  1. package/out/analyze-query/src/bin-analyze.js +19 -7
  2. package/out/analyze-query/src/bin-analyze.js.map +1 -1
  3. package/out/zero/package.js +1 -1
  4. package/out/zero/package.js.map +1 -1
  5. package/out/zero-cache/src/config/zero-config.d.ts +6 -0
  6. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  7. package/out/zero-cache/src/config/zero-config.js +12 -0
  8. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  9. package/out/zero-cache/src/server/anonymous-otel-start.d.ts.map +1 -1
  10. package/out/zero-cache/src/server/anonymous-otel-start.js +1 -14
  11. package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
  12. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  13. package/out/zero-cache/src/server/change-streamer.js +2 -2
  14. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  15. package/out/zero-cache/src/services/analyze.js +1 -1
  16. package/out/zero-cache/src/services/change-source/change-source.d.ts +7 -0
  17. package/out/zero-cache/src/services/change-source/change-source.d.ts.map +1 -1
  18. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.d.ts.map +1 -1
  19. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js +1 -1
  20. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
  21. package/out/zero-cache/src/services/change-source/custom/change-source.d.ts.map +1 -1
  22. package/out/zero-cache/src/services/change-source/custom/change-source.js +3 -0
  23. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  24. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +9 -1
  25. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  26. package/out/zero-cache/src/services/change-source/pg/change-source.js +150 -45
  27. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  28. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts +8 -0
  29. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts.map +1 -1
  30. package/out/zero-cache/src/services/change-source/protocol/current/status.d.ts +26 -1
  31. package/out/zero-cache/src/services/change-source/protocol/current/status.d.ts.map +1 -1
  32. package/out/zero-cache/src/services/change-source/protocol/current/status.js +7 -2
  33. package/out/zero-cache/src/services/change-source/protocol/current/status.js.map +1 -1
  34. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts +8 -0
  35. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts.map +1 -1
  36. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
  37. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +10 -2
  38. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  39. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +25 -0
  40. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  41. package/out/zero-cache/src/services/change-streamer/change-streamer.js +8 -1
  42. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  43. package/out/zero-cache/src/services/change-streamer/forwarder.d.ts +2 -0
  44. package/out/zero-cache/src/services/change-streamer/forwarder.d.ts.map +1 -1
  45. package/out/zero-cache/src/services/change-streamer/forwarder.js +3 -0
  46. package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
  47. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +3 -2
  48. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
  49. package/out/zero-cache/src/services/change-streamer/subscriber.js +17 -8
  50. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  51. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts +2 -2
  52. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  53. package/out/zero-cache/src/services/replicator/incremental-sync.js +19 -4
  54. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  55. package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
  56. package/out/zero-cache/src/services/replicator/replicator.js +2 -2
  57. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  58. package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts +12 -0
  59. package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts.map +1 -0
  60. package/out/zero-cache/src/services/replicator/reporter/recorder.js +58 -0
  61. package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -0
  62. package/out/zero-cache/src/services/replicator/reporter/report-schema.d.ts +35 -0
  63. package/out/zero-cache/src/services/replicator/reporter/report-schema.d.ts.map +1 -0
  64. package/out/zero-cache/src/services/replicator/reporter/report-schema.js +20 -0
  65. package/out/zero-cache/src/services/replicator/reporter/report-schema.js.map +1 -0
  66. package/out/zero-cache/src/services/run-ast.js +1 -1
  67. package/out/zero-cache/src/types/pg.d.ts.map +1 -1
  68. package/out/zero-cache/src/types/pg.js +2 -0
  69. package/out/zero-cache/src/types/pg.js.map +1 -1
  70. package/out/zero-client/src/client/version.js +1 -1
  71. package/package.json +1 -1
  72. package/out/analyze-query/src/run-ast.d.ts +0 -22
  73. package/out/analyze-query/src/run-ast.d.ts.map +0 -1
  74. package/out/analyze-query/src/run-ast.js +0 -75
  75. package/out/analyze-query/src/run-ast.js.map +0 -1
@@ -1,8 +1,8 @@
1
+ import { assert } from "../../../../../shared/src/asserts.js";
1
2
  import { deepEqual } from "../../../../../shared/src/json.js";
2
- import { promiseVoid } from "../../../../../shared/src/resolved-promises.js";
3
3
  import { AbortError } from "../../../../../shared/src/abort-error.js";
4
4
  import { sleep } from "../../../../../shared/src/sleep.js";
5
- import { parse } from "../../../../../shared/src/valita.js";
5
+ import { parse, valita_exports } from "../../../../../shared/src/valita.js";
6
6
  import { must } from "../../../../../shared/src/must.js";
7
7
  import { mapValues } from "../../../../../shared/src/objects.js";
8
8
  import { equals, intersection, symmetricDifferences } from "../../../../../shared/src/set-utils.js";
@@ -29,6 +29,7 @@ import { initialSync } from "./initial-sync.js";
29
29
  import { streamBackfill } from "./backfill-stream.js";
30
30
  import { subscribe } from "./logical-replication/stream.js";
31
31
  import postgres from "postgres";
32
+ import { nanoid } from "nanoid";
32
33
  import { PG_ADMIN_SHUTDOWN, PG_OBJECT_IN_USE } from "@drdgvhbh/postgres-error-codes";
33
34
  //#region ../zero-cache/src/services/change-source/pg/change-source.ts
34
35
  /**
@@ -36,7 +37,7 @@ import { PG_ADMIN_SHUTDOWN, PG_OBJECT_IN_USE } from "@drdgvhbh/postgres-error-co
36
37
  * replica, before streaming changes from the corresponding logical replication
37
38
  * stream.
38
39
  */
39
- async function initializePostgresChangeSource(lc, upstreamURI, shard, replicaDbFile, syncOptions, context) {
40
+ async function initializePostgresChangeSource(lc, upstreamURI, shard, replicaDbFile, syncOptions, context, lagReportIntervalMs) {
40
41
  await initReplica(lc, `replica-${shard.appID}-${shard.shardNum}`, replicaDbFile, (log, tx) => initialSync(log, shard, tx, upstreamURI, syncOptions, context));
41
42
  const replica = new Database(lc, replicaDbFile);
42
43
  const subscriptionState = getSubscriptionStateAndContext(new StatementRunner(replica));
@@ -45,7 +46,7 @@ async function initializePostgresChangeSource(lc, upstreamURI, shard, replicaDbF
45
46
  try {
46
47
  return {
47
48
  subscriptionState,
48
- changeSource: new PostgresChangeSource(lc, upstreamURI, shard, await checkAndUpdateUpstream(lc, db, shard, subscriptionState), context)
49
+ changeSource: new PostgresChangeSource(lc, upstreamURI, shard, await checkAndUpdateUpstream(lc, db, shard, subscriptionState), context, lagReportIntervalMs ?? null)
49
50
  };
50
51
  } finally {
51
52
  await db.end();
@@ -83,50 +84,68 @@ var MAX_LOW_PRIORITY_DELAY_MS = 1e3;
83
84
  */
84
85
  var PostgresChangeSource = class {
85
86
  #lc;
87
+ #db;
86
88
  #upstreamUri;
87
89
  #shard;
88
90
  #replica;
89
91
  #context;
90
- constructor(lc, upstreamUri, shard, replica, context) {
92
+ #lagReporter;
93
+ constructor(lc, upstreamUri, shard, replica, context, lagReportIntervalMs) {
91
94
  this.#lc = lc.withContext("component", "change-source");
95
+ this.#db = pgClient(lc, upstreamUri, {
96
+ ["idle_timeout"]: 60,
97
+ connection: { ["application_name"]: "zero-replication-monitor" }
98
+ });
92
99
  this.#upstreamUri = upstreamUri;
93
100
  this.#shard = shard;
94
101
  this.#replica = replica;
95
102
  this.#context = context;
103
+ this.#lagReporter = lagReportIntervalMs ? new LagReporter(lc.withContext("component", "lag-reporter"), shard, this.#db, lagReportIntervalMs) : null;
104
+ }
105
+ startLagReporter() {
106
+ return this.#lagReporter ? this.#lagReporter.initiateLagReport() : null;
96
107
  }
97
108
  async startStream(clientWatermark, backfillRequests = []) {
98
- const db = pgClient(this.#lc, this.#upstreamUri);
99
109
  const { slot } = this.#replica;
100
- let cleanup = promiseVoid;
101
- try {
102
- ({cleanup} = await this.#stopExistingReplicationSlotSubscribers(db, slot));
103
- const config = await getInternalShardConfig(db, this.#shard);
104
- this.#lc.info?.(`starting replication stream@${slot}`);
105
- return await this.#startStream(db, slot, clientWatermark, config, backfillRequests);
106
- } finally {
107
- cleanup.then(() => db.end());
108
- }
110
+ await this.#stopExistingReplicationSlotSubscribers(slot);
111
+ const config = await getInternalShardConfig(this.#db, this.#shard);
112
+ this.#lc.info?.(`starting replication stream@${slot}`);
113
+ return this.#startStream(slot, clientWatermark, config, backfillRequests);
109
114
  }
110
- async #startStream(db, slot, clientWatermark, shardConfig, backfillRequests) {
115
+ async #startStream(slot, clientWatermark, shardConfig, backfillRequests) {
111
116
  const clientStart = majorVersionFromString(clientWatermark) + 1n;
112
- const { messages, acks } = await subscribe(this.#lc, db, slot, [...shardConfig.publications], clientStart);
117
+ const { messages, acks } = await subscribe(this.#lc, this.#db, slot, [...shardConfig.publications], clientStart);
113
118
  const acker = new Acker(acks);
114
119
  const changes = new ChangeStreamMultiplexer(this.#lc, clientWatermark);
115
120
  const backfillManager = new BackfillManager(this.#lc, changes, (req) => streamBackfill(this.#lc, this.#upstreamUri, this.#replica, req));
116
121
  changes.addProducers(messages, backfillManager).addListeners(backfillManager, acker);
117
122
  backfillManager.run(clientWatermark, backfillRequests);
118
- const changeMaker = new ChangeMaker(this.#lc, this.#shard, shardConfig, this.#replica.initialSchema, this.#upstreamUri);
123
+ const changeMaker = new ChangeMaker(this.#lc, this.#shard, shardConfig, this.#db, this.#replica.initialSchema);
124
+ /**
125
+ * Determines if the incoming message is transactional, otherwise handling
126
+ * non-transactional messages with a downstream status message.
127
+ */
128
+ const isTransactionalMessage = (lsn, msg) => {
129
+ if (msg.tag === "keepalive") {
130
+ changes.pushStatus([
131
+ "status",
132
+ { ack: msg.shouldRespond },
133
+ { watermark: majorVersionToString(lsn) }
134
+ ]);
135
+ return false;
136
+ }
137
+ if (msg.tag === "message" && msg.prefix === this.#lagReporter?.messagePrefix) {
138
+ changes.pushStatus(this.#lagReporter.processLagReport(msg));
139
+ return false;
140
+ }
141
+ return true;
142
+ };
119
143
  (async () => {
120
144
  try {
121
145
  let reservation = null;
122
146
  let inTransaction = false;
123
147
  for await (const [lsn, msg] of messages) {
124
- if (msg.tag === "keepalive") {
125
- changes.pushStatus([
126
- "status",
127
- { ack: msg.shouldRespond },
128
- { watermark: majorVersionToString(lsn) }
129
- ]);
148
+ if (!isTransactionalMessage(lsn, msg)) {
130
149
  if (!inTransaction && reservation?.lastWatermark) {
131
150
  changes.release(reservation.lastWatermark);
132
151
  reservation = null;
@@ -170,14 +189,11 @@ var PostgresChangeSource = class {
170
189
  };
171
190
  }
172
191
  async #logCurrentReplicaInfo() {
173
- const db = pgClient(this.#lc, this.#upstreamUri);
174
192
  try {
175
- const replica = await getReplicaAtVersion(this.#lc, db, this.#shard, this.#replica.version);
193
+ const replica = await getReplicaAtVersion(this.#lc, this.#db, this.#shard, this.#replica.version);
176
194
  if (replica) this.#lc.info?.(`Shutdown signal from replica@${this.#replica.version}: ${stringify(replica.subscriberContext)}`);
177
195
  } catch (e) {
178
196
  this.#lc.warn?.(`error logging replica info`, e);
179
- } finally {
180
- await db.end();
181
197
  }
182
198
  }
183
199
  /**
@@ -189,10 +205,10 @@ var PostgresChangeSource = class {
189
205
  * the timestamp suffix) are preserved, as those are newly syncing replicas
190
206
  * that will soon take over the slot.
191
207
  */
192
- async #stopExistingReplicationSlotSubscribers(db, slotToKeep) {
208
+ async #stopExistingReplicationSlotSubscribers(slotToKeep) {
193
209
  const slotExpression = replicationSlotExpression(this.#shard);
194
210
  const legacySlotName = legacyReplicationSlot(this.#shard);
195
- const result = await runTx(db, async (sql) => {
211
+ const result = await runTx(this.#db, async (sql) => {
196
212
  const result = await sql`
197
213
  SELECT slot_name as slot, pg_terminate_backend(active_pid) as terminated, active_pid as pid
198
214
  FROM pg_replication_slots
@@ -230,10 +246,11 @@ var PostgresChangeSource = class {
230
246
  const pids = result.filter(({ pid }) => pid !== null).map(({ pid }) => pid);
231
247
  if (pids.length) this.#lc.info?.(`signaled subscriber ${pids} to shut down`);
232
248
  const otherSlots = result.filter(({ slot }) => slot !== slotToKeep).map(({ slot }) => slot);
233
- return { cleanup: otherSlots.length ? this.#dropReplicationSlots(db, otherSlots) : promiseVoid };
249
+ if (otherSlots.length) this.#dropReplicationSlots(otherSlots).catch((e) => this.#lc.warn?.(`error dropping replication slots`, e));
234
250
  }
235
- async #dropReplicationSlots(sql, slots) {
251
+ async #dropReplicationSlots(slots) {
236
252
  this.#lc.info?.(`dropping other replication slot(s) ${slots}`);
253
+ const sql = this.#db;
237
254
  for (let i = 0; i < 5; i++) try {
238
255
  await sql`
239
256
  SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots
@@ -282,24 +299,112 @@ var Acker = class {
282
299
  this.#acks.push(lsn);
283
300
  }
284
301
  };
302
+ var lagReportSchema = valita_exports.object({
303
+ id: valita_exports.string(),
304
+ sendTimeMs: valita_exports.number(),
305
+ commitTimeMs: valita_exports.number()
306
+ });
307
+ var LagReporter = class LagReporter {
308
+ static MESSAGE_SUFFIX = "/lag-report/v1";
309
+ #lc;
310
+ messagePrefix;
311
+ #db;
312
+ #lagIntervalMs;
313
+ #pgVersion;
314
+ #lastReportID = "";
315
+ #timer;
316
+ constructor(lc, shard, db, lagIntervalMs) {
317
+ this.#lc = lc;
318
+ this.messagePrefix = `${shard.appID}/${shard.shardNum}${LagReporter.MESSAGE_SUFFIX}`;
319
+ this.#db = db;
320
+ this.#lagIntervalMs = lagIntervalMs;
321
+ }
322
+ async #getPgVersion() {
323
+ if (this.#pgVersion === void 0) {
324
+ const [{ pgVersion }] = await this.#db`
325
+ SELECT current_setting('server_version_num')::int as "pgVersion"`;
326
+ this.#pgVersion = pgVersion;
327
+ }
328
+ return this.#pgVersion;
329
+ }
330
+ async initiateLagReport(now = Date.now()) {
331
+ const pgVersion = this.#pgVersion ?? await this.#getPgVersion();
332
+ this.#lastReportID = nanoid();
333
+ if (pgVersion >= 17e4) await this.#db`
334
+ SELECT pg_logical_emit_message(
335
+ false,
336
+ ${this.messagePrefix},
337
+ json_build_object(
338
+ 'id', ${this.#lastReportID}::text,
339
+ 'sendTimeMs', ${now}::int8,
340
+ 'commitTimeMs', extract(epoch from now()) * 1000
341
+ )::text,
342
+ true
343
+ );
344
+ `;
345
+ else await this.#db`
346
+ SELECT pg_logical_emit_message(
347
+ false,
348
+ ${this.messagePrefix},
349
+ json_build_object(
350
+ 'id', ${this.#lastReportID}::text,
351
+ 'sendTimeMs', ${now}::int8,
352
+ 'commitTimeMs', extract(epoch from now()) * 1000
353
+ )::text
354
+ );
355
+ `;
356
+ return { nextSendTimeMs: now };
357
+ }
358
+ #scheduleNextReport(delayMs) {
359
+ clearTimeout(this.#timer);
360
+ this.#timer = setTimeout(async () => {
361
+ try {
362
+ await this.initiateLagReport();
363
+ } catch (e) {
364
+ this.#lc.warn?.(`error initiating lag report`, e);
365
+ this.#scheduleNextReport(this.#lagIntervalMs);
366
+ }
367
+ }, delayMs);
368
+ }
369
+ processLagReport(msg) {
370
+ assert(msg.prefix === this.messagePrefix, `unexpected message prefix: ${msg.prefix}`);
371
+ const report = parseLogicalMessageContent(msg, lagReportSchema);
372
+ const now = Date.now();
373
+ const nextSendTimeMs = Math.max(now, report.sendTimeMs + this.#lagIntervalMs);
374
+ if (report.id === this.#lastReportID) this.#scheduleNextReport(nextSendTimeMs - now);
375
+ const { sendTimeMs, commitTimeMs } = report;
376
+ return [
377
+ "status",
378
+ {
379
+ ack: false,
380
+ lagReport: {
381
+ lastTimings: {
382
+ sendTimeMs,
383
+ commitTimeMs,
384
+ receiveTimeMs: now
385
+ },
386
+ nextSendTimeMs
387
+ }
388
+ },
389
+ { watermark: toStateVersionString(msg.messageLsn ?? "0/0") }
390
+ ];
391
+ }
392
+ };
285
393
  var SET_REPLICA_IDENTITY_DELAY_MS = 50;
286
394
  var ChangeMaker = class {
287
395
  #lc;
288
396
  #shardPrefix;
289
397
  #shardConfig;
290
398
  #initialSchema;
291
- #upstreamDB;
399
+ #db;
292
400
  #replicaIdentityTimer;
293
401
  #error;
294
- constructor(lc, { appID, shardNum }, shardConfig, initialSchema, upstreamURI) {
402
+ constructor(lc, { appID, shardNum }, shardConfig, db, initialSchema) {
295
403
  this.#lc = lc;
296
404
  this.#shardPrefix = `${appID}/${shardNum}`;
297
405
  this.#shardConfig = shardConfig;
298
406
  this.#initialSchema = initialSchema;
299
- this.#upstreamDB = pgClient(lc, upstreamURI, {
300
- ["idle_timeout"]: 10,
301
- connection: { ["application_name"]: "zero-schema-change-detector" }
302
- });
407
+ this.#db = db;
303
408
  }
304
409
  async makeChanges(lsn, msg) {
305
410
  if (this.#error) {
@@ -398,7 +503,7 @@ var ChangeMaker = class {
398
503
  #preSchema;
399
504
  #lastSnapshotInTx;
400
505
  #handleDdlMessage(msg) {
401
- const event = this.#parseReplicationEvent(msg.content);
506
+ const event = parseLogicalMessageContent(msg, replicationEventSchema);
402
507
  clearTimeout(this.#replicaIdentityTimer);
403
508
  let previousSchema;
404
509
  const { type } = event;
@@ -427,7 +532,7 @@ var ChangeMaker = class {
427
532
  const replicaIdentities = replicaIdentitiesForTablesWithoutPrimaryKeys(event.schema);
428
533
  if (replicaIdentities) this.#replicaIdentityTimer = setTimeout(async () => {
429
534
  try {
430
- await replicaIdentities.apply(this.#lc, this.#upstreamDB);
535
+ await replicaIdentities.apply(this.#lc, this.#db);
431
536
  } catch (err) {
432
537
  this.#lc.warn?.(`error setting replica identities`, err);
433
538
  }
@@ -602,10 +707,6 @@ var ChangeMaker = class {
602
707
  }
603
708
  return changes;
604
709
  }
605
- #parseReplicationEvent(content) {
606
- const str = content instanceof Buffer ? content.toString("utf-8") : new TextDecoder().decode(content);
607
- return parse(JSON.parse(str), replicationEventSchema, "passthrough");
608
- }
609
710
  /**
610
711
  * If `ddlDetection === true`, relation messages are irrelevant,
611
712
  * as schema changes are detected by event triggers that
@@ -627,7 +728,7 @@ var ChangeMaker = class {
627
728
  async #handleRelation(rel) {
628
729
  const { publications, ddlDetection } = this.#shardConfig;
629
730
  if (ddlDetection) return [];
630
- const currentSchema = await getPublicationInfo(this.#upstreamDB, publications);
731
+ const currentSchema = await getPublicationInfo(this.#db, publications);
631
732
  const difference = getSchemaDifference(this.#initialSchema, currentSchema);
632
733
  if (difference !== null) throw new MissingEventTriggerSupport(difference);
633
734
  const orel = this.#initialSchema.tables.find((t) => t.oid === rel.relationOid);
@@ -752,6 +853,10 @@ var ShutdownSignal = class extends AbortError {
752
853
  super("shutdown signal received (e.g. another zero-cache taking over the replication stream)", { cause });
753
854
  }
754
855
  };
856
+ function parseLogicalMessageContent({ content }, schema) {
857
+ const str = content instanceof Buffer ? content.toString("utf-8") : new TextDecoder().decode(content);
858
+ return parse(JSON.parse(str), schema, "passthrough");
859
+ }
755
860
  //#endregion
756
861
  export { initializePostgresChangeSource };
757
862