@rocicorp/zero 1.11.0-canary.4 → 1.11.0-canary.6

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 (123) hide show
  1. package/out/replicache/src/kv/expo-sqlite/store.d.ts.map +1 -1
  2. package/out/replicache/src/kv/expo-sqlite/store.js +27 -4
  3. package/out/replicache/src/kv/expo-sqlite/store.js.map +1 -1
  4. package/out/replicache/src/kv/sqlite-store.d.ts +11 -0
  5. package/out/replicache/src/kv/sqlite-store.d.ts.map +1 -1
  6. package/out/replicache/src/kv/sqlite-store.js +43 -3
  7. package/out/replicache/src/kv/sqlite-store.js.map +1 -1
  8. package/out/shared/src/error.d.ts +11 -0
  9. package/out/shared/src/error.d.ts.map +1 -1
  10. package/out/shared/src/error.js +35 -1
  11. package/out/shared/src/error.js.map +1 -1
  12. package/out/shared/src/map.d.ts +7 -2
  13. package/out/shared/src/map.d.ts.map +1 -1
  14. package/out/shared/src/map.js.map +1 -1
  15. package/out/zero/package.js +2 -2
  16. package/out/zero/package.js.map +1 -1
  17. package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
  18. package/out/zero-cache/src/config/normalize.js +7 -1
  19. package/out/zero-cache/src/config/normalize.js.map +1 -1
  20. package/out/zero-cache/src/config/zero-config.d.ts +9 -0
  21. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  22. package/out/zero-cache/src/config/zero-config.js +21 -0
  23. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  24. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  25. package/out/zero-cache/src/server/change-streamer.js +15 -6
  26. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  27. package/out/zero-cache/src/server/main.d.ts.map +1 -1
  28. package/out/zero-cache/src/server/main.js +8 -1
  29. package/out/zero-cache/src/server/main.js.map +1 -1
  30. package/out/zero-cache/src/server/mutator.js +2 -0
  31. package/out/zero-cache/src/server/mutator.js.map +1 -1
  32. package/out/zero-cache/src/server/reaper.d.ts.map +1 -1
  33. package/out/zero-cache/src/server/reaper.js +2 -0
  34. package/out/zero-cache/src/server/reaper.js.map +1 -1
  35. package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
  36. package/out/zero-cache/src/server/replicator.js +2 -0
  37. package/out/zero-cache/src/server/replicator.js.map +1 -1
  38. package/out/zero-cache/src/server/runner/zero-dispatcher.d.ts.map +1 -1
  39. package/out/zero-cache/src/server/runner/zero-dispatcher.js +3 -0
  40. package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
  41. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  42. package/out/zero-cache/src/server/syncer.js +2 -0
  43. package/out/zero-cache/src/server/syncer.js.map +1 -1
  44. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +1 -1
  45. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.d.ts.map +1 -1
  46. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js +68 -8
  47. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js.map +1 -1
  48. package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts +19 -4
  49. package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts.map +1 -1
  50. package/out/zero-cache/src/services/change-streamer/change-log-codec.js +28 -8
  51. package/out/zero-cache/src/services/change-streamer/change-log-codec.js.map +1 -1
  52. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +3 -0
  53. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  54. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +3 -1
  55. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  56. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +6 -0
  57. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
  58. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +86 -51
  59. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  60. package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts +1 -1
  61. package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts.map +1 -1
  62. package/out/zero-cache/src/services/change-streamer/schema/tables.js +4 -4
  63. package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
  64. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.d.ts +6 -1
  65. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.d.ts.map +1 -1
  66. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js +27 -9
  67. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js.map +1 -1
  68. package/out/zero-cache/src/services/change-streamer/storer.d.ts +2 -2
  69. package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
  70. package/out/zero-cache/src/services/change-streamer/storer.js +6 -5
  71. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  72. package/out/zero-cache/src/services/litestream/litestream-checkpointer.js +50 -23
  73. package/out/zero-cache/src/services/litestream/litestream-checkpointer.js.map +1 -1
  74. package/out/zero-cache/src/services/profz.d.ts +7 -0
  75. package/out/zero-cache/src/services/profz.d.ts.map +1 -0
  76. package/out/zero-cache/src/services/profz.js +99 -0
  77. package/out/zero-cache/src/services/profz.js.map +1 -0
  78. package/out/zero-cache/src/services/replicator/change-log-stream-writer.d.ts +5 -4
  79. package/out/zero-cache/src/services/replicator/change-log-stream-writer.d.ts.map +1 -1
  80. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js +20 -22
  81. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js.map +1 -1
  82. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.d.ts +6 -6
  83. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.d.ts.map +1 -1
  84. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js +19 -14
  85. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js.map +1 -1
  86. package/out/zero-cache/src/services/view-syncer/cvr.d.ts +17 -0
  87. package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
  88. package/out/zero-cache/src/services/view-syncer/cvr.js +59 -0
  89. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  90. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.d.ts +51 -0
  91. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.d.ts.map +1 -0
  92. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js +78 -0
  93. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js.map +1 -0
  94. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  95. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +18 -6
  96. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  97. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  98. package/out/zero-cache/src/services/view-syncer/view-syncer.js +154 -6
  99. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  100. package/out/zero-cache/src/types/processes.d.ts +24 -0
  101. package/out/zero-cache/src/types/processes.d.ts.map +1 -1
  102. package/out/zero-cache/src/types/processes.js +25 -1
  103. package/out/zero-cache/src/types/processes.js.map +1 -1
  104. package/out/zero-cache/src/types/profiler.d.ts +12 -0
  105. package/out/zero-cache/src/types/profiler.d.ts.map +1 -1
  106. package/out/zero-cache/src/types/profiler.js +46 -1
  107. package/out/zero-cache/src/types/profiler.js.map +1 -1
  108. package/out/zero-client/src/client/version.js +1 -1
  109. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  110. package/out/zero-client/src/client/zero.js +12 -8
  111. package/out/zero-client/src/client/zero.js.map +1 -1
  112. package/out/zero-protocol/src/down.d.ts +27 -27
  113. package/out/zero-react/src/use-query.d.ts.map +1 -1
  114. package/out/zero-react/src/use-query.js +50 -3
  115. package/out/zero-react/src/use-query.js.map +1 -1
  116. package/out/zero-solid/src/use-query.js +1 -2
  117. package/out/zero-solid/src/use-query.js.map +1 -1
  118. package/out/zql/src/ivm/data.d.ts.map +1 -1
  119. package/out/zql/src/ivm/data.js +8 -3
  120. package/out/zql/src/ivm/data.js.map +1 -1
  121. package/out/zql/src/ivm/view-apply-change.js +6 -1
  122. package/out/zql/src/ivm/view-apply-change.js.map +1 -1
  123. package/package.json +2 -2
