@rocicorp/zero 1.9.0-head-ef892a12-20260713 → 1.9.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 (163) hide show
  1. package/out/z2s/src/compiler.d.ts.map +1 -1
  2. package/out/z2s/src/compiler.js +1 -22
  3. package/out/z2s/src/compiler.js.map +1 -1
  4. package/out/zero/package.js +3 -20
  5. package/out/zero/package.js.map +1 -1
  6. package/out/zero-cache/src/config/zero-config.d.ts +9 -0
  7. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  8. package/out/zero-cache/src/config/zero-config.js +32 -2
  9. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  10. package/out/zero-cache/src/custom/fetch.js +1 -1
  11. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  12. package/out/zero-cache/src/db/delete-lite-db.js +2 -1
  13. package/out/zero-cache/src/db/delete-lite-db.js.map +1 -1
  14. package/out/zero-cache/src/db/migration-lite.d.ts.map +1 -1
  15. package/out/zero-cache/src/db/migration-lite.js +5 -2
  16. package/out/zero-cache/src/db/migration-lite.js.map +1 -1
  17. package/out/zero-cache/src/db/migration.d.ts.map +1 -1
  18. package/out/zero-cache/src/db/migration.js +3 -1
  19. package/out/zero-cache/src/db/migration.js.map +1 -1
  20. package/out/zero-cache/src/db/sqlite-corruption.d.ts +12 -0
  21. package/out/zero-cache/src/db/sqlite-corruption.d.ts.map +1 -0
  22. package/out/zero-cache/src/db/sqlite-corruption.js +196 -0
  23. package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -0
  24. package/out/zero-cache/src/observability/metrics.d.ts +1 -1
  25. package/out/zero-cache/src/observability/metrics.d.ts.map +1 -1
  26. package/out/zero-cache/src/observability/metrics.js +5 -1
  27. package/out/zero-cache/src/observability/metrics.js.map +1 -1
  28. package/out/zero-cache/src/server/backup-watermark-reader.js +1 -1
  29. package/out/zero-cache/src/server/backup-watermark-reader.js.map +1 -1
  30. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  31. package/out/zero-cache/src/server/change-streamer.js +10 -5
  32. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  33. package/out/zero-cache/src/server/logging.d.ts +8 -0
  34. package/out/zero-cache/src/server/logging.d.ts.map +1 -1
  35. package/out/zero-cache/src/server/logging.js +27 -5
  36. package/out/zero-cache/src/server/logging.js.map +1 -1
  37. package/out/zero-cache/src/server/main.d.ts.map +1 -1
  38. package/out/zero-cache/src/server/main.js +6 -1
  39. package/out/zero-cache/src/server/main.js.map +1 -1
  40. package/out/zero-cache/src/server/mutator.js +1 -1
  41. package/out/zero-cache/src/server/mutator.js.map +1 -1
  42. package/out/zero-cache/src/server/otel-log-sink.d.ts +7 -1
  43. package/out/zero-cache/src/server/otel-log-sink.d.ts.map +1 -1
  44. package/out/zero-cache/src/server/otel-log-sink.js +7 -2
  45. package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
  46. package/out/zero-cache/src/server/reaper.js +1 -1
  47. package/out/zero-cache/src/server/reaper.js.map +1 -1
  48. package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
  49. package/out/zero-cache/src/server/replicator.js +11 -1
  50. package/out/zero-cache/src/server/replicator.js.map +1 -1
  51. package/out/zero-cache/src/server/shadow-syncer.js +1 -1
  52. package/out/zero-cache/src/server/shadow-syncer.js.map +1 -1
  53. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  54. package/out/zero-cache/src/server/syncer.js +6 -1
  55. package/out/zero-cache/src/server/syncer.js.map +1 -1
  56. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
  57. package/out/zero-cache/src/services/change-source/common/replica-schema.js +8 -5
  58. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  59. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +1 -1
  60. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  61. package/out/zero-cache/src/services/change-source/pg/change-source.js +20 -10
  62. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  63. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +15 -0
  64. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  65. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +41 -10
  66. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  67. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts +65 -1
  68. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts.map +1 -1
  69. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +88 -8
  70. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
  71. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +60 -93
  72. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
  73. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +15 -11
  74. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  75. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts +1 -0
  76. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts.map +1 -1
  77. package/out/zero-cache/src/services/change-source/protocol/current/data.js +4 -1
  78. package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
  79. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts +3 -0
  80. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts.map +1 -1
  81. package/out/zero-cache/src/services/change-source/protocol/current/status.d.ts +1 -0
  82. package/out/zero-cache/src/services/change-source/protocol/current/status.d.ts.map +1 -1
  83. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts +1 -0
  84. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts.map +1 -1
  85. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +4 -4
  86. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  87. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +1 -0
  88. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  89. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  90. package/out/zero-cache/src/services/change-streamer/storer.d.ts +7 -1
  91. package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
  92. package/out/zero-cache/src/services/change-streamer/storer.js +24 -5
  93. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  94. package/out/zero-cache/src/services/life-cycle.d.ts +1 -1
  95. package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
  96. package/out/zero-cache/src/services/life-cycle.js +16 -4
  97. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  98. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  99. package/out/zero-cache/src/services/litestream/commands.js +91 -25
  100. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  101. package/out/zero-cache/src/services/litestream/config.yml +3 -2
  102. package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
  103. package/out/zero-cache/src/services/mutagen/mutagen.js +4 -1
  104. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  105. package/out/zero-cache/src/services/replicator/change-processor.d.ts +6 -0
  106. package/out/zero-cache/src/services/replicator/change-processor.d.ts.map +1 -1
  107. package/out/zero-cache/src/services/replicator/change-processor.js +54 -23
  108. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  109. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  110. package/out/zero-cache/src/services/replicator/incremental-sync.js +8 -2
  111. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  112. package/out/zero-cache/src/services/replicator/notifier.d.ts +0 -22
  113. package/out/zero-cache/src/services/replicator/notifier.d.ts.map +1 -1
  114. package/out/zero-cache/src/services/replicator/notifier.js +7 -1
  115. package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
  116. package/out/zero-cache/src/services/replicator/replicator.d.ts +16 -1
  117. package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
  118. package/out/zero-cache/src/services/replicator/replicator.js +7 -1
  119. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  120. package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts +27 -0
  121. package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts.map +1 -1
  122. package/out/zero-cache/src/services/replicator/reporter/recorder.js +32 -0
  123. package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
  124. package/out/zero-cache/src/services/replicator/reporter/report-schema.d.ts +1 -1
  125. package/out/zero-cache/src/services/replicator/write-worker.js +46 -7
  126. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  127. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.d.ts +44 -0
  128. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.d.ts.map +1 -0
  129. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js +59 -0
  130. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js.map +1 -0
  131. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
  132. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  133. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  134. package/out/zero-cache/src/services/view-syncer/view-syncer.js +18 -1
  135. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  136. package/out/zero-cache/src/types/streams.d.ts +11 -0
  137. package/out/zero-cache/src/types/streams.d.ts.map +1 -1
  138. package/out/zero-cache/src/types/streams.js.map +1 -1
  139. package/out/zero-cache/src/types/subscription.d.ts +11 -0
  140. package/out/zero-cache/src/types/subscription.d.ts.map +1 -1
  141. package/out/zero-cache/src/types/subscription.js +27 -0
  142. package/out/zero-cache/src/types/subscription.js.map +1 -1
  143. package/out/zero-client/src/client/version.js +1 -1
  144. package/out/zero-client/src/client/zero.d.ts +6 -7
  145. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  146. package/out/zero-client/src/client/zero.js +156 -101
  147. package/out/zero-client/src/client/zero.js.map +1 -1
  148. package/out/zero-server/src/custom.js +10 -3
  149. package/out/zero-server/src/custom.js.map +1 -1
  150. package/out/zero-server/src/schema.d.ts +8 -0
  151. package/out/zero-server/src/schema.d.ts.map +1 -1
  152. package/out/zero-server/src/schema.js +63 -24
  153. package/out/zero-server/src/schema.js.map +1 -1
  154. package/out/zql/src/mutate/crud.d.ts +10 -8
  155. package/out/zql/src/mutate/crud.d.ts.map +1 -1
  156. package/out/zql/src/mutate/crud.js.map +1 -1
  157. package/out/zql/src/query/query-delegate-base.d.ts.map +1 -1
  158. package/out/zql/src/query/query-delegate-base.js +5 -1
  159. package/out/zql/src/query/query-delegate-base.js.map +1 -1
  160. package/out/zqlite/src/resolve-scalar-subqueries.d.ts.map +1 -1
  161. package/out/zqlite/src/resolve-scalar-subqueries.js +13 -1
  162. package/out/zqlite/src/resolve-scalar-subqueries.js.map +1 -1
  163. package/package.json +3 -34
@@ -12,7 +12,7 @@ import postgres from "postgres";
12
12
  import { PG_ADMIN_SHUTDOWN, PG_OBJECT_IN_USE, PG_OBJECT_NOT_IN_PREREQUISITE_STATE } from "@drdgvhbh/postgres-error-codes";
13
13
  //#region ../zero-cache/src/services/change-source/pg/logical-replication/stream.ts
14
14
  var DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE = 5;
15
- async function subscribe(lc, db, slot, publications, lsn, retriesIfReplicationSlotActive = DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE, applicationName = "zero-replicator") {
15
+ async function subscribe(lc, db, slot, publications, lsn, retriesIfReplicationSlotActive = DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE, applicationName = "zero-replicator", inboundTimeoutOverrideMs) {
16
16
  const session = postgres(defu({
17
17
  max: 1,
18
18
  ["fetch_types"]: false,
@@ -26,28 +26,33 @@ async function subscribe(lc, db, slot, publications, lsn, retriesIfReplicationSl
26
26
  const [{ walSenderTimeoutMs }] = await session`SELECT EXTRACT(EPOCH FROM (setting || unit)::interval) * 1000
27
27
  AS "walSenderTimeoutMs" FROM pg_settings
28
28
  WHERE name = 'wal_sender_timeout'`.simple();
29
- const manualKeepaliveTimeout = Math.floor(walSenderTimeoutMs * .75);
30
- lc.info?.(`wal_sender_timeout: ${walSenderTimeoutMs}ms. Ensuring manual keepalives at least every ${manualKeepaliveTimeout}ms`);
29
+ const { enabled: livenessEnabled, manualKeepaliveTimeout, inboundTimeoutMs, timerIntervalMs } = computeLivenessTimings(walSenderTimeoutMs, inboundTimeoutOverrideMs);
30
+ lc.info?.(livenessEnabled ? `wal_sender_timeout: ${walSenderTimeoutMs}ms. Ensuring manual keepalives at least every ${manualKeepaliveTimeout}ms. Inbound timeout: ${inboundTimeoutMs}ms` + (inboundTimeoutMs === walSenderTimeoutMs * 2 ? "" : " (configured override)") : "wal_sender_timeout is disabled (0); skipping manual keepalives and inbound liveness detection.");
31
31
  const [readable, writable] = await startReplicationStream(lc, session, slot, publications, lsn, retriesIfReplicationSlotActive + 1);
32
32
  let lastAckTime = Date.now();
33
33
  function sendAck(lsn) {
34
34
  writable.write(makeAck(lsn));
35
35
  lastAckTime = Date.now();
36
36
  }
37
- const inboundTimeoutMs = walSenderTimeoutMs * 2;
38
37
  let lastReceivedTime = Date.now();
39
- const livenessTimer = setInterval(() => {
38
+ const livenessTimer = livenessEnabled ? setInterval(() => {
40
39
  const now = Date.now();
41
40
  if (now - lastAckTime > manualKeepaliveTimeout) {
42
41
  sendAck(0n);
43
42
  lc.debug?.(`sent manual keepalive`);
44
43
  }
45
44
  const sinceLastReceived = now - lastReceivedTime;
46
- if (sinceLastReceived > inboundTimeoutMs && !readable.destroyed) {
47
- lc.warn?.(`no message received from ${db.options.host} in ${sinceLastReceived}ms (> 2x wal_sender_timeout ${walSenderTimeoutMs}ms). Destroying replication stream to force reconnect.`);
45
+ const { resetWindow, timedOut } = evaluateInboundLiveness({
46
+ sinceLastReceived,
47
+ inboundTimeoutMs,
48
+ queued: messages.queued
49
+ });
50
+ if (resetWindow) lastReceivedTime = now;
51
+ else if (timedOut && !readable.destroyed) {
52
+ lc.warn?.(`no message received from ${db.options.host} in ${sinceLastReceived}ms (> inbound timeout ${inboundTimeoutMs}ms). Destroying replication stream to force reconnect.`);
48
53
  readable.destroy(/* @__PURE__ */ new Error(`replication stream inbound timeout after ${sinceLastReceived}ms`));
49
54
  }
50
- }, manualKeepaliveTimeout / 5);
55
+ }, timerIntervalMs) : void 0;
51
56
  let destroyed = false;
52
57
  const parser = new PgoutputParser(await getTypeParsers(db, { returnJsonAsString: true }));
53
58
  const messages = Subscription.create({ cleanup: () => {
@@ -73,6 +78,81 @@ async function subscribe(lc, db, slot, publications, lsn, retriesIfReplicationSl
73
78
  };
74
79
  }
75
80
  /**
81
+ * Derives the timings for the replication liveness timer from the server's
82
+ * `wal_sender_timeout` (in milliseconds).
83
+ *
84
+ * Postgres treats `wal_sender_timeout = 0` as "disabled": it will neither
85
+ * terminate an idle wal sender nor emit the periodic keepalives it otherwise
86
+ * sends at roughly `wal_sender_timeout / 2`. Every timing below is derived from
87
+ * that interval, so a disabled timeout disables the liveness machinery entirely
88
+ * (`enabled: false`). Otherwise the derived thresholds would all collapse to 0,
89
+ * inverting the intended meaning: the inbound watchdog would fire on the very
90
+ * first tick and destroy the stream, while the timer itself would busy-spin at
91
+ * a 0ms interval — together producing a continuous reconnect storm.
92
+ *
93
+ * An `inboundTimeoutOverrideMs` (when positive) replaces the default
94
+ * `2x wal_sender_timeout` inbound threshold. The two timeouts protect
95
+ * different parties with asymmetric false-positive costs: `wal_sender_timeout`
96
+ * is the server reaping dead clients (a false positive there costs a cheap
97
+ * reconnect), while the inbound watchdog tears down the stream and replays the
98
+ * in-flight transaction — expensive, and self-defeating when the wal sender is
99
+ * legitimately silent for long stretches (e.g. decoding through WAL from
100
+ * unpublished tables, or assembling a large transaction that pgoutput only
101
+ * sends at commit). Operators who don't own the server setting (e.g. managed
102
+ * or operator-provisioned Postgres with an aggressive cluster-wide
103
+ * `wal_sender_timeout`) can widen the client-side fuse independently.
104
+ *
105
+ * The override has no effect when `wal_sender_timeout` is disabled (0): the
106
+ * server then sends no periodic keepalives, so inbound silence is normal even
107
+ * on an idle healthy connection and any silence-based watchdog would
108
+ * false-positive.
109
+ *
110
+ * https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT
111
+ */
112
+ function computeLivenessTimings(walSenderTimeoutMs, inboundTimeoutOverrideMs) {
113
+ if (!(walSenderTimeoutMs > 0)) return {
114
+ enabled: false,
115
+ manualKeepaliveTimeout: 0,
116
+ inboundTimeoutMs: 0,
117
+ timerIntervalMs: 0
118
+ };
119
+ const manualKeepaliveTimeout = Math.floor(walSenderTimeoutMs * .75);
120
+ return {
121
+ enabled: true,
122
+ manualKeepaliveTimeout,
123
+ inboundTimeoutMs: inboundTimeoutOverrideMs !== void 0 && inboundTimeoutOverrideMs > 0 ? inboundTimeoutOverrideMs : walSenderTimeoutMs * 2,
124
+ timerIntervalMs: manualKeepaliveTimeout / 5
125
+ };
126
+ }
127
+ /**
128
+ * Pure decision for the inbound-liveness watchdog, given the time since the
129
+ * last inbound message, the timeout threshold, and the number of messages
130
+ * currently queued downstream (see {@link SourceWithPendingQueue.queued}).
131
+ *
132
+ * A non-empty downstream queue means we have received data we haven't yet
133
+ * consumed: the reason we aren't receiving *more* is our own back-pressure, not
134
+ * upstream silence. In that case we `resetWindow` (treat the backlog as
135
+ * liveness evidence) rather than counting the stall against the connection.
136
+ * This both suppresses false timeouts during back-pressure and — crucially —
137
+ * keeps `lastReceivedTime` fresh so the drain transition (queue emptying after
138
+ * a long stall) doesn't fire the watchdog against a stale timestamp on the very
139
+ * next tick.
140
+ *
141
+ * Only when the queue is empty (consumption has caught up, so reading — and
142
+ * thus keepalives — should be flowing again) does an over-threshold gap
143
+ * indicate a genuinely dead inbound half.
144
+ */
145
+ function evaluateInboundLiveness({ sinceLastReceived, inboundTimeoutMs, queued }) {
146
+ if (queued > 0) return {
147
+ resetWindow: true,
148
+ timedOut: false
149
+ };
150
+ return {
151
+ resetWindow: false,
152
+ timedOut: sinceLastReceived > inboundTimeoutMs
153
+ };
154
+ }
155
+ /**
76
156
  * Formats publication names for the START_REPLICATION command.
77
157
  * The replication protocol expects format: publication_names 'pub1,pub2'
78
158
  * Each name is escaped to handle any quotes that may have passed validation.
@@ -1 +1 @@
1
- {"version":3,"file":"stream.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/logical-replication/stream.ts"],"sourcesContent":["import {\n PG_ADMIN_SHUTDOWN,\n PG_OBJECT_IN_USE,\n PG_OBJECT_NOT_IN_PREREQUISITE_STATE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {defu} from 'defu';\nimport postgres, {type Options, type PostgresType} from 'postgres';\nimport {sleep} from '../../../../../../shared/src/sleep.ts';\nimport {getTypeParsers} from '../../../../db/pg-type-parser.ts';\nimport {type PostgresDB} from '../../../../types/pg.ts';\nimport {id, lit} from '../../../../types/sql.ts';\nimport {pipe, type Sink, type Source} from '../../../../types/streams.ts';\nimport {Subscription} from '../../../../types/subscription.ts';\nimport {AutoResetSignal} from '../../../change-streamer/schema/tables.ts';\nimport {fromBigInt} from '../lsn.ts';\nimport {PgoutputParser} from './pgoutput-parser.ts';\nimport type {Message} from './pgoutput.types.ts';\n\nconst DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE = 5;\n\nexport type StreamMessage = [\n lsn: bigint,\n Message | {tag: 'keepalive'; shouldRespond: boolean},\n];\n\n// Expose the `queued` variable of the Subscription to allow\n// the change-source to determine if there are more messages\n// immediately available.\ntype SourceWithPendingQueue<T> = Source<T> & {\n queued: number;\n};\n\nexport async function subscribe(\n lc: LogContext,\n db: PostgresDB,\n slot: string,\n publications: string[],\n lsn: bigint,\n retriesIfReplicationSlotActive = DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE,\n applicationName = 'zero-replicator',\n): Promise<{\n messages: SourceWithPendingQueue<StreamMessage>;\n acks: Sink<bigint>;\n}> {\n const session = postgres(\n defu(\n {\n max: 1,\n ['fetch_types']: false, // Necessary for the streaming protocol\n ['idle_timeout']: null,\n ['max_lifetime']: null as unknown as number,\n connection: {\n ['application_name']: applicationName,\n replication: 'database', // https://www.postgresql.org/docs/current/protocol-replication.html\n },\n },\n // ParsedOptions are technically compatible with Options, but happen\n // to not be typed that way. The postgres.js author does an equivalent\n // merge of ParsedOptions and Options here:\n // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/subscribe.js#L13\n db.options as unknown as Options<Record<string, PostgresType>>,\n ),\n );\n\n // Postgres will send keepalives before timing out a wal_sender. It is possible that\n // these keepalives are not received if there is back-pressure in the replication\n // stream. To keep the connection alive, explicitly send keepalives if none have been\n // sent within the last 75% of the wal_sender_timeout.\n //\n // https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT\n const [{walSenderTimeoutMs}] = await session<\n {walSenderTimeoutMs: number}[]\n >`SELECT EXTRACT(EPOCH FROM (setting || unit)::interval) * 1000 \n AS \"walSenderTimeoutMs\" FROM pg_settings\n WHERE name = 'wal_sender_timeout'`.simple();\n const manualKeepaliveTimeout = Math.floor(walSenderTimeoutMs * 0.75);\n lc.info?.(\n `wal_sender_timeout: ${walSenderTimeoutMs}ms. ` +\n `Ensuring manual keepalives at least every ${manualKeepaliveTimeout}ms`,\n );\n\n const [readable, writable] = await startReplicationStream(\n lc,\n session,\n slot,\n publications,\n lsn,\n retriesIfReplicationSlotActive + 1,\n );\n\n let lastAckTime = Date.now();\n function sendAck(lsn: bigint) {\n writable.write(makeAck(lsn));\n lastAckTime = Date.now();\n }\n\n // Postgres promises to send a keepalive at roughly wal_sender_timeout / 2.\n // If we go long enough without receiving anything (data or keepalive), the\n // inbound half of the connection is silently dead — our outbound acks keep\n // flowing into the void, neither side errors, and the change-source sits\n // there forever. Tear down the readable to force a reconnect.\n //\n // The threshold is 2x wal_sender_timeout — i.e. two consecutive keepalives\n // missed — to avoid spurious reconnects from a single delayed keepalive\n // (PG scheduling jitter under load, our own event-loop stalls, etc.).\n const inboundTimeoutMs = walSenderTimeoutMs * 2;\n let lastReceivedTime = Date.now();\n\n const livenessTimer = setInterval(() => {\n const now = Date.now();\n if (now - lastAckTime > manualKeepaliveTimeout) {\n sendAck(0n);\n lc.debug?.(`sent manual keepalive`);\n }\n const sinceLastReceived = now - lastReceivedTime;\n if (sinceLastReceived > inboundTimeoutMs && !readable.destroyed) {\n lc.warn?.(\n `no message received from ${db.options.host} in ${sinceLastReceived}ms ` +\n `(> 2x wal_sender_timeout ${walSenderTimeoutMs}ms). Destroying ` +\n `replication stream to force reconnect.`,\n );\n readable.destroy(\n new Error(\n `replication stream inbound timeout after ${sinceLastReceived}ms`,\n ),\n );\n }\n }, manualKeepaliveTimeout / 5);\n\n let destroyed = false;\n const typeParsers = await getTypeParsers(db, {returnJsonAsString: true});\n const parser = new PgoutputParser(typeParsers);\n const messages = Subscription.create<StreamMessage>({\n cleanup: () => {\n destroyed = true;\n readable.destroyed || readable.destroy();\n clearInterval(livenessTimer);\n return session.end();\n },\n });\n\n readable.once(\n 'close',\n () =>\n // Only log a warning if the stream was not manually closed.\n destroyed || lc.warn?.(`replication stream closed by ${db.options.host}`),\n );\n readable.once(\n 'error',\n e =>\n // Don't log the shutdown signal. This is the expected way for upstream\n // to close the connection (and will be logged downstream).\n (e instanceof postgres.PostgresError && e.code === PG_ADMIN_SHUTDOWN) ||\n lc.warn?.(`error from ${db.options.host}`, e),\n );\n\n pipe({\n source: readable,\n sink: messages,\n parse: buffer => {\n lastReceivedTime = Date.now();\n return parseStreamMessage(lc, buffer, parser);\n },\n // Allow a small buffer of messages to be queued in the subscription so\n // that the change-source loop can check the queue to determine if more\n // messages are immediately available.\n bufferMessages: 5,\n });\n\n return {\n messages,\n acks: {push: sendAck},\n };\n}\n\n/**\n * Formats publication names for the START_REPLICATION command.\n * The replication protocol expects format: publication_names 'pub1,pub2'\n * Each name is escaped to handle any quotes that may have passed validation.\n */\nfunction formatPublicationNames(publications: string[]): string {\n // lit() returns 'escaped_name' with surrounding quotes\n // We strip the quotes since the outer quotes are in the template\n return publications.map(p => lit(p).slice(1, -1)).join(',');\n}\n\nasync function startReplicationStream(\n lc: LogContext,\n session: postgres.Sql,\n slot: string,\n publications: string[],\n lsn: bigint,\n maxAttempts: number,\n) {\n for (let i = 0; i < maxAttempts; i++) {\n try {\n const stream = session\n .unsafe(\n `START_REPLICATION SLOT ${id(slot)} LOGICAL ${fromBigInt(lsn)} (\n proto_version '1',\n publication_names '${formatPublicationNames(publications)}',\n messages 'true'\n )`,\n )\n .execute();\n return await Promise.all([stream.readable(), stream.writable()]);\n } catch (e) {\n if (e instanceof postgres.PostgresError) {\n // error: replication slot \"zero_slot_change_source_test_id\" is active for PID 268\n if (e.code === PG_OBJECT_IN_USE) {\n // The freeing up of the replication slot is not transactional;\n // sometimes it takes time for Postgres to consider the slot\n // inactive.\n lc.warn?.(`attempt ${i + 1}: ${String(e)}`, e);\n await sleep(10);\n continue;\n }\n // error: This slot has been invalidated because it exceeded the maximum reserved size.\n // (This is a different manifestation of a slot being invalidated when\n // the wal exceeds the max_slot_wal_keep_size)\n if (e.code === PG_OBJECT_NOT_IN_PREREQUISITE_STATE) {\n lc.error?.(`error starting replication stream`, e);\n throw new AutoResetSignal(`unable to start replication stream`, {\n cause: e,\n });\n }\n }\n throw e;\n }\n }\n throw new Error(\n `exceeded max attempts (${maxAttempts}) to start the Postgres stream`,\n );\n}\n\nfunction parseStreamMessage(\n lc: LogContext,\n buffer: Buffer,\n parser: PgoutputParser,\n): StreamMessage | null {\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-START-REPLICATION\n if (buffer[0] !== 0x77 && buffer[0] !== 0x6b) {\n lc.warn?.('Unknown message', buffer[0]);\n return null;\n }\n const lsn = buffer.readBigUInt64BE(1);\n if (buffer[0] === 0x77) {\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-XLOGDATA\n // (Byte 25 is where the WAL data begins)\n return [lsn, parser.parse(buffer.subarray(25))];\n }\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-PRIMARY-KEEPALIVE-MESSAGE\n // (Byte 17: shouldRespond)\n const shouldRespond = buffer.readInt8(17) !== 0;\n return [lsn, {tag: 'keepalive', shouldRespond}];\n}\n\n// https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-STANDBY-STATUS-UPDATE\nfunction makeAck(lsn: bigint): Buffer {\n const microNow = BigInt(Date.now() - Date.UTC(2000, 0, 1)) * BigInt(1000);\n\n const x = Buffer.alloc(34);\n x[0] = 'r'.charCodeAt(0);\n x.writeBigInt64BE(lsn, 1);\n x.writeBigInt64BE(lsn, 9);\n x.writeBigInt64BE(lsn, 17);\n x.writeBigInt64BE(microNow, 25);\n return x;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"stream.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/logical-replication/stream.ts"],"sourcesContent":["import {\n PG_ADMIN_SHUTDOWN,\n PG_OBJECT_IN_USE,\n PG_OBJECT_NOT_IN_PREREQUISITE_STATE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {defu} from 'defu';\nimport postgres, {type Options, type PostgresType} from 'postgres';\nimport {sleep} from '../../../../../../shared/src/sleep.ts';\nimport {getTypeParsers} from '../../../../db/pg-type-parser.ts';\nimport {type PostgresDB} from '../../../../types/pg.ts';\nimport {id, lit} from '../../../../types/sql.ts';\nimport {pipe, type Sink, type Source} from '../../../../types/streams.ts';\nimport {Subscription} from '../../../../types/subscription.ts';\nimport {AutoResetSignal} from '../../../change-streamer/schema/tables.ts';\nimport {fromBigInt} from '../lsn.ts';\nimport {PgoutputParser} from './pgoutput-parser.ts';\nimport type {Message} from './pgoutput.types.ts';\n\nconst DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE = 5;\n\nexport type StreamMessage = [\n lsn: bigint,\n Message | {tag: 'keepalive'; shouldRespond: boolean},\n];\n\n// Expose the `queued` variable of the Subscription to allow\n// the change-source to determine if there are more messages\n// immediately available.\ntype SourceWithPendingQueue<T> = Source<T> & {\n queued: number;\n};\n\nexport async function subscribe(\n lc: LogContext,\n db: PostgresDB,\n slot: string,\n publications: string[],\n lsn: bigint,\n retriesIfReplicationSlotActive = DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE,\n applicationName = 'zero-replicator',\n inboundTimeoutOverrideMs?: number | undefined,\n): Promise<{\n messages: SourceWithPendingQueue<StreamMessage>;\n acks: Sink<bigint>;\n}> {\n const session = postgres(\n defu(\n {\n max: 1,\n ['fetch_types']: false, // Necessary for the streaming protocol\n ['idle_timeout']: null,\n ['max_lifetime']: null as unknown as number,\n connection: {\n ['application_name']: applicationName,\n replication: 'database', // https://www.postgresql.org/docs/current/protocol-replication.html\n },\n },\n // ParsedOptions are technically compatible with Options, but happen\n // to not be typed that way. The postgres.js author does an equivalent\n // merge of ParsedOptions and Options here:\n // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/subscribe.js#L13\n db.options as unknown as Options<Record<string, PostgresType>>,\n ),\n );\n\n // Postgres will send keepalives before timing out a wal_sender. It is possible that\n // these keepalives are not received if there is back-pressure in the replication\n // stream. To keep the connection alive, explicitly send keepalives if none have been\n // sent within the last 75% of the wal_sender_timeout.\n //\n // https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT\n const [{walSenderTimeoutMs}] = await session<\n {walSenderTimeoutMs: number}[]\n >`SELECT EXTRACT(EPOCH FROM (setting || unit)::interval) * 1000 \n AS \"walSenderTimeoutMs\" FROM pg_settings\n WHERE name = 'wal_sender_timeout'`.simple();\n const {\n enabled: livenessEnabled,\n manualKeepaliveTimeout,\n inboundTimeoutMs,\n timerIntervalMs,\n } = computeLivenessTimings(walSenderTimeoutMs, inboundTimeoutOverrideMs);\n lc.info?.(\n livenessEnabled\n ? `wal_sender_timeout: ${walSenderTimeoutMs}ms. ` +\n `Ensuring manual keepalives at least every ${manualKeepaliveTimeout}ms. ` +\n `Inbound timeout: ${inboundTimeoutMs}ms` +\n (inboundTimeoutMs === walSenderTimeoutMs * 2\n ? ''\n : ' (configured override)')\n : `wal_sender_timeout is disabled (0); skipping manual keepalives and ` +\n `inbound liveness detection.`,\n );\n\n const [readable, writable] = await startReplicationStream(\n lc,\n session,\n slot,\n publications,\n lsn,\n retriesIfReplicationSlotActive + 1,\n );\n\n let lastAckTime = Date.now();\n function sendAck(lsn: bigint) {\n writable.write(makeAck(lsn));\n lastAckTime = Date.now();\n }\n\n // Tear down the readable to force a reconnect if the inbound half of the\n // connection goes silent (our outbound acks keep flowing into the void,\n // neither side errors, and the change-source would otherwise sit there\n // forever). See computeLivenessTimings for the derivation of these\n // thresholds — and why the whole timer is disabled when wal_sender_timeout\n // is 0.\n let lastReceivedTime = Date.now();\n\n const livenessTimer = livenessEnabled\n ? setInterval(() => {\n const now = Date.now();\n if (now - lastAckTime > manualKeepaliveTimeout) {\n sendAck(0n);\n lc.debug?.(`sent manual keepalive`);\n }\n const sinceLastReceived = now - lastReceivedTime;\n const {resetWindow, timedOut} = evaluateInboundLiveness({\n sinceLastReceived,\n inboundTimeoutMs,\n queued: messages.queued,\n });\n if (resetWindow) {\n // Downstream back-pressure is preventing us from consuming (and thus\n // receiving) upstream messages. Keep the inbound window fresh so that\n // when the backlog finally drains we grant a full inboundTimeoutMs\n // before concluding the connection is dead — otherwise a long stall\n // would leave lastReceivedTime stale and fire this watchdog on the\n // first post-drain tick, tearing down a healthy connection.\n lastReceivedTime = now;\n } else if (timedOut && !readable.destroyed) {\n lc.warn?.(\n `no message received from ${db.options.host} in ${sinceLastReceived}ms ` +\n `(> inbound timeout ${inboundTimeoutMs}ms). Destroying ` +\n `replication stream to force reconnect.`,\n );\n readable.destroy(\n new Error(\n `replication stream inbound timeout after ${sinceLastReceived}ms`,\n ),\n );\n }\n }, timerIntervalMs)\n : undefined;\n\n let destroyed = false;\n const typeParsers = await getTypeParsers(db, {returnJsonAsString: true});\n const parser = new PgoutputParser(typeParsers);\n const messages = Subscription.create<StreamMessage>({\n cleanup: () => {\n destroyed = true;\n readable.destroyed || readable.destroy();\n clearInterval(livenessTimer);\n return session.end();\n },\n });\n\n readable.once(\n 'close',\n () =>\n // Only log a warning if the stream was not manually closed.\n destroyed || lc.warn?.(`replication stream closed by ${db.options.host}`),\n );\n readable.once(\n 'error',\n e =>\n // Don't log the shutdown signal. This is the expected way for upstream\n // to close the connection (and will be logged downstream).\n (e instanceof postgres.PostgresError && e.code === PG_ADMIN_SHUTDOWN) ||\n lc.warn?.(`error from ${db.options.host}`, e),\n );\n\n pipe({\n source: readable,\n sink: messages,\n parse: buffer => {\n lastReceivedTime = Date.now();\n return parseStreamMessage(lc, buffer, parser);\n },\n // Allow a small buffer of messages to be queued in the subscription so\n // that the change-source loop can check the queue to determine if more\n // messages are immediately available.\n bufferMessages: 5,\n });\n\n return {\n messages,\n acks: {push: sendAck},\n };\n}\n\n/**\n * Derives the timings for the replication liveness timer from the server's\n * `wal_sender_timeout` (in milliseconds).\n *\n * Postgres treats `wal_sender_timeout = 0` as \"disabled\": it will neither\n * terminate an idle wal sender nor emit the periodic keepalives it otherwise\n * sends at roughly `wal_sender_timeout / 2`. Every timing below is derived from\n * that interval, so a disabled timeout disables the liveness machinery entirely\n * (`enabled: false`). Otherwise the derived thresholds would all collapse to 0,\n * inverting the intended meaning: the inbound watchdog would fire on the very\n * first tick and destroy the stream, while the timer itself would busy-spin at\n * a 0ms interval — together producing a continuous reconnect storm.\n *\n * An `inboundTimeoutOverrideMs` (when positive) replaces the default\n * `2x wal_sender_timeout` inbound threshold. The two timeouts protect\n * different parties with asymmetric false-positive costs: `wal_sender_timeout`\n * is the server reaping dead clients (a false positive there costs a cheap\n * reconnect), while the inbound watchdog tears down the stream and replays the\n * in-flight transaction — expensive, and self-defeating when the wal sender is\n * legitimately silent for long stretches (e.g. decoding through WAL from\n * unpublished tables, or assembling a large transaction that pgoutput only\n * sends at commit). Operators who don't own the server setting (e.g. managed\n * or operator-provisioned Postgres with an aggressive cluster-wide\n * `wal_sender_timeout`) can widen the client-side fuse independently.\n *\n * The override has no effect when `wal_sender_timeout` is disabled (0): the\n * server then sends no periodic keepalives, so inbound silence is normal even\n * on an idle healthy connection and any silence-based watchdog would\n * false-positive.\n *\n * https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT\n */\nexport function computeLivenessTimings(\n walSenderTimeoutMs: number,\n inboundTimeoutOverrideMs?: number | undefined,\n): {\n enabled: boolean;\n manualKeepaliveTimeout: number;\n inboundTimeoutMs: number;\n timerIntervalMs: number;\n} {\n // Guard with `!(x > 0)` so that 0, negative, and NaN values all disable the\n // timer rather than producing degenerate (zero / NaN) thresholds.\n if (!(walSenderTimeoutMs > 0)) {\n return {\n enabled: false,\n manualKeepaliveTimeout: 0,\n inboundTimeoutMs: 0,\n timerIntervalMs: 0,\n };\n }\n // Send a manual keepalive if nothing has been sent in the last 75% of the\n // wal_sender_timeout, so Postgres never times us out under back-pressure.\n const manualKeepaliveTimeout = Math.floor(walSenderTimeoutMs * 0.75);\n return {\n enabled: true,\n manualKeepaliveTimeout,\n // Force a reconnect after 2x wal_sender_timeout — i.e. two consecutive\n // missed keepalives — without any inbound message, to avoid spurious\n // reconnects from a single delayed keepalive (PG scheduling jitter under\n // load, our own event-loop stalls, etc.). A 0 / negative / NaN override\n // falls back to the default rather than degenerating the threshold.\n inboundTimeoutMs:\n inboundTimeoutOverrideMs !== undefined && inboundTimeoutOverrideMs > 0\n ? inboundTimeoutOverrideMs\n : walSenderTimeoutMs * 2,\n // Poll at 1/5th of the manual keepalive interval.\n timerIntervalMs: manualKeepaliveTimeout / 5,\n };\n}\n\n/**\n * Pure decision for the inbound-liveness watchdog, given the time since the\n * last inbound message, the timeout threshold, and the number of messages\n * currently queued downstream (see {@link SourceWithPendingQueue.queued}).\n *\n * A non-empty downstream queue means we have received data we haven't yet\n * consumed: the reason we aren't receiving *more* is our own back-pressure, not\n * upstream silence. In that case we `resetWindow` (treat the backlog as\n * liveness evidence) rather than counting the stall against the connection.\n * This both suppresses false timeouts during back-pressure and — crucially —\n * keeps `lastReceivedTime` fresh so the drain transition (queue emptying after\n * a long stall) doesn't fire the watchdog against a stale timestamp on the very\n * next tick.\n *\n * Only when the queue is empty (consumption has caught up, so reading — and\n * thus keepalives — should be flowing again) does an over-threshold gap\n * indicate a genuinely dead inbound half.\n */\nexport function evaluateInboundLiveness({\n sinceLastReceived,\n inboundTimeoutMs,\n queued,\n}: {\n sinceLastReceived: number;\n inboundTimeoutMs: number;\n queued: number;\n}): {resetWindow: boolean; timedOut: boolean} {\n if (queued > 0) {\n return {resetWindow: true, timedOut: false};\n }\n return {resetWindow: false, timedOut: sinceLastReceived > inboundTimeoutMs};\n}\n\n/**\n * Formats publication names for the START_REPLICATION command.\n * The replication protocol expects format: publication_names 'pub1,pub2'\n * Each name is escaped to handle any quotes that may have passed validation.\n */\nfunction formatPublicationNames(publications: string[]): string {\n // lit() returns 'escaped_name' with surrounding quotes\n // We strip the quotes since the outer quotes are in the template\n return publications.map(p => lit(p).slice(1, -1)).join(',');\n}\n\nasync function startReplicationStream(\n lc: LogContext,\n session: postgres.Sql,\n slot: string,\n publications: string[],\n lsn: bigint,\n maxAttempts: number,\n) {\n for (let i = 0; i < maxAttempts; i++) {\n try {\n const stream = session\n .unsafe(\n `START_REPLICATION SLOT ${id(slot)} LOGICAL ${fromBigInt(lsn)} (\n proto_version '1',\n publication_names '${formatPublicationNames(publications)}',\n messages 'true'\n )`,\n )\n .execute();\n return await Promise.all([stream.readable(), stream.writable()]);\n } catch (e) {\n if (e instanceof postgres.PostgresError) {\n // error: replication slot \"zero_slot_change_source_test_id\" is active for PID 268\n if (e.code === PG_OBJECT_IN_USE) {\n // The freeing up of the replication slot is not transactional;\n // sometimes it takes time for Postgres to consider the slot\n // inactive.\n lc.warn?.(`attempt ${i + 1}: ${String(e)}`, e);\n await sleep(10);\n continue;\n }\n // error: This slot has been invalidated because it exceeded the maximum reserved size.\n // (This is a different manifestation of a slot being invalidated when\n // the wal exceeds the max_slot_wal_keep_size)\n if (e.code === PG_OBJECT_NOT_IN_PREREQUISITE_STATE) {\n lc.error?.(`error starting replication stream`, e);\n throw new AutoResetSignal(`unable to start replication stream`, {\n cause: e,\n });\n }\n }\n throw e;\n }\n }\n throw new Error(\n `exceeded max attempts (${maxAttempts}) to start the Postgres stream`,\n );\n}\n\nfunction parseStreamMessage(\n lc: LogContext,\n buffer: Buffer,\n parser: PgoutputParser,\n): StreamMessage | null {\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-START-REPLICATION\n if (buffer[0] !== 0x77 && buffer[0] !== 0x6b) {\n lc.warn?.('Unknown message', buffer[0]);\n return null;\n }\n const lsn = buffer.readBigUInt64BE(1);\n if (buffer[0] === 0x77) {\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-XLOGDATA\n // (Byte 25 is where the WAL data begins)\n return [lsn, parser.parse(buffer.subarray(25))];\n }\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-PRIMARY-KEEPALIVE-MESSAGE\n // (Byte 17: shouldRespond)\n const shouldRespond = buffer.readInt8(17) !== 0;\n return [lsn, {tag: 'keepalive', shouldRespond}];\n}\n\n// https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-STANDBY-STATUS-UPDATE\nfunction makeAck(lsn: bigint): Buffer {\n const microNow = BigInt(Date.now() - Date.UTC(2000, 0, 1)) * BigInt(1000);\n\n const x = Buffer.alloc(34);\n x[0] = 'r'.charCodeAt(0);\n x.writeBigInt64BE(lsn, 1);\n x.writeBigInt64BE(lsn, 9);\n x.writeBigInt64BE(lsn, 17);\n x.writeBigInt64BE(microNow, 25);\n return x;\n}\n"],"mappings":";;;;;;;;;;;;;AAmBA,IAAM,6CAA6C;AAcnD,eAAsB,UACpB,IACA,IACA,MACA,cACA,KACA,iCAAiC,4CACjC,kBAAkB,mBAClB,0BAIC;CACD,MAAM,UAAU,SACd,KACE;EACE,KAAK;GACJ,gBAAgB;GAChB,iBAAiB;GACjB,iBAAiB;EAClB,YAAY;IACT,qBAAqB;GACtB,aAAa;EACf;CACF,GAKA,GAAG,OACL,CACF;CAQA,MAAM,CAAC,EAAC,wBAAuB,MAAM,OAEpC;;2CAEwC,OAAO;CAChD,MAAM,EACJ,SAAS,iBACT,wBACA,kBACA,oBACE,uBAAuB,oBAAoB,wBAAwB;CACvE,GAAG,OACD,kBACI,uBAAuB,mBAAmB,gDACK,uBAAuB,uBAChD,iBAAiB,OACpC,qBAAqB,qBAAqB,IACvC,KACA,4BACN,gGAEN;CAEA,MAAM,CAAC,UAAU,YAAY,MAAM,uBACjC,IACA,SACA,MACA,cACA,KACA,iCAAiC,CACnC;CAEA,IAAI,cAAc,KAAK,IAAI;CAC3B,SAAS,QAAQ,KAAa;EAC5B,SAAS,MAAM,QAAQ,GAAG,CAAC;EAC3B,cAAc,KAAK,IAAI;CACzB;CAQA,IAAI,mBAAmB,KAAK,IAAI;CAEhC,MAAM,gBAAgB,kBAClB,kBAAkB;EAChB,MAAM,MAAM,KAAK,IAAI;EACrB,IAAI,MAAM,cAAc,wBAAwB;GAC9C,QAAQ,EAAE;GACV,GAAG,QAAQ,uBAAuB;EACpC;EACA,MAAM,oBAAoB,MAAM;EAChC,MAAM,EAAC,aAAa,aAAY,wBAAwB;GACtD;GACA;GACA,QAAQ,SAAS;EACnB,CAAC;EACD,IAAI,aAOF,mBAAmB;OACd,IAAI,YAAY,CAAC,SAAS,WAAW;GAC1C,GAAG,OACD,4BAA4B,GAAG,QAAQ,KAAK,MAAM,kBAAkB,wBAC5C,iBAAiB,uDAE3C;GACA,SAAS,wBACP,IAAI,MACF,4CAA4C,kBAAkB,GAChE,CACF;EACF;CACF,GAAG,eAAe,IAClB,KAAA;CAEJ,IAAI,YAAY;CAEhB,MAAM,SAAS,IAAI,eAAe,MADR,eAAe,IAAI,EAAC,oBAAoB,KAAI,CAAC,CAC1B;CAC7C,MAAM,WAAW,aAAa,OAAsB,EAClD,eAAe;EACb,YAAY;EACZ,SAAS,aAAa,SAAS,QAAQ;EACvC,cAAc,aAAa;EAC3B,OAAO,QAAQ,IAAI;CACrB,EACF,CAAC;CAED,SAAS,KACP,eAGE,aAAa,GAAG,OAAO,gCAAgC,GAAG,QAAQ,MAAM,CAC5E;CACA,SAAS,KACP,UACA,MAGG,aAAa,SAAS,iBAAiB,EAAE,SAAS,qBACnD,GAAG,OAAO,cAAc,GAAG,QAAQ,QAAQ,CAAC,CAChD;CAEA,KAAK;EACH,QAAQ;EACR,MAAM;EACN,QAAO,WAAU;GACf,mBAAmB,KAAK,IAAI;GAC5B,OAAO,mBAAmB,IAAI,QAAQ,MAAM;EAC9C;EAIA,gBAAgB;CAClB,CAAC;CAED,OAAO;EACL;EACA,MAAM,EAAC,MAAM,QAAO;CACtB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,uBACd,oBACA,0BAMA;CAGA,IAAI,EAAE,qBAAqB,IACzB,OAAO;EACL,SAAS;EACT,wBAAwB;EACxB,kBAAkB;EAClB,iBAAiB;CACnB;CAIF,MAAM,yBAAyB,KAAK,MAAM,qBAAqB,GAAI;CACnE,OAAO;EACL,SAAS;EACT;EAMA,kBACE,6BAA6B,KAAA,KAAa,2BAA2B,IACjE,2BACA,qBAAqB;EAE3B,iBAAiB,yBAAyB;CAC5C;AACF;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,wBAAwB,EACtC,mBACA,kBACA,UAK4C;CAC5C,IAAI,SAAS,GACX,OAAO;EAAC,aAAa;EAAM,UAAU;CAAK;CAE5C,OAAO;EAAC,aAAa;EAAO,UAAU,oBAAoB;CAAgB;AAC5E;;;;;;AAOA,SAAS,uBAAuB,cAAgC;CAG9D,OAAO,aAAa,KAAI,MAAK,IAAI,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK,GAAG;AAC5D;AAEA,eAAe,uBACb,IACA,SACA,MACA,cACA,KACA,aACA;CACA,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,KAC/B,IAAI;EACF,MAAM,SAAS,QACZ,OACC,0BAA0B,GAAG,IAAI,EAAE,WAAW,WAAW,GAAG,EAAE;;6BAE3C,uBAAuB,YAAY,EAAE;;QAG1D,EACC,QAAQ;EACX,OAAO,MAAM,QAAQ,IAAI,CAAC,OAAO,SAAS,GAAG,OAAO,SAAS,CAAC,CAAC;CACjE,SAAS,GAAG;EACV,IAAI,aAAa,SAAS,eAAe;GAEvC,IAAI,EAAE,SAAS,kBAAkB;IAI/B,GAAG,OAAO,WAAW,IAAI,EAAE,IAAI,OAAO,CAAC,KAAK,CAAC;IAC7C,MAAM,MAAM,EAAE;IACd;GACF;GAIA,IAAI,EAAE,SAAS,qCAAqC;IAClD,GAAG,QAAQ,qCAAqC,CAAC;IACjD,MAAM,IAAI,gBAAgB,sCAAsC,EAC9D,OAAO,EACT,CAAC;GACH;EACF;EACA,MAAM;CACR;CAEF,MAAM,IAAI,MACR,0BAA0B,YAAY,+BACxC;AACF;AAEA,SAAS,mBACP,IACA,QACA,QACsB;CAEtB,IAAI,OAAO,OAAO,OAAQ,OAAO,OAAO,KAAM;EAC5C,GAAG,OAAO,mBAAmB,OAAO,EAAE;EACtC,OAAO;CACT;CACA,MAAM,MAAM,OAAO,gBAAgB,CAAC;CACpC,IAAI,OAAO,OAAO,KAGhB,OAAO,CAAC,KAAK,OAAO,MAAM,OAAO,SAAS,EAAE,CAAC,CAAC;CAKhD,OAAO,CAAC,KAAK;EAAC,KAAK;EAAa,eADV,OAAO,SAAS,EAAE,MAAM;CACD,CAAC;AAChD;AAGA,SAAS,QAAQ,KAAqB;CACpC,MAAM,WAAW,OAAO,KAAK,IAAI,IAAI,KAAK,IAAI,KAAM,GAAG,CAAC,CAAC,IAAI,OAAO,GAAI;CAExE,MAAM,IAAI,OAAO,MAAM,EAAE;CACzB,EAAE,KAAK,IAAI,WAAW,CAAC;CACvB,EAAE,gBAAgB,KAAK,CAAC;CACxB,EAAE,gBAAgB,KAAK,CAAC;CACxB,EAAE,gBAAgB,KAAK,EAAE;CACzB,EAAE,gBAAgB,UAAU,EAAE;CAC9B,OAAO;AACT"}
@@ -5,57 +5,24 @@ export declare const ddlEventSchema: v.ObjectType<Omit<{
5
5
  context: v.ObjectType<{
6
6
  query: v.Type<string>;
7
7
  }, v.Type<string>>;
8
- }, "schema" | "event" | "version" | "newColumns"> & {
9
- version: v.Type<1>;
10
- schema: v.Type<{
11
- indexes: {
12
- isReplicaIdentity?: boolean | undefined;
13
- isPrimaryKey?: boolean | undefined;
14
- isImmediate?: boolean | undefined;
15
- name: string;
16
- columns: Record<string, "ASC" | "DESC">;
17
- schema: string;
18
- tableName: string;
19
- unique: boolean;
20
- }[];
21
- tables: {
22
- replicaIdentityColumns: string[];
23
- primaryKey?: string[] | undefined;
24
- schemaOID?: number | undefined;
25
- replicaIdentity?: "n" | "d" | "f" | "i" | undefined;
26
- name: string;
27
- schema: string;
28
- oid: number;
29
- columns: Record<string, {
30
- pgTypeClass?: "e" | "d" | "b" | "c" | "p" | "r" | "m" | undefined;
31
- elemPgTypeClass?: "e" | "d" | "b" | "c" | "p" | "r" | "m" | null | undefined;
32
- characterMaximumLength?: number | null | undefined;
33
- notNull?: boolean | null | undefined;
34
- dflt?: string | null | undefined;
35
- pos: number;
36
- dataType: string;
37
- typeOID: number;
38
- }>;
39
- publications: Record<string, {
40
- rowFilter: string | null;
41
- }>;
42
- }[];
43
- }>;
8
+ }, "event" | "version" | "newColumns"> & {
9
+ version: v.Type<1 | 2>;
44
10
  event: v.ObjectType<{
45
11
  tag: v.Type<string>;
46
12
  }, undefined>;
47
13
  newColumns: v.Optional<Record<string, number[]> | null>;
48
14
  }, undefined>;
49
15
  /**
50
- * A {@link DdlStartEvent} message is emitted before every DDL event, containing
51
- * the current `schema` and the command `tag`.
16
+ * A {@link DdlStartEvent} message is emitted before every DDL event,
17
+ * containing the command `tag`.
52
18
  *
53
19
  * In most cases, the `DdlStartEvent` itself will not be associated with a
54
- * schema change, in which case `previousSchema` will be `null`. However, the
55
- * message is still emitted, both for backwards compatibility and to provide
56
- * the command `tag` context in case an immediately following `DdlStartEvent`
57
- * tag is emitted with a schema change (which can happen when another event
58
- * trigger results in a nested ddl statement).
20
+ * schema change, in which case the event is context-only (in protocol v2,
21
+ * planned, both `previousSchema` and `schema` will be absent; in v1,
22
+ * `schema` is set and `previousSchema` is `null`). The message is still
23
+ * emitted to provide the command `tag` context in case an immediately
24
+ * following `DdlStartEvent` tag is emitted with a schema change (which can
25
+ * happen when another event trigger results in a nested ddl statement).
59
26
  *
60
27
  * In such cases, the `previousSchema` and `schema` fields of the latter event
61
28
  * are used to determine the necessary schema change operations (as they are
@@ -68,9 +35,15 @@ export declare const ddlStartEventSchema: v.ObjectType<Omit<Omit<{
68
35
  context: v.ObjectType<{
69
36
  query: v.Type<string>;
70
37
  }, v.Type<string>>;
71
- }, "schema" | "event" | "version" | "newColumns"> & {
72
- version: v.Type<1>;
73
- schema: v.Type<{
38
+ }, "event" | "version" | "newColumns"> & {
39
+ version: v.Type<1 | 2>;
40
+ event: v.ObjectType<{
41
+ tag: v.Type<string>;
42
+ }, undefined>;
43
+ newColumns: v.Optional<Record<string, number[]> | null>;
44
+ }, "type" | "schema" | "event" | "previousSchema"> & {
45
+ type: v.Type<"ddlStart">;
46
+ schema: v.Optional<{
74
47
  indexes: {
75
48
  isReplicaIdentity?: boolean | undefined;
76
49
  isPrimaryKey?: boolean | undefined;
@@ -104,12 +77,6 @@ export declare const ddlStartEventSchema: v.ObjectType<Omit<Omit<{
104
77
  }>;
105
78
  }[];
106
79
  }>;
107
- event: v.ObjectType<{
108
- tag: v.Type<string>;
109
- }, undefined>;
110
- newColumns: v.Optional<Record<string, number[]> | null>;
111
- }, "type" | "event" | "previousSchema"> & {
112
- type: v.Type<"ddlStart">;
113
80
  previousSchema: v.Optional<{
114
81
  indexes: {
115
82
  isReplicaIdentity?: boolean | undefined;
@@ -158,8 +125,14 @@ export declare const ddlUpdateEventSchema: v.ObjectType<Omit<Omit<{
158
125
  context: v.ObjectType<{
159
126
  query: v.Type<string>;
160
127
  }, v.Type<string>>;
161
- }, "schema" | "event" | "version" | "newColumns"> & {
162
- version: v.Type<1>;
128
+ }, "event" | "version" | "newColumns"> & {
129
+ version: v.Type<1 | 2>;
130
+ event: v.ObjectType<{
131
+ tag: v.Type<string>;
132
+ }, undefined>;
133
+ newColumns: v.Optional<Record<string, number[]> | null>;
134
+ }, "type" | "schema" | "previousSchema"> & {
135
+ type: v.Type<"ddlUpdate">;
163
136
  schema: v.Type<{
164
137
  indexes: {
165
138
  isReplicaIdentity?: boolean | undefined;
@@ -194,12 +167,6 @@ export declare const ddlUpdateEventSchema: v.ObjectType<Omit<Omit<{
194
167
  }>;
195
168
  }[];
196
169
  }>;
197
- event: v.ObjectType<{
198
- tag: v.Type<string>;
199
- }, undefined>;
200
- newColumns: v.Optional<Record<string, number[]> | null>;
201
- }, "type" | "previousSchema"> & {
202
- type: v.Type<"ddlUpdate">;
203
170
  previousSchema: v.Optional<{
204
171
  indexes: {
205
172
  isReplicaIdentity?: boolean | undefined;
@@ -262,8 +229,14 @@ export declare const schemaSnapshotEventSchema: v.ObjectType<Omit<Omit<{
262
229
  context: v.ObjectType<{
263
230
  query: v.Type<string>;
264
231
  }, v.Type<string>>;
265
- }, "schema" | "event" | "version" | "newColumns"> & {
266
- version: v.Type<1>;
232
+ }, "event" | "version" | "newColumns"> & {
233
+ version: v.Type<1 | 2>;
234
+ event: v.ObjectType<{
235
+ tag: v.Type<string>;
236
+ }, undefined>;
237
+ newColumns: v.Optional<Record<string, number[]> | null>;
238
+ }, "type" | "schema" | "previousSchema"> & {
239
+ type: v.Type<"schemaSnapshot">;
267
240
  schema: v.Type<{
268
241
  indexes: {
269
242
  isReplicaIdentity?: boolean | undefined;
@@ -298,12 +271,6 @@ export declare const schemaSnapshotEventSchema: v.ObjectType<Omit<Omit<{
298
271
  }>;
299
272
  }[];
300
273
  }>;
301
- event: v.ObjectType<{
302
- tag: v.Type<string>;
303
- }, undefined>;
304
- newColumns: v.Optional<Record<string, number[]> | null>;
305
- }, "type" | "previousSchema"> & {
306
- type: v.Type<"schemaSnapshot">;
307
274
  previousSchema: v.Optional<{
308
275
  indexes: {
309
276
  isReplicaIdentity?: boolean | undefined;
@@ -344,9 +311,15 @@ export declare const replicationEventSchema: v.UnionType<[v.ObjectType<Omit<Omit
344
311
  context: v.ObjectType<{
345
312
  query: v.Type<string>;
346
313
  }, v.Type<string>>;
347
- }, "schema" | "event" | "version" | "newColumns"> & {
348
- version: v.Type<1>;
349
- schema: v.Type<{
314
+ }, "event" | "version" | "newColumns"> & {
315
+ version: v.Type<1 | 2>;
316
+ event: v.ObjectType<{
317
+ tag: v.Type<string>;
318
+ }, undefined>;
319
+ newColumns: v.Optional<Record<string, number[]> | null>;
320
+ }, "type" | "schema" | "event" | "previousSchema"> & {
321
+ type: v.Type<"ddlStart">;
322
+ schema: v.Optional<{
350
323
  indexes: {
351
324
  isReplicaIdentity?: boolean | undefined;
352
325
  isPrimaryKey?: boolean | undefined;
@@ -380,12 +353,6 @@ export declare const replicationEventSchema: v.UnionType<[v.ObjectType<Omit<Omit
380
353
  }>;
381
354
  }[];
382
355
  }>;
383
- event: v.ObjectType<{
384
- tag: v.Type<string>;
385
- }, undefined>;
386
- newColumns: v.Optional<Record<string, number[]> | null>;
387
- }, "type" | "event" | "previousSchema"> & {
388
- type: v.Type<"ddlStart">;
389
356
  previousSchema: v.Optional<{
390
357
  indexes: {
391
358
  isReplicaIdentity?: boolean | undefined;
@@ -427,8 +394,14 @@ export declare const replicationEventSchema: v.UnionType<[v.ObjectType<Omit<Omit
427
394
  context: v.ObjectType<{
428
395
  query: v.Type<string>;
429
396
  }, v.Type<string>>;
430
- }, "schema" | "event" | "version" | "newColumns"> & {
431
- version: v.Type<1>;
397
+ }, "event" | "version" | "newColumns"> & {
398
+ version: v.Type<1 | 2>;
399
+ event: v.ObjectType<{
400
+ tag: v.Type<string>;
401
+ }, undefined>;
402
+ newColumns: v.Optional<Record<string, number[]> | null>;
403
+ }, "type" | "schema" | "previousSchema"> & {
404
+ type: v.Type<"ddlUpdate">;
432
405
  schema: v.Type<{
433
406
  indexes: {
434
407
  isReplicaIdentity?: boolean | undefined;
@@ -463,12 +436,6 @@ export declare const replicationEventSchema: v.UnionType<[v.ObjectType<Omit<Omit
463
436
  }>;
464
437
  }[];
465
438
  }>;
466
- event: v.ObjectType<{
467
- tag: v.Type<string>;
468
- }, undefined>;
469
- newColumns: v.Optional<Record<string, number[]> | null>;
470
- }, "type" | "previousSchema"> & {
471
- type: v.Type<"ddlUpdate">;
472
439
  previousSchema: v.Optional<{
473
440
  indexes: {
474
441
  isReplicaIdentity?: boolean | undefined;
@@ -507,8 +474,14 @@ export declare const replicationEventSchema: v.UnionType<[v.ObjectType<Omit<Omit
507
474
  context: v.ObjectType<{
508
475
  query: v.Type<string>;
509
476
  }, v.Type<string>>;
510
- }, "schema" | "event" | "version" | "newColumns"> & {
511
- version: v.Type<1>;
477
+ }, "event" | "version" | "newColumns"> & {
478
+ version: v.Type<1 | 2>;
479
+ event: v.ObjectType<{
480
+ tag: v.Type<string>;
481
+ }, undefined>;
482
+ newColumns: v.Optional<Record<string, number[]> | null>;
483
+ }, "type" | "schema" | "previousSchema"> & {
484
+ type: v.Type<"schemaSnapshot">;
512
485
  schema: v.Type<{
513
486
  indexes: {
514
487
  isReplicaIdentity?: boolean | undefined;
@@ -543,12 +516,6 @@ export declare const replicationEventSchema: v.UnionType<[v.ObjectType<Omit<Omit
543
516
  }>;
544
517
  }[];
545
518
  }>;
546
- event: v.ObjectType<{
547
- tag: v.Type<string>;
548
- }, undefined>;
549
- newColumns: v.Optional<Record<string, number[]> | null>;
550
- }, "type" | "previousSchema"> & {
551
- type: v.Type<"schemaSnapshot">;
552
519
  previousSchema: v.Optional<{
553
520
  indexes: {
554
521
  isReplicaIdentity?: boolean | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"ddl.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/ddl.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,CAAC,MAAM,wCAAwC,CAAC;AAC5D,OAAO,EAAiB,KAAK,WAAW,EAAC,MAAM,6BAA6B,CAAC;AAU7E,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAQlC,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAiBzB,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAc9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAQ/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAGpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAIlC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAYtE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,WAAW,UAwM/D;AAGD,eAAO,MAAM,IAAI,2HAQP,CAAC;AAEX,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,WAAW,UAoC9D;AAGD,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,GAAG,MAAM,UAMzB"}
1
+ {"version":3,"file":"ddl.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/ddl.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,CAAC,MAAM,wCAAwC,CAAC;AAC5D,OAAO,EAAiB,KAAK,WAAW,EAAC,MAAM,6BAA6B,CAAC;AAU7E,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAkBlC,eAAO,MAAM,cAAc;;;;;;;;;;aAgBzB,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAc9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAU/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAIpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAIlC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAYtE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,WAAW,UAwM/D;AAGD,eAAO,MAAM,IAAI,2HAQP,CAAC;AAEX,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,WAAW,UAoC9D;AAGD,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,GAAG,MAAM,UAMzB"}
@@ -1,25 +1,26 @@
1
1
  import { assert } from "../../../../../../shared/src/asserts.js";
2
- import { valita_exports } from "../../../../../../shared/src/valita.js";
2
+ import { literalUnion, valita_exports } from "../../../../../../shared/src/valita.js";
3
3
  import { upstreamSchema } from "../../../../types/shards.js";
4
4
  import { id } from "../../../../types/sql.js";
5
5
  import { publishedSchema, publishedSchemaQuery } from "./published.js";
6
6
  import { literal } from "pg-format";
7
+ var versionSchema = literalUnion(1, 2);
7
8
  var ddlEventSchema = valita_exports.object({ context: valita_exports.object({ query: valita_exports.string() }).rest(valita_exports.string()) }).extend({
8
- version: valita_exports.literal(1),
9
- schema: publishedSchema,
9
+ version: versionSchema,
10
10
  event: valita_exports.object({ tag: valita_exports.string() }),
11
11
  newColumns: valita_exports.record(valita_exports.array(valita_exports.number())).nullable().optional()
12
12
  });
13
13
  /**
14
- * A {@link DdlStartEvent} message is emitted before every DDL event, containing
15
- * the current `schema` and the command `tag`.
14
+ * A {@link DdlStartEvent} message is emitted before every DDL event,
15
+ * containing the command `tag`.
16
16
  *
17
17
  * In most cases, the `DdlStartEvent` itself will not be associated with a
18
- * schema change, in which case `previousSchema` will be `null`. However, the
19
- * message is still emitted, both for backwards compatibility and to provide
20
- * the command `tag` context in case an immediately following `DdlStartEvent`
21
- * tag is emitted with a schema change (which can happen when another event
22
- * trigger results in a nested ddl statement).
18
+ * schema change, in which case the event is context-only (in protocol v2,
19
+ * planned, both `previousSchema` and `schema` will be absent; in v1,
20
+ * `schema` is set and `previousSchema` is `null`). The message is still
21
+ * emitted to provide the command `tag` context in case an immediately
22
+ * following `DdlStartEvent` tag is emitted with a schema change (which can
23
+ * happen when another event trigger results in a nested ddl statement).
23
24
  *
24
25
  * In such cases, the `previousSchema` and `schema` fields of the latter event
25
26
  * are used to determine the necessary schema change operations (as they are
@@ -30,6 +31,7 @@ var ddlEventSchema = valita_exports.object({ context: valita_exports.object({ qu
30
31
  */
31
32
  var ddlStartEventSchema = ddlEventSchema.extend({
32
33
  type: valita_exports.literal("ddlStart"),
34
+ schema: publishedSchema.optional(),
33
35
  previousSchema: publishedSchema.nullable().optional(),
34
36
  event: valita_exports.object({ tag: valita_exports.string() }).optional(() => ({ tag: "UNKNOWN" }))
35
37
  });
@@ -40,6 +42,7 @@ var ddlStartEventSchema = ddlEventSchema.extend({
40
42
  */
41
43
  var ddlUpdateEventSchema = ddlEventSchema.extend({
42
44
  type: valita_exports.literal("ddlUpdate"),
45
+ schema: publishedSchema,
43
46
  previousSchema: publishedSchema.optional()
44
47
  });
45
48
  /**
@@ -66,6 +69,7 @@ var ddlUpdateEventSchema = ddlEventSchema.extend({
66
69
  */
67
70
  var schemaSnapshotEventSchema = ddlEventSchema.extend({
68
71
  type: valita_exports.literal("schemaSnapshot"),
72
+ schema: publishedSchema,
69
73
  previousSchema: publishedSchema.optional()
70
74
  });
71
75
  var replicationEventSchema = valita_exports.union(ddlStartEventSchema, ddlUpdateEventSchema, schemaSnapshotEventSchema);
@@ -175,7 +179,7 @@ BEGIN
175
179
 
176
180
  PERFORM pg_logical_emit_message(true, '${appID}/${shardNum}/ddl', message);
177
181
 
178
- RAISE NOTICE 'Emitted ${appID}_${shardNum} % for % %', event_type, tag,
182
+ RAISE NOTICE 'Emitted ${appID}_${shardNum} % for % %', event_type, tag,
179
183
  COALESCE(row_to_json(target)::text, '');
180
184
  END
181
185
  $$ LANGUAGE plpgsql;