@@ -1,12 +1,16 @@
1
+ import { assert } from "../../../../shared/src/asserts.js";
1
2
  import { must } from "../../../../shared/src/must.js";
3
+ import { h32 } from "../../../../shared/src/hash.js";
2
4
  import { readCookieRowCounts, readCookies } from "../replicator/change-log-cookies.js";
3
5
  import { StatementRunner } from "../../db/statements.js";
4
6
  import { isSQLiteCorruption, logSQLiteCorruptionDiagnostics } from "../../db/sqlite-corruption.js";
5
7
  import { ChangeLogRebuildRequired, changeLogFileName, changeLogWipeReason, deleteChangeLogDB, openChangeLogDBForWriting, readChangeLogHead, rebuildChangeLogDBForWriting, reconcileChangeLog } from "../replicator/change-log-db.js";
8
+ import { extractChangeSubstring } from "./change-log-codec.js";
6
9
  import { ChangeLogStreamWriter } from "../replicator/change-log-stream-writer.js";
7
10
  import { SQLiteChangeLogObserver, getSQLiteChangeLogInfo, logSQLiteChangeLogStartup, recordSQLiteChangeLogReconcile } from "../replicator/sqlite-change-log-observability.js";
8
11
  import { SqliteError } from "@rocicorp/zero-sqlite3";
9
12
  //#region ../zero-cache/src/services/change-streamer/sqlite-change-log-writer.ts
13
+ var DEFAULT_SHADOW_VALIDATION_PERCENT = 1;
10
14
  /**
11
15
  * Appends the canonical change stream to the SQLite change log, from the
12
16
  * change-streamer's own stream loop.
@@ -41,14 +45,18 @@ var SQLiteChangeLogWriter = class {
41
45
  #lc;
42
46
  #opts;
43
47
  #now;
48
+ #shadowValidationPercent;
44
49
  #db;
45
50
  #writer;
46
51
  #observer;
47
52
  #disabled = false;
53
+ #validateTransaction = false;
48
54
  constructor(lc, opts) {
49
55
  this.#lc = lc.withContext("component", "sqlite-change-log-writer");
50
56
  this.#opts = opts;
51
57
  this.#now = opts.now ?? Date.now;
58
+ this.#shadowValidationPercent = opts.shadowValidationPercent ?? DEFAULT_SHADOW_VALIDATION_PERCENT;
59
+ assert(Number.isInteger(this.#shadowValidationPercent) && this.#shadowValidationPercent >= 0 && this.#shadowValidationPercent <= 100, "SQLite change-log shadow validation percentage must be an integer between 0 and 100");
52
60
  }
53
61
  /** False once the writer has failed soft, or after {@link close}. */
54
62
  get enabled() {
@@ -161,26 +169,28 @@ var SQLiteChangeLogWriter = class {
161
169
  * Writes one stream message, committing at transaction boundaries. Never
162
170
  * throws: a write failure disables the writer instead of failing the stream.
163
171
  */
164
- write(change, json) {
172
+ write(change, json, storedChange = extractChangeSubstring(json, change[1].tag)) {
165
173
  const writer = this.#writer;
166
174
  const db = this.#db;
167
175
  if (this.#disabled || writer === void 0 || db === void 0) return;
168
- const observer = this.#observer;
169
- observer?.messageReceived(change, json);
170
176
  const [type, msg] = change;
171
- const startedAt = performance.now();
177
+ if (type === "begin") this.#validateTransaction = this.#isSelectedForShadowValidation(change[2].commitWatermark);
178
+ const validateTransaction = this.#validateTransaction;
179
+ const observer = this.#observer;
180
+ observer?.messageReceived(change, json, validateTransaction, storedChange);
181
+ const startedAt = validateTransaction ? performance.now() : void 0;
172
182
  try {
173
183
  let commit = null;
174
184
  switch (type) {
175
185
  case "begin":
176
- writer.begin(change[2].commitWatermark, json);
186
+ writer.begin(change[2].commitWatermark, json, storedChange, validateTransaction);
177
187
  break;
178
188
  case "commit": {
179
189
  const { watermark } = change[2];
180
190
  const commitStart = performance.now();
181
191
  commit = {
182
192
  watermark,
183
- stats: writer.commit(watermark, json, this.#now()),
193
+ stats: writer.commit(watermark, json, this.#now(), storedChange),
184
194
  logCommitMs: performance.now() - commitStart
185
195
  };
186
196
  break;
@@ -189,14 +199,16 @@ var SQLiteChangeLogWriter = class {
189
199
  writer.rollback();
190
200
  break;
191
201
  default:
192
- writer.append(json, msg);
202
+ writer.append(json, msg, storedChange);
193
203
  break;
194
204
  }
195
- observer?.messageProcessed(change, commit, performance.now() - startedAt);
205
+ observer?.messageProcessed(change, commit, startedAt === void 0 ? void 0 : performance.now() - startedAt);
196
206
  if (commit !== null && commit.stats.cookieMutations.length > 0) observer?.cookiesMutated(commit.stats.cookieMutations, readCookieRowCounts(db));
197
207
  if (commit !== null) this.#opts.onCommit?.(commit.watermark);
208
+ if (type === "commit" || type === "rollback") this.#validateTransaction = false;
198
209
  } catch (e) {
199
- observer?.messageFailed(change, e, performance.now() - startedAt);
210
+ observer?.messageFailed(change, e, startedAt === void 0 ? void 0 : performance.now() - startedAt);
211
+ this.#validateTransaction = false;
200
212
  if (isConstraintViolation(e)) observer?.invariantFailure("SQLite change-log insert violated a constraint, which means a transaction above the resume watermark was not truncated", {
201
213
  tag: msg.tag,
202
214
  error: String(e)
@@ -204,12 +216,17 @@ var SQLiteChangeLogWriter = class {
204
216
  this.#failSoft("writing to the SQLite change log", e);
205
217
  }
206
218
  }
219
+ #isSelectedForShadowValidation(watermark) {
220
+ const percent = this.#shadowValidationPercent;
221
+ return percent >= 100 || percent > 0 && h32(`${this.#opts.identity.generation}/${this.#opts.identity.replicaID ?? ""}:${watermark}`) % 100 < percent;
222
+ }
207
223
  /**
208
224
  * Discards the open transaction, if any. Called when the change stream is
209
225
  * interrupted mid-transaction.
210
226
  */
211
227
  abort() {
212
228
  const writer = this.#writer;
229
+ this.#validateTransaction = false;
213
230
  if (this.#disabled || writer === void 0) return;
214
231
  this.#observer?.abort();
215
232
  try {
@@ -220,6 +237,7 @@ var SQLiteChangeLogWriter = class {
220
237
  }
221
238
  close() {
222
239
  this.#disabled = true;
240
+ this.#validateTransaction = false;
223
241
  this.#writer = void 0;
224
242
  this.#observer = void 0;
225
243
  const db = this.#db;
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-change-log-writer.js","names":["#lc","#opts","#now","#disabled","#db","#observer","#reconcile","#resumeFromLog","#writer","#failSoft"],"sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-writer.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {SqliteError} from '@rocicorp/zero-sqlite3';\nimport {must} from '../../../../shared/src/must.ts';\nimport type {Database} from '../../../../zqlite/src/db.ts';\nimport {\n isSQLiteCorruption,\n logSQLiteCorruptionDiagnostics,\n} from '../../db/sqlite-corruption.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport {\n readCookieRowCounts,\n readCookies,\n} from '../replicator/change-log-cookies.ts';\nimport {\n ChangeLogRebuildRequired,\n changeLogFileName,\n changeLogWipeReason,\n deleteChangeLogDB,\n openChangeLogDBForWriting,\n readChangeLogHead,\n rebuildChangeLogDBForWriting,\n reconcileChangeLog,\n type ChangeLogAnchor,\n type ChangeLogIdentity,\n type ChangeLogResumePoint,\n type ReconcileResult,\n} from '../replicator/change-log-db.ts';\nimport {ChangeLogStreamWriter} from '../replicator/change-log-stream-writer.ts';\nimport {\n getSQLiteChangeLogInfo,\n logSQLiteChangeLogStartup,\n recordSQLiteChangeLogReconcile,\n SQLiteChangeLogObserver,\n type ChangeLogCommit,\n type SQLiteChangeLogObservabilityState,\n} from '../replicator/sqlite-change-log-observability.ts';\n\nexport type SQLiteChangeLogWriterOptions = {\n /** The replica file the log is named after, i.e. `${replicaFile}-change-log`. */\n replicaFile: string;\n /** The replica whose contents this log belongs to. */\n identity: ChangeLogIdentity;\n /**\n * Called after each transaction's log commit, with the log's new head. This\n * is what releases the catchup barrier: after the writer moved in-process,\n * the head advances here rather than at a subscriber's ACK.\n */\n onCommit?: ((watermark: string) => void) | undefined;\n /**\n * Called when the writer disables itself and deletes the file. The reader is\n * cached on the service, so a delete that leaves it open would leave an\n * in-process reader serving an unlinked inode while every new open sees\n * nothing.\n */\n onDisabled?: (() => void) | undefined;\n /** Called when reconciliation replaces the file, to close cached readers. */\n onRebuilt?: (() => void) | undefined;\n /** Overridable for tests. */\n now?: (() => number) | undefined;\n};\n\n/**\n * Appends the canonical change stream to the SQLite change log, from the\n * change-streamer's own stream loop.\n *\n * ### Ordering\n *\n * The log's commit for a transaction precedes the forward of that transaction's\n * `commit` message, and nothing `await`s in between. Everything that can\n * advance the watermark the stream would resume from — the storer's Postgres\n * commit, `lastWatermark`, the upstream ACK — runs strictly after it, so the\n * log's head is always at or above the resume point. A *hole* is therefore\n * unreachable at any crash timing, and a *phantom* is the normal state, which is\n * what makes truncate-above the whole of reconciliation.\n *\n * ### Fail-soft\n *\n * The log is a catchup cache: nothing in it is not either already durable in the\n * replica's litestream backup or re-derivable from the replication slot. A write\n * failure must therefore not stop replication for the shard. Corruption is\n * rebuilt once at open; any other write error — a full disk, an I/O error, a\n * permissions problem — logs, disables the writer, deletes the file, closes the\n * reader, and lets the stream continue. The cost is catchup reach, and reach is\n * recoverable: `too-old` sends a subscriber to a restore.\n *\n * One carve-out keeps that from hiding the bug class this topology introduces. A\n * missed truncate-above surfaces as `SQLITE_CONSTRAINT` on the writer's plain\n * `INSERT`, which is \"any write error other than corruption\" and would be\n * absorbed quietly. It is counted as an invariant failure and alerted on before\n * failing soft.\n */\nexport class SQLiteChangeLogWriter {\n readonly #lc: LogContext;\n readonly #opts: SQLiteChangeLogWriterOptions;\n readonly #now: () => number;\n\n #db: Database | undefined;\n #writer: ChangeLogStreamWriter | undefined;\n #observer: SQLiteChangeLogObserver | undefined;\n #disabled = false;\n\n constructor(lc: LogContext, opts: SQLiteChangeLogWriterOptions) {\n this.#lc = lc.withContext('component', 'sqlite-change-log-writer');\n this.#opts = opts;\n this.#now = opts.now ?? Date.now;\n }\n\n /** False once the writer has failed soft, or after {@link close}. */\n get enabled(): boolean {\n return !this.#disabled;\n }\n\n /**\n * The writer's own connection, which is the one purge runs on (§3.3): one\n * process appends *and* deletes, on one handle, with no second writable\n * connection and no cross-process barrier. `undefined` until the stream\n * loop's first reconcile creates the log, and again once the writer has\n * failed soft and deleted the file — both of which a purge cycle treats as\n * \"skip\", not as an error.\n */\n get connection(): Database | undefined {\n return this.#db;\n }\n\n /** Exposed for tests and for the integration assertions on the metrics. */\n state(): SQLiteChangeLogObservabilityState | undefined {\n return this.#observer?.state();\n }\n\n /**\n * Opens the log if necessary and reconciles it with the Postgres resume\n * point. Returns the resume point stored in the log.\n *\n * Returns `undefined` when the writer is disabled. The caller must then use\n * another source.\n *\n * This runs for every connection because Postgres supplies a new resume point\n * after each reconnect. The log can contain transactions above that point.\n * Reconciliation removes them before Postgres sends them again.\n *\n * `resumeFrom` includes cookies because the cookies must match the resume\n * watermark.\n */\n reconcile(\n resumeFrom: ChangeLogResumePoint,\n ): ChangeLogResumePoint | undefined {\n return this.#reconcile(() => resumeFrom);\n }\n\n /**\n * Reconciles the log without a Postgres resume point.\n *\n * A valid log uses its own head and cookies. A new or invalid log uses the\n * replica values returned by `seed`.\n */\n reconcileFromLog(\n seed: () => ChangeLogResumePoint,\n ): ChangeLogResumePoint | undefined {\n return this.#reconcile(db => this.#resumeFromLog(db, seed));\n }\n\n #reconcile(\n resolve: (db: Database) => ChangeLogResumePoint,\n ): ChangeLogResumePoint | undefined {\n if (this.#disabled) {\n return undefined;\n }\n const {replicaFile, identity} = this.#opts;\n const nowMs = this.#now();\n const anchor = (db: Database): ChangeLogAnchor => ({\n ...resolve(db),\n identity,\n nowMs,\n });\n try {\n const db = this.#db;\n if (db === undefined) {\n const opened = openChangeLogDBForWriting(this.#lc, replicaFile, anchor);\n this.#db = opened.db;\n if (opened.replacedFile) {\n this.#opts.onRebuilt?.();\n }\n recordSQLiteChangeLogReconcile(this.#lc, opened.result);\n const info = getSQLiteChangeLogInfo(opened.db);\n logSQLiteChangeLogStartup(\n this.#lc,\n changeLogFileName(replicaFile),\n info,\n );\n this.#observer = new SQLiteChangeLogObserver(this.#lc, info);\n } else {\n const resolved = anchor(db);\n let result: ReconcileResult;\n try {\n result = reconcileChangeLog(this.#lc, db, resolved, {\n rebuildInsteadOfUnboundedWork: true,\n });\n } catch (e) {\n if (!(e instanceof ChangeLogRebuildRequired)) {\n throw e;\n }\n // Close every handle to the old inode before unlinking it. The writer\n // is reconstructed below against the replacement connection.\n this.#writer = undefined;\n this.#db = undefined;\n db.close();\n this.#opts.onRebuilt?.();\n const rebuilt = rebuildChangeLogDBForWriting(\n this.#lc,\n replicaFile,\n resolved,\n e,\n );\n this.#db = rebuilt.db;\n result = rebuilt.result;\n }\n recordSQLiteChangeLogReconcile(this.#lc, result);\n if (result.action !== 'none') {\n this.#observer?.reconciled(\n getSQLiteChangeLogInfo(\n must(this.#db, 'the SQLite change log is not open'),\n ),\n );\n }\n }\n // A reseed drops and recreates the stream table, so the append statements\n // are prepared fresh against whatever the reconciliation left behind.\n const reconciledDB = must(this.#db, 'the SQLite change log is not open');\n this.#writer = new ChangeLogStreamWriter(\n new StatementRunner(reconciledDB),\n );\n // Read both values after reconciliation. A truncate or reseed updates the\n // head and cookies in the same transaction.\n return {\n resumeWatermark: must(\n readChangeLogHead(reconciledDB),\n 'the SQLite change log has no head after reconciliation',\n ),\n cookies: readCookies(reconciledDB),\n };\n } catch (e) {\n this.#failSoft('reconciling the SQLite change log', e);\n return undefined;\n }\n }\n\n /**\n * Returns the resume point supplied by the log.\n *\n * A valid log uses its own head and cookies. A log that must be recreated uses\n * the replica values returned by `seed`.\n */\n #resumeFromLog(\n db: Database,\n seed: () => ChangeLogResumePoint,\n ): ChangeLogResumePoint {\n if (changeLogWipeReason(db, this.#opts.identity) !== undefined) {\n return seed();\n }\n const head = readChangeLogHead(db);\n // An empty log has no resume point, so use the replica seed.\n return head === null\n ? seed()\n : {resumeWatermark: head, cookies: readCookies(db)};\n }\n\n /**\n * Writes one stream message, committing at transaction boundaries. Never\n * throws: a write failure disables the writer instead of failing the stream.\n */\n write(change: ChangeStreamData, json: string): void {\n const writer = this.#writer;\n const db = this.#db;\n if (this.#disabled || writer === undefined || db === undefined) {\n return;\n }\n const observer = this.#observer;\n observer?.messageReceived(change, json);\n const [type, msg] = change;\n const startedAt = performance.now();\n try {\n let commit: ChangeLogCommit | null = null;\n switch (type) {\n case 'begin':\n writer.begin(change[2].commitWatermark, json);\n break;\n case 'commit': {\n const {watermark} = change[2];\n const commitStart = performance.now();\n const stats = writer.commit(watermark, json, this.#now());\n commit = {\n watermark,\n stats,\n logCommitMs: performance.now() - commitStart,\n };\n break;\n }\n case 'rollback':\n writer.rollback();\n break;\n default:\n writer.append(json, msg);\n break;\n }\n observer?.messageProcessed(change, commit, performance.now() - startedAt);\n if (commit !== null && commit.stats.cookieMutations.length > 0) {\n // Only re-counted when a schema change actually moved the cookie jar,\n // which keeps the count off the forward path of every other\n // transaction. The counts are read after the commit, so they are what\n // a reader of the file would see.\n observer?.cookiesMutated(\n commit.stats.cookieMutations,\n readCookieRowCounts(db),\n );\n }\n if (commit !== null) {\n // The head advanced, and it is durable. Releasing the barrier here is\n // what makes its poll interval a backstop rather than the mechanism.\n this.#opts.onCommit?.(commit.watermark);\n }\n } catch (e) {\n observer?.messageFailed(change, e, performance.now() - startedAt);\n if (isConstraintViolation(e)) {\n // Reconciliation should have truncated whatever this collided with.\n // Report it as an invariant failure before failing soft, so that a\n // truncate-above bug cannot look like a lost cache.\n observer?.invariantFailure(\n 'SQLite change-log insert violated a constraint, which means a ' +\n 'transaction above the resume watermark was not truncated',\n {tag: msg.tag, error: String(e)},\n );\n }\n this.#failSoft('writing to the SQLite change log', e);\n }\n }\n\n /**\n * Discards the open transaction, if any. Called when the change stream is\n * interrupted mid-transaction.\n */\n abort(): void {\n const writer = this.#writer;\n if (this.#disabled || writer === undefined) {\n return;\n }\n this.#observer?.abort();\n try {\n writer.rollback();\n } catch (e) {\n this.#failSoft('rolling back the SQLite change log', e);\n }\n }\n\n close(): void {\n this.#disabled = true;\n this.#writer = undefined;\n this.#observer = undefined;\n const db = this.#db;\n this.#db = undefined;\n try {\n db?.close();\n } catch (e) {\n this.#lc.warn?.('error closing the SQLite change log', e);\n }\n }\n\n /**\n * Logs, disables the writer, deletes the file, and closes the reader, leaving\n * replication running. Disabling is for the life of the process: a log that\n * failed a write is not re-opened until the task restarts, so the behavior a\n * subscriber sees is the same as `sqliteChangeLogMode=off`.\n */\n #failSoft(what: string, e: unknown): void {\n const file = changeLogFileName(this.#opts.replicaFile);\n if (isSQLiteCorruption(e)) {\n // Open-time corruption is rebuilt by openChangeLogDBForWriting. Reaching\n // here means the file went bad under an open handle, which the same\n // delete-and-reseed answers -- on the next process start rather than now.\n logSQLiteCorruptionDiagnostics(this.#lc, 'change-log', file, e);\n }\n this.#lc.error?.(\n `error ${what}: disabling the writer and deleting ${file}. ` +\n `Replication continues; catchup reach is lost until this task restarts.`,\n e,\n );\n this.close();\n try {\n deleteChangeLogDB(this.#opts.replicaFile);\n } catch (deleteError) {\n this.#lc.error?.(`unable to delete ${file}`, deleteError);\n }\n this.#opts.onDisabled?.();\n }\n}\n\nfunction isConstraintViolation(e: unknown): boolean {\n return e instanceof SqliteError && e.code.startsWith('SQLITE_CONSTRAINT');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sqlite-change-log-writer.js","names":["#lc","#opts","#now","#shadowValidationPercent","#disabled","#db","#observer","#reconcile","#resumeFromLog","#writer","#failSoft","#validateTransaction","#isSelectedForShadowValidation"],"sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-writer.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {SqliteError} from '@rocicorp/zero-sqlite3';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {h32} from '../../../../shared/src/hash.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport type {Database} from '../../../../zqlite/src/db.ts';\nimport {\n isSQLiteCorruption,\n logSQLiteCorruptionDiagnostics,\n} from '../../db/sqlite-corruption.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport {\n readCookieRowCounts,\n readCookies,\n} from '../replicator/change-log-cookies.ts';\nimport {\n ChangeLogRebuildRequired,\n changeLogFileName,\n changeLogWipeReason,\n deleteChangeLogDB,\n openChangeLogDBForWriting,\n readChangeLogHead,\n rebuildChangeLogDBForWriting,\n reconcileChangeLog,\n type ChangeLogAnchor,\n type ChangeLogIdentity,\n type ChangeLogResumePoint,\n type ReconcileResult,\n} from '../replicator/change-log-db.ts';\nimport {ChangeLogStreamWriter} from '../replicator/change-log-stream-writer.ts';\nimport {\n getSQLiteChangeLogInfo,\n logSQLiteChangeLogStartup,\n recordSQLiteChangeLogReconcile,\n SQLiteChangeLogObserver,\n type ChangeLogCommit,\n type SQLiteChangeLogObservabilityState,\n} from '../replicator/sqlite-change-log-observability.ts';\nimport {extractChangeSubstring} from './change-log-codec.ts';\n\nexport type SQLiteChangeLogWriterOptions = {\n /** The replica file the log is named after, i.e. `${replicaFile}-change-log`. */\n replicaFile: string;\n /** The replica whose contents this log belongs to. */\n identity: ChangeLogIdentity;\n /**\n * Called after each transaction's log commit, with the log's new head. This\n * is what releases the catchup barrier: after the writer moved in-process,\n * the head advances here rather than at a subscriber's ACK.\n */\n onCommit?: ((watermark: string) => void) | undefined;\n /**\n * Called when the writer disables itself and deletes the file. The reader is\n * cached on the service, so a delete that leaves it open would leave an\n * in-process reader serving an unlinked inode while every new open sees\n * nothing.\n */\n onDisabled?: (() => void) | undefined;\n /** Called when reconciliation replaces the file, to close cached readers. */\n onRebuilt?: (() => void) | undefined;\n /** Overridable for tests. */\n now?: (() => number) | undefined;\n /**\n * Percentage of transactions that retain the rollout-only per-message\n * timing and source-vs-stored hash validation. Defaults to 1.\n */\n shadowValidationPercent?: number | undefined;\n};\n\nconst DEFAULT_SHADOW_VALIDATION_PERCENT = 1;\n\n/**\n * Appends the canonical change stream to the SQLite change log, from the\n * change-streamer's own stream loop.\n *\n * ### Ordering\n *\n * The log's commit for a transaction precedes the forward of that transaction's\n * `commit` message, and nothing `await`s in between. Everything that can\n * advance the watermark the stream would resume from — the storer's Postgres\n * commit, `lastWatermark`, the upstream ACK — runs strictly after it, so the\n * log's head is always at or above the resume point. A *hole* is therefore\n * unreachable at any crash timing, and a *phantom* is the normal state, which is\n * what makes truncate-above the whole of reconciliation.\n *\n * ### Fail-soft\n *\n * The log is a catchup cache: nothing in it is not either already durable in the\n * replica's litestream backup or re-derivable from the replication slot. A write\n * failure must therefore not stop replication for the shard. Corruption is\n * rebuilt once at open; any other write error — a full disk, an I/O error, a\n * permissions problem — logs, disables the writer, deletes the file, closes the\n * reader, and lets the stream continue. The cost is catchup reach, and reach is\n * recoverable: `too-old` sends a subscriber to a restore.\n *\n * One carve-out keeps that from hiding the bug class this topology introduces. A\n * missed truncate-above surfaces as `SQLITE_CONSTRAINT` on the writer's plain\n * `INSERT`, which is \"any write error other than corruption\" and would be\n * absorbed quietly. It is counted as an invariant failure and alerted on before\n * failing soft.\n */\nexport class SQLiteChangeLogWriter {\n readonly #lc: LogContext;\n readonly #opts: SQLiteChangeLogWriterOptions;\n readonly #now: () => number;\n readonly #shadowValidationPercent: number;\n\n #db: Database | undefined;\n #writer: ChangeLogStreamWriter | undefined;\n #observer: SQLiteChangeLogObserver | undefined;\n #disabled = false;\n #validateTransaction = false;\n\n constructor(lc: LogContext, opts: SQLiteChangeLogWriterOptions) {\n this.#lc = lc.withContext('component', 'sqlite-change-log-writer');\n this.#opts = opts;\n this.#now = opts.now ?? Date.now;\n this.#shadowValidationPercent =\n opts.shadowValidationPercent ?? DEFAULT_SHADOW_VALIDATION_PERCENT;\n assert(\n Number.isInteger(this.#shadowValidationPercent) &&\n this.#shadowValidationPercent >= 0 &&\n this.#shadowValidationPercent <= 100,\n 'SQLite change-log shadow validation percentage must be an integer between 0 and 100',\n );\n }\n\n /** False once the writer has failed soft, or after {@link close}. */\n get enabled(): boolean {\n return !this.#disabled;\n }\n\n /**\n * The writer's own connection, which is the one purge runs on (§3.3): one\n * process appends *and* deletes, on one handle, with no second writable\n * connection and no cross-process barrier. `undefined` until the stream\n * loop's first reconcile creates the log, and again once the writer has\n * failed soft and deleted the file — both of which a purge cycle treats as\n * \"skip\", not as an error.\n */\n get connection(): Database | undefined {\n return this.#db;\n }\n\n /** Exposed for tests and for the integration assertions on the metrics. */\n state(): SQLiteChangeLogObservabilityState | undefined {\n return this.#observer?.state();\n }\n\n /**\n * Opens the log if necessary and reconciles it with the Postgres resume\n * point. Returns the resume point stored in the log.\n *\n * Returns `undefined` when the writer is disabled. The caller must then use\n * another source.\n *\n * This runs for every connection because Postgres supplies a new resume point\n * after each reconnect. The log can contain transactions above that point.\n * Reconciliation removes them before Postgres sends them again.\n *\n * `resumeFrom` includes cookies because the cookies must match the resume\n * watermark.\n */\n reconcile(\n resumeFrom: ChangeLogResumePoint,\n ): ChangeLogResumePoint | undefined {\n return this.#reconcile(() => resumeFrom);\n }\n\n /**\n * Reconciles the log without a Postgres resume point.\n *\n * A valid log uses its own head and cookies. A new or invalid log uses the\n * replica values returned by `seed`.\n */\n reconcileFromLog(\n seed: () => ChangeLogResumePoint,\n ): ChangeLogResumePoint | undefined {\n return this.#reconcile(db => this.#resumeFromLog(db, seed));\n }\n\n #reconcile(\n resolve: (db: Database) => ChangeLogResumePoint,\n ): ChangeLogResumePoint | undefined {\n if (this.#disabled) {\n return undefined;\n }\n const {replicaFile, identity} = this.#opts;\n const nowMs = this.#now();\n const anchor = (db: Database): ChangeLogAnchor => ({\n ...resolve(db),\n identity,\n nowMs,\n });\n try {\n const db = this.#db;\n if (db === undefined) {\n const opened = openChangeLogDBForWriting(this.#lc, replicaFile, anchor);\n this.#db = opened.db;\n if (opened.replacedFile) {\n this.#opts.onRebuilt?.();\n }\n recordSQLiteChangeLogReconcile(this.#lc, opened.result);\n const info = getSQLiteChangeLogInfo(opened.db);\n logSQLiteChangeLogStartup(\n this.#lc,\n changeLogFileName(replicaFile),\n info,\n );\n this.#observer = new SQLiteChangeLogObserver(this.#lc, info);\n } else {\n const resolved = anchor(db);\n let result: ReconcileResult;\n try {\n result = reconcileChangeLog(this.#lc, db, resolved, {\n rebuildInsteadOfUnboundedWork: true,\n });\n } catch (e) {\n if (!(e instanceof ChangeLogRebuildRequired)) {\n throw e;\n }\n // Close every handle to the old inode before unlinking it. The writer\n // is reconstructed below against the replacement connection.\n this.#writer = undefined;\n this.#db = undefined;\n db.close();\n this.#opts.onRebuilt?.();\n const rebuilt = rebuildChangeLogDBForWriting(\n this.#lc,\n replicaFile,\n resolved,\n e,\n );\n this.#db = rebuilt.db;\n result = rebuilt.result;\n }\n recordSQLiteChangeLogReconcile(this.#lc, result);\n if (result.action !== 'none') {\n this.#observer?.reconciled(\n getSQLiteChangeLogInfo(\n must(this.#db, 'the SQLite change log is not open'),\n ),\n );\n }\n }\n // A reseed drops and recreates the stream table, so the append statements\n // are prepared fresh against whatever the reconciliation left behind.\n const reconciledDB = must(this.#db, 'the SQLite change log is not open');\n this.#writer = new ChangeLogStreamWriter(\n new StatementRunner(reconciledDB),\n );\n // Read both values after reconciliation. A truncate or reseed updates the\n // head and cookies in the same transaction.\n return {\n resumeWatermark: must(\n readChangeLogHead(reconciledDB),\n 'the SQLite change log has no head after reconciliation',\n ),\n cookies: readCookies(reconciledDB),\n };\n } catch (e) {\n this.#failSoft('reconciling the SQLite change log', e);\n return undefined;\n }\n }\n\n /**\n * Returns the resume point supplied by the log.\n *\n * A valid log uses its own head and cookies. A log that must be recreated uses\n * the replica values returned by `seed`.\n */\n #resumeFromLog(\n db: Database,\n seed: () => ChangeLogResumePoint,\n ): ChangeLogResumePoint {\n if (changeLogWipeReason(db, this.#opts.identity) !== undefined) {\n return seed();\n }\n const head = readChangeLogHead(db);\n // An empty log has no resume point, so use the replica seed.\n return head === null\n ? seed()\n : {resumeWatermark: head, cookies: readCookies(db)};\n }\n\n /**\n * Writes one stream message, committing at transaction boundaries. Never\n * throws: a write failure disables the writer instead of failing the stream.\n */\n write(\n change: ChangeStreamData,\n json: string,\n storedChange = extractChangeSubstring(json, change[1].tag),\n ): void {\n const writer = this.#writer;\n const db = this.#db;\n if (this.#disabled || writer === undefined || db === undefined) {\n return;\n }\n const [type, msg] = change;\n if (type === 'begin') {\n this.#validateTransaction = this.#isSelectedForShadowValidation(\n change[2].commitWatermark,\n );\n }\n const validateTransaction = this.#validateTransaction;\n const observer = this.#observer;\n observer?.messageReceived(change, json, validateTransaction, storedChange);\n const startedAt = validateTransaction ? performance.now() : undefined;\n try {\n let commit: ChangeLogCommit | null = null;\n switch (type) {\n case 'begin':\n writer.begin(\n change[2].commitWatermark,\n json,\n storedChange,\n validateTransaction,\n );\n break;\n case 'commit': {\n const {watermark} = change[2];\n const commitStart = performance.now();\n const stats = writer.commit(\n watermark,\n json,\n this.#now(),\n storedChange,\n );\n commit = {\n watermark,\n stats,\n logCommitMs: performance.now() - commitStart,\n };\n break;\n }\n case 'rollback':\n writer.rollback();\n break;\n default:\n writer.append(json, msg, storedChange);\n break;\n }\n observer?.messageProcessed(\n change,\n commit,\n startedAt === undefined ? undefined : performance.now() - startedAt,\n );\n if (commit !== null && commit.stats.cookieMutations.length > 0) {\n // Only re-counted when a schema change actually moved the cookie jar,\n // which keeps the count off the forward path of every other\n // transaction. The counts are read after the commit, so they are what\n // a reader of the file would see.\n observer?.cookiesMutated(\n commit.stats.cookieMutations,\n readCookieRowCounts(db),\n );\n }\n if (commit !== null) {\n // The head advanced, and it is durable. Releasing the barrier here is\n // what makes its poll interval a backstop rather than the mechanism.\n this.#opts.onCommit?.(commit.watermark);\n }\n if (type === 'commit' || type === 'rollback') {\n this.#validateTransaction = false;\n }\n } catch (e) {\n observer?.messageFailed(\n change,\n e,\n startedAt === undefined ? undefined : performance.now() - startedAt,\n );\n this.#validateTransaction = false;\n if (isConstraintViolation(e)) {\n // Reconciliation should have truncated whatever this collided with.\n // Report it as an invariant failure before failing soft, so that a\n // truncate-above bug cannot look like a lost cache.\n observer?.invariantFailure(\n 'SQLite change-log insert violated a constraint, which means a ' +\n 'transaction above the resume watermark was not truncated',\n {tag: msg.tag, error: String(e)},\n );\n }\n this.#failSoft('writing to the SQLite change log', e);\n }\n }\n\n #isSelectedForShadowValidation(watermark: string): boolean {\n const percent = this.#shadowValidationPercent;\n return (\n percent >= 100 ||\n (percent > 0 &&\n h32(\n `${this.#opts.identity.generation}/${this.#opts.identity.replicaID ?? ''}:${watermark}`,\n ) %\n 100 <\n percent)\n );\n }\n\n /**\n * Discards the open transaction, if any. Called when the change stream is\n * interrupted mid-transaction.\n */\n abort(): void {\n const writer = this.#writer;\n this.#validateTransaction = false;\n if (this.#disabled || writer === undefined) {\n return;\n }\n this.#observer?.abort();\n try {\n writer.rollback();\n } catch (e) {\n this.#failSoft('rolling back the SQLite change log', e);\n }\n }\n\n close(): void {\n this.#disabled = true;\n this.#validateTransaction = false;\n this.#writer = undefined;\n this.#observer = undefined;\n const db = this.#db;\n this.#db = undefined;\n try {\n db?.close();\n } catch (e) {\n this.#lc.warn?.('error closing the SQLite change log', e);\n }\n }\n\n /**\n * Logs, disables the writer, deletes the file, and closes the reader, leaving\n * replication running. Disabling is for the life of the process: a log that\n * failed a write is not re-opened until the task restarts, so the behavior a\n * subscriber sees is the same as `sqliteChangeLogMode=off`.\n */\n #failSoft(what: string, e: unknown): void {\n const file = changeLogFileName(this.#opts.replicaFile);\n if (isSQLiteCorruption(e)) {\n // Open-time corruption is rebuilt by openChangeLogDBForWriting. Reaching\n // here means the file went bad under an open handle, which the same\n // delete-and-reseed answers -- on the next process start rather than now.\n logSQLiteCorruptionDiagnostics(this.#lc, 'change-log', file, e);\n }\n this.#lc.error?.(\n `error ${what}: disabling the writer and deleting ${file}. ` +\n `Replication continues; catchup reach is lost until this task restarts.`,\n e,\n );\n this.close();\n try {\n deleteChangeLogDB(this.#opts.replicaFile);\n } catch (deleteError) {\n this.#lc.error?.(`unable to delete ${file}`, deleteError);\n }\n this.#opts.onDisabled?.();\n }\n}\n\nfunction isConstraintViolation(e: unknown): boolean {\n return e instanceof SqliteError && e.code.startsWith('SQLITE_CONSTRAINT');\n}\n"],"mappings":"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"}
@@ -9,7 +9,7 @@ import type { UpstreamStatusMessage } from '../change-source/protocol/current/st
9
9
  import { type CookieSet } from '../replicator/change-log-cookies.ts';
10
10
  import type { ReplicatorMode } from '../replicator/replicator.ts';
11
11
  import type { Service } from '../service.ts';
12
- import { type ChangeLogEntry } from './change-log-codec.ts';
12
+ import { type ChangeLogEntry, type SerializedChangeStreamData } from './change-log-codec.ts';
13
13
  import type { Subscriber } from './subscriber.ts';
14
14
  /**
15
15
  * Factory for creating dynamic connection pools used by the Storer to
@@ -106,7 +106,7 @@ export declare class Storer implements Service {
106
106
  /**
107
107
  * @returns The JSON stringified stream message to be sent downstream.
108
108
  */
109
- store(watermark: string, data: ChangeStreamData): string;
109
+ store(watermark: string, data: ChangeStreamData, serialized?: SerializedChangeStreamData): string;
110
110
  abort(): void;
111
111
  catchup(subscriber: Subscriber, mode: ReplicatorMode): void;
112
112
  readyForMore(): Promise<void> | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"storer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/storer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"storer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/storer.ts"],"names":[],"mappings":"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"}
@@ -16,7 +16,7 @@ import { Queue } from "../../../../shared/src/queue.js";
16
16
  import { AutoResetSignal, markResetRequired } from "./schema/tables.js";
17
17
  import { TransactionPool, sharedSnapshot } from "../../db/transaction-pool.js";
18
18
  import { orTimeout } from "../../types/timeout.js";
19
- import { extractChangeSubstring, reconstructWatermarkedChange, serializeChangeStreamData } from "./change-log-codec.js";
19
+ import { reconstructWatermarkedChange, serializeChangeStreamDataWithChange } from "./change-log-codec.js";
20
20
  import { resolver } from "@rocicorp/resolver";
21
21
  import "postgres";
22
22
  import { getHeapStatistics } from "node:v8";
@@ -263,14 +263,15 @@ var Storer = class {
263
263
  /**
264
264
  * @returns The JSON stringified stream message to be sent downstream.
265
265
  */
266
- store(watermark, data) {
267
- const json = serializeChangeStreamData(data);
266
+ store(watermark, data, serialized = serializeChangeStreamDataWithChange(data)) {
267
+ const { json, change: storedChange } = serialized;
268
268
  this.#approximateQueuedBytes += json.length;
269
269
  const change = data[1];
270
270
  this.#queue.enqueue([
271
271
  "change",
272
272
  watermark,
273
273
  json,
274
+ storedChange,
274
275
  isDataChange(change) ? null : change
275
276
  ]);
276
277
  return json;
@@ -411,7 +412,7 @@ var Storer = class {
411
412
  }
412
413
  continue;
413
414
  }
414
- const [_, watermark, json, change] = msg;
415
+ const [_, watermark, json, storedChange, change] = msg;
415
416
  const tag = change?.tag;
416
417
  this.#approximateQueuedBytes -= json.length;
417
418
  if (tag === "begin") {
@@ -441,7 +442,7 @@ var Storer = class {
441
442
  watermark: tag === "commit" ? watermark : tx.preCommitWatermark,
442
443
  precommit: tag === "commit" ? tx.preCommitWatermark : null,
443
444
  pos: tx.pos,
444
- change: extractChangeSubstring(json, tag)
445
+ change: storedChange
445
446
  };
446
447
  if (change !== null && isSchemaChange(change)) {
447
448
  await this.#flushChangeLog(tx);