@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.
- package/out/replicache/src/kv/expo-sqlite/store.d.ts.map +1 -1
- package/out/replicache/src/kv/expo-sqlite/store.js +27 -4
- package/out/replicache/src/kv/expo-sqlite/store.js.map +1 -1
- package/out/replicache/src/kv/sqlite-store.d.ts +11 -0
- package/out/replicache/src/kv/sqlite-store.d.ts.map +1 -1
- package/out/replicache/src/kv/sqlite-store.js +43 -3
- package/out/replicache/src/kv/sqlite-store.js.map +1 -1
- package/out/shared/src/error.d.ts +11 -0
- package/out/shared/src/error.d.ts.map +1 -1
- package/out/shared/src/error.js +35 -1
- package/out/shared/src/error.js.map +1 -1
- package/out/shared/src/map.d.ts +7 -2
- package/out/shared/src/map.d.ts.map +1 -1
- package/out/shared/src/map.js.map +1 -1
- package/out/zero/package.js +2 -2
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
- package/out/zero-cache/src/config/normalize.js +7 -1
- package/out/zero-cache/src/config/normalize.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +9 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +21 -0
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +15 -6
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/main.d.ts.map +1 -1
- package/out/zero-cache/src/server/main.js +8 -1
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/mutator.js +2 -0
- package/out/zero-cache/src/server/mutator.js.map +1 -1
- package/out/zero-cache/src/server/reaper.d.ts.map +1 -1
- package/out/zero-cache/src/server/reaper.js +2 -0
- package/out/zero-cache/src/server/reaper.js.map +1 -1
- package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/server/replicator.js +2 -0
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/runner/zero-dispatcher.d.ts.map +1 -1
- package/out/zero-cache/src/server/runner/zero-dispatcher.js +3 -0
- package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
- package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/server/syncer.js +2 -0
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js +68 -8
- package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts +19 -4
- package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-log-codec.js +28 -8
- package/out/zero-cache/src/services/change-streamer/change-log-codec.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +3 -0
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +3 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +6 -0
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +86 -51
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.js +4 -4
- package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.d.ts +6 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js +27 -9
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.d.ts +2 -2
- package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js +6 -5
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/services/litestream/litestream-checkpointer.js +50 -23
- package/out/zero-cache/src/services/litestream/litestream-checkpointer.js.map +1 -1
- package/out/zero-cache/src/services/profz.d.ts +7 -0
- package/out/zero-cache/src/services/profz.d.ts.map +1 -0
- package/out/zero-cache/src/services/profz.js +99 -0
- package/out/zero-cache/src/services/profz.js.map +1 -0
- package/out/zero-cache/src/services/replicator/change-log-stream-writer.d.ts +5 -4
- package/out/zero-cache/src/services/replicator/change-log-stream-writer.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/change-log-stream-writer.js +20 -22
- package/out/zero-cache/src/services/replicator/change-log-stream-writer.js.map +1 -1
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.d.ts +6 -6
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js +19 -14
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts +17 -0
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +59 -0
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.d.ts +51 -0
- package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js +78 -0
- package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +18 -6
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +154 -6
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/processes.d.ts +24 -0
- package/out/zero-cache/src/types/processes.d.ts.map +1 -1
- package/out/zero-cache/src/types/processes.js +25 -1
- package/out/zero-cache/src/types/processes.js.map +1 -1
- package/out/zero-cache/src/types/profiler.d.ts +12 -0
- package/out/zero-cache/src/types/profiler.d.ts.map +1 -1
- package/out/zero-cache/src/types/profiler.js +46 -1
- package/out/zero-cache/src/types/profiler.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +12 -8
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-protocol/src/down.d.ts +27 -27
- package/out/zero-react/src/use-query.d.ts.map +1 -1
- package/out/zero-react/src/use-query.js +50 -3
- package/out/zero-react/src/use-query.js.map +1 -1
- package/out/zero-solid/src/use-query.js +1 -2
- package/out/zero-solid/src/use-query.js.map +1 -1
- package/out/zql/src/ivm/data.d.ts.map +1 -1
- package/out/zql/src/ivm/data.js +8 -3
- package/out/zql/src/ivm/data.js.map +1 -1
- package/out/zql/src/ivm/view-apply-change.js +6 -1
- package/out/zql/src/ivm/view-apply-change.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-streamer-service.js","names":["#lc","#shard","#changeDBProvider","#replicaVersion","#source","#storer","#forwarder","#reservations","#replicationStatusPublisher","#sqliteCatchupOptions","#changeLogWriter","#purgeScheduler","#comparator","#acker","#initializer","#readRouter","#autoReset","#state","#serving","#txCounter","#changeCounter","#transactionForwardDuration","#catchupRoutes","#reservationDemotions","#reservationConfirmDelays","#sqliteCatchup","#closeSQLiteCatchup","#purgeLock","#latestStatus","#latestLagReportCommitTimeMs","#lastForwardedCommitWatermark","#stream","#handleControlMessage","#recordForwardedTransactionBoundary","#selectSQLiteCatchup","#captureRequiredHead","#recordCatchupRoute","#confirmReservations","#backupWatermark","#requestSQLitePurge","#maybeSchedulePGPurge","#maybeScheduleSQLitePurge","#getChangeLogState","#pgPurgeScheduled","#pgPurgeRunning","#pgPurgedWatermark","#purgePGChangeLog","#sqlitePurgeScheduled","#sqlitePurgeRunning","#sqlitePurgeContinuation","#purgeSQLiteChangeLog","#getCleanupFloor","#loggedBehindWatermark","#currentTransactionCompletion","#transactionForwardStartedAt","#recordTransactionForwardDuration","#declineSQLiteCatchup","#openSQLiteCatchup","#changeLogUnavailableSince"],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-service.ts"],"sourcesContent":["import {getDefaultHighWaterMark} from 'node:stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {defu} from 'defu';\nimport postgres, {type Options, type PostgresType} from 'postgres';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseOrAbort} from '../../../../shared/src/promise-race.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {publishCriticalEvent} from '../../observability/events.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {min} from '../../types/lexi-version.ts';\nimport type {PostgresDB} from '../../types/pg.ts';\nimport type {ShardID} from '../../types/shards.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {\n ChangeSource,\n ChangeStream,\n} from '../change-source/change-source.ts';\nimport {\n type ChangeStreamControl,\n type ChangeStreamData,\n type Rollback,\n} from '../change-source/protocol/current/downstream.ts';\nimport type {LitestreamVersion} from '../litestream/metrics.ts';\nimport {\n publishReplicationError,\n replicationStatusError,\n type ReplicationStatusPublisher,\n} from '../replicator/replication-status.ts';\nimport type {SubscriptionState} from '../replicator/schema/replication-state.ts';\nimport {\n DEFAULT_MAX_RETRY_DELAY_MS,\n RunningState,\n UnrecoverableError,\n} from '../running-state.ts';\nimport {\n ChangeLogInitializer,\n replicaInitializationSource,\n} from './change-log-initializer.ts';\nimport {\n type ChangeStreamerService,\n type Status,\n type SubscriberContext,\n type WatermarkedChange,\n} from './change-streamer.ts';\nimport * as ErrorType from './error-type-enum.ts';\nimport {Forwarder} from './forwarder.ts';\nimport {\n AutoResetSignal,\n ensureReplicationConfig,\n markResetRequired,\n} from './schema/tables.ts';\nimport {SnapshotReservations} from './snapshot-reservations.ts';\nimport type {SnapshotMessage} from './snapshot.ts';\nimport {\n SQLiteChangeLogCatchup,\n type SQLiteChangeLogCleanupGuard,\n} from './sqlite-change-log-catchup.ts';\nimport {\n SQLiteChangeLogComparator,\n type SQLiteChangeLogCompareOptions,\n} from './sqlite-change-log-comparator.ts';\nimport {\n SQLiteChangeLogPurgeScheduler,\n type PurgeContinuation,\n type SQLiteChangeLogPurgeSchedulerOptions,\n} from './sqlite-change-log-purge-scheduler.ts';\nimport {\n inspectSQLiteChangeLog,\n SQLiteChangeLogReadRouter,\n type ChangeLogReadRoute,\n type SQLiteChangeLogCoverage,\n} from './sqlite-change-log-read-router.ts';\nimport {SQLiteChangeLogReader} from './sqlite-change-log-reader.ts';\nimport {\n SQLiteChangeLogWriter,\n type SQLiteChangeLogWriterOptions,\n} from './sqlite-change-log-writer.ts';\nimport type {PostgresDBProvider} from './storer.ts';\nimport {\n Storer,\n type PurgeLock,\n type TuningOptions as StorerOptions,\n} from './storer.ts';\nimport {Subscriber} from './subscriber.ts';\nimport {UpstreamAcker} from './upstream-acker.ts';\n\nexport type BackupConfig = {\n backupURL: string;\n litestreamVersion: LitestreamVersion;\n};\n\nexport type SQLiteCatchupOptions = {\n /** The change-log database, i.e. `changeLogFileName(replicaFile)`. */\n changeLogFile: string;\n readBatchRows: number;\n barrierTimeoutMs: number;\n /**\n * Backstop for the change-log writer's ACK, which is what normally releases\n * the barrier. Defaults to the catchup coordinator's interval.\n */\n barrierPollIntervalMs?: number | undefined;\n /**\n * Optional policy hook used by focused tests. Production canary selection is\n * supplied by `sqliteChangeLogServe` and is stable by shard plus task ID.\n * Backup subscribers are rejected before either selector is invoked.\n */\n shouldUse?: ((ctx: SubscriberContext) => boolean) | undefined;\n /**\n * Overrides the purge scheduler's guard, for tests. When absent, the\n * service supplies the scheduler's real writer-serialized guard, or the\n * catchup's no-op default when purging is not configured.\n */\n cleanupGuard?: SQLiteChangeLogCleanupGuard | undefined;\n /**\n * How long the change log may be unavailable before declining to serve from\n * it is reported as a warning rather than a debug line. Defaults to\n * {@link DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS}.\n */\n notReadyWarnThresholdMs?: number | undefined;\n};\n\nexport type SQLiteChangeLogServeOptions = {\n /** Stable percentage of eligible serving tasks routed to SQLite. */\n readPercent: number;\n /**\n * Stable percentage of eligible serving tasks routed to a log that has not\n * yet aged through {@link retentionMs}. Zero keeps a reseeded log on PG for\n * that whole window; above zero it serves that share of tasks, and a\n * reservation it cannot cover is demoted rather than held pending.\n * Defaults to zero.\n */\n coldReadPercent?: number | undefined;\n /** The warm-up window a reseeded log ages through. */\n retentionMs: number;\n /** Injectable for deterministic breaker tests. */\n failureCooldownMs?: number | undefined;\n /** Injectable for deterministic warm-up and breaker tests. */\n now?: (() => number) | undefined;\n /** Injectable readiness inspection for service-level tests. */\n inspect?: (() => SQLiteChangeLogCoverage | undefined) | undefined;\n};\n\nexport type TuningOptions = StorerOptions & {\n flowControlConsensusTimeoutProportion: number;\n flowControlSlowSubscriberGracePeriodMs?: number | undefined;\n sqliteCatchup?: SQLiteCatchupOptions | undefined;\n /**\n * Supplied when `sqliteChangeLogMode != off`, i.e. this is the gate on the\n * change-log writer. Absent, nothing writes the log.\n */\n sqliteChangeLogWriter?:\n | Omit<\n SQLiteChangeLogWriterOptions,\n 'onCommit' | 'onDisabled' | 'onRebuilt'\n >\n | undefined;\n /**\n * Also supplied when `sqliteChangeLogMode != off`, and deliberately not\n * gated on the read path: in `write` mode no reader ever opens, and this is\n * the configuration the purge scheduler actually ships in. The scheduler\n * runs on the writer's own connection, so with the writer absent (mode\n * `off`) it is never constructed, and a leftover file on disk is left to\n * the replicator's cleanup.\n */\n sqliteChangeLogPurge?: SQLiteChangeLogPurgeSchedulerOptions | undefined;\n /**\n * Supplied in `compare` mode and later modes.\n * `replicaFile` enables the initialization comparison.\n * The other fields configure sampled catchup comparisons.\n * Both checks are advisory, and Postgres remains authoritative.\n * The comparator also requires the writer and catchup options.\n */\n sqliteChangeLogCompare?:\n | (SQLiteChangeLogCompareOptions & {replicaFile: string})\n | undefined;\n /** Supplied only in `serve` mode. A zero percentage keeps every read on PG. */\n sqliteChangeLogServe?: SQLiteChangeLogServeOptions | undefined;\n};\n\n/**\n * Performs initialization and schema migrations to initialize a ChangeStreamerImpl.\n */\nexport async function initializeStreamer(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n changeDB: PostgresDB,\n changeSource: ChangeSource,\n replicationStatusPublisher: ReplicationStatusPublisher,\n subscriptionState: SubscriptionState,\n backupConfig: BackupConfig | null,\n purgeLock: PurgeLock | null,\n autoReset: boolean,\n opts: TuningOptions,\n setTimeoutFn = setTimeout,\n): Promise<ChangeStreamerService> {\n await ensureReplicationConfig(\n lc,\n changeDB,\n subscriptionState,\n shard,\n autoReset,\n purgeLock ?? undefined,\n setTimeoutFn,\n );\n\n // Dynamically creates connection pools that the implementation uses to\n // isolate concurrent, long-running transactions. This works around a bug in\n // the postgres.js client where connections get swapped in certain conditions.\n //\n // https://github.com/porsager/postgres/issues/1204\n const changeDBProvider: PostgresDBProvider = (\n applicationName: string,\n max: number,\n ) =>\n postgres(\n defu(\n {max, connection: {['application_name']: applicationName}},\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 changeDB.options as unknown as Options<Record<string, PostgresType>>,\n ),\n ) as PostgresDB;\n\n const {replicaVersion} = subscriptionState;\n return new ChangeStreamerImpl(\n lc,\n shard,\n taskID,\n discoveryAddress,\n discoveryProtocol,\n changeDBProvider,\n replicaVersion,\n changeSource,\n replicationStatusPublisher,\n backupConfig,\n purgeLock,\n autoReset,\n opts,\n setTimeoutFn,\n );\n}\n\nconst REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS = 5000;\n\n// How long the change log may be unavailable before declining to serve from it\n// stops looking like normal startup ordering. Subscriptions can arrive before\n// the stream loop's first reconcile has created and seeded the log, so early\n// declines are expected. Still declining a minute later means the log is not\n// being written at all -- `sqliteChangeLogMode=off`, a writer that has failed\n// soft, or a path mismatch -- which is worth surfacing.\nconst DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS = 60_000;\n\n/**\n * Upstream-agnostic dispatch of messages in a {@link ChangeStreamMessage} to a\n * {@link Forwarder} and {@link Storer} to execute the forward-store-ack\n * procedure described in {@link ChangeStreamer}.\n *\n * ### Subscriber Catchup\n *\n * Connecting clients first need to be \"caught up\" to the current watermark\n * (from stored change log entries) before new entries are forwarded to\n * them. This is non-trivial because the replication stream may be in the\n * middle of a pending streamed Transaction for which some entries have\n * already been forwarded but are not yet committed to the store.\n *\n *\n * ```\n * ------------------------------- - - - - - - - - - - - - - - - - - - -\n * | Historic changes in storage | Pending (streamed) tx | Next tx\n * ------------------------------- - - - - - - - - - - - - - - - - - - -\n * Replication stream\n * > > > > > > > > >\n * ^ ---> required catchup ---> ^\n * Subscriber watermark Subscription begins\n * ```\n *\n * Preemptively buffering the changes of every pending transaction\n * would be wasteful and consume too much memory for large transactions.\n *\n * Instead, the streamer synchronously dispatches changes and subscriptions\n * to the {@link Forwarder} and the {@link Storer} such that the two\n * components are aligned as to where in the stream the subscription started.\n * The two components then coordinate catchup and handoff via the\n * {@link Subscriber} object with the following algorithm:\n *\n * * If the streamer is in the middle of a pending Transaction, the\n * Subscriber is \"queued\" on both the Forwarder and the Storer. In this\n * state, new changes are *not* forwarded to the Subscriber, and catchup\n * is not yet executed.\n * * Once the commit message for the pending Transaction is processed\n * by the Storer, it begins catchup on the Subscriber (with a READONLY\n * snapshot so that it does not block subsequent storage operations).\n * This catchup is thus guaranteed to load the change log entries of\n * that last Transaction.\n * * When the Forwarder processes that same commit message, it moves the\n * Subscriber from the \"queued\" to the \"active\" set of clients such that\n * the Subscriber begins receiving new changes, starting from the next\n * Transaction.\n * * The Subscriber does not forward those changes, however, if its catchup\n * is not complete. Until then, it buffers the changes in memory.\n * * Once catchup is complete, the buffered changes are immediately sent\n * and the Subscriber henceforth forwards changes as they are received.\n *\n * In the (common) case where the streamer is not in the middle of a pending\n * transaction when a subscription begins, the Storer begins catchup\n * immediately and the Forwarder directly adds the Subscriber to its active\n * set. However, the Subscriber still buffers any forwarded messages until\n * its catchup is complete.\n *\n * ### Watermarks and ordering\n *\n * The ChangeStreamerService depends on its {@link ChangeSource} to send\n * changes in contiguous [`begin`, `data` ..., `data`, `commit`] sequences\n * in commit order. This follows Postgres's Logical Replication Protocol\n * Message Flow:\n *\n * https://www.postgresql.org/docs/16/protocol-logical-replication.html#PROTOCOL-LOGICAL-MESSAGES-FLOW\n *\n * > The logical replication protocol sends individual transactions one by one.\n * > This means that all messages between a pair of Begin and Commit messages belong to the same transaction.\n *\n * In order to correctly replay (new) and filter (old) messages to subscribers\n * at different points in the replication stream, these changes must be assigned\n * watermarks such that they preserve the order in which they were received\n * from the ChangeSource.\n *\n * A previous implementation incorrectly derived these watermarks from the Postgres\n * Log Sequence Numbers (LSN) of each message. However, LSNs from concurrent,\n * non-conflicting transactions can overlap, which can result in a `begin` message\n * with an earlier LSN arriving after a `commit` message. For example, the\n * changes for these transactions:\n *\n * ```\n * LSN: 1 2 3 4 5 6 7 8 9 10\n * tx1: begin data data data commit\n * tx2: begin data data data commit\n * ```\n *\n * will arrive as:\n *\n * ```\n * begin1, data2, data4, data6, commit8, begin3, data5, data7, data9, commit10\n * ```\n *\n * Thus, LSN of non-commit messages are not suitable for tracking the sorting\n * order of the replication stream.\n *\n * Instead, the ChangeStreamer uses the following algorithm for deterministic\n * catchup and filtering of changes:\n *\n * * A `commit` message is assigned to a watermark corresponding to its LSN.\n * These are guaranteed to be in commit order by definition.\n *\n * * `begin` and `data` messages are assigned to the watermark of the\n * preceding `commit` (the previous transaction, or the replication\n * slot's starting LSN) plus 1. This guarantees that they will be sorted\n * after the previously commit transaction even if their LSNs came before it.\n * This is referred to as the `preCommitWatermark`.\n *\n * * In the ChangeLog DB, messages have a secondary sort column `pos`, which is\n * the position of the message within its transaction, with the `begin` message\n * starting at `0`. This guarantees that `begin` and `data` messages will be\n * fetched in the original ChangeSource order during catchup.\n *\n * `begin` and `data` messages share the same watermark, but this is sufficient for\n * Subscriber filtering because subscribers only know about the `commit` watermarks\n * exposed in the `Downstream` `Commit` message. The Subscriber object thus compares\n * the internal watermarks of the incoming messages against the commit watermark of\n * the caller, updating the watermark at every `Commit` message that is forwarded.\n *\n * ### Cleanup\n *\n * As mentioned in the {@link ChangeStreamer} documentation: \"the ChangeStreamer\n * uses a combination of [the \"initial\", i.e. backup-derived watermark and] ACK\n * responses from connected subscribers to determine the watermark up\n * to which it is safe to purge old change log entries.\"\n *\n * More concretely:\n *\n * * The `initial`, backup-derived watermark is the earliest to which cleanup\n * should ever happen.\n *\n * * However, it is possible for the replica backup to be *ahead* of a connected\n * subscriber; and if a network error causes that subscriber to retry from its\n * last watermark, the change streamer must support it.\n *\n * Thus, before cleaning up to an `initial` backup-derived watermark, the change\n * streamer first confirms that all connected subscribers have also passed\n * that watermark.\n */\nclass ChangeStreamerImpl implements ChangeStreamerService {\n readonly id: string;\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #changeDBProvider: PostgresDBProvider;\n readonly #replicaVersion: string;\n readonly #source: ChangeSource;\n readonly #storer: Storer;\n readonly #forwarder: Forwarder;\n readonly #reservations: SnapshotReservations | undefined;\n readonly #replicationStatusPublisher: ReplicationStatusPublisher;\n readonly #sqliteCatchupOptions: SQLiteCatchupOptions | undefined;\n readonly #changeLogWriter: SQLiteChangeLogWriter | undefined;\n readonly #purgeScheduler: SQLiteChangeLogPurgeScheduler | undefined;\n readonly #comparator: SQLiteChangeLogComparator | undefined;\n readonly #acker: UpstreamAcker;\n readonly #initializer: ChangeLogInitializer;\n readonly #readRouter: SQLiteChangeLogReadRouter | undefined;\n\n readonly #autoReset: boolean;\n readonly #state: RunningState;\n\n // Starting the (Postgres) ChangeStream results in killing the previous\n // Postgres subscriber, potentially creating a gap in which the old\n // change-streamer has shut down and the new change-streamer has not yet\n // been recognized as \"healthy\" (and thus does not get any requests).\n //\n // To minimize this gap, delay starting the ChangeStream until the first\n // request from a `serving` replicator, indicating that higher level\n // load-balancing / routing logic has begun routing requests to this task.\n readonly #serving = resolver();\n\n readonly #txCounter = getOrCreateCounter(\n 'replication',\n 'transactions',\n 'Count of replicated transactions',\n );\n readonly #changeCounter = getOrCreateCounter(\n 'replication',\n 'changes',\n 'Count of replicated changes (DML or DDL statements)',\n );\n // The number the SQLite change-log commit lands in. It is labeled by whether\n // the log is being written so that the cost of putting a commit on the\n // forward path is attributable rather than inferred.\n readonly #transactionForwardDuration = getOrCreateLatencyHistogram(\n 'replication',\n 'transaction_forward_duration',\n \"Time from receiving a transaction's `begin` to forwarding its `commit`, \" +\n 'i.e. the change-streamer half of forward-to-subscriber latency.',\n );\n readonly #catchupRoutes = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.catchup_routes',\n 'Catchup subscriptions by selected source and low-cardinality reason.',\n );\n readonly #reservationDemotions = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.reservation_demotions',\n 'Snapshot reservations demoted from SQLite to PG because the change ' +\n 'log could not cover the backup being restored. With PG retired ' +\n 'these followers have no fallback, so this is the rate at which one ' +\n 'would instead have to wait for a later backup.',\n );\n readonly #reservationConfirmDelays = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.reservation_confirm_delays',\n 'Snapshot reservations whose confirmation was deferred because the ' +\n \"selected source's change-log minimum was later than the backup \" +\n 'watermark. Counted once per reservation, by that source.',\n );\n\n #latestStatus: Status;\n #latestLagReportCommitTimeMs = 0;\n #backupWatermark: string | undefined;\n #pgPurgedWatermark: string = '';\n /**\n * The floor last logged as held back by a laggard. The level-triggered\n * retry re-evaluates every {@link CLEANUP_DELAY_MS}; logging only when the\n * blocking floor moves keeps a stuck subscriber from emitting the same\n * line indefinitely.\n */\n #loggedBehindWatermark: string | undefined;\n #sqlitePurgeContinuation: PurgeContinuation | undefined;\n #purgeLock: PurgeLock | null;\n // PG and SQLite intentionally own separate level-triggered loops. Neither\n // waits for, advances, or retries the other.\n #pgPurgeScheduled = false;\n #pgPurgeRunning = false;\n #sqlitePurgeScheduled = false;\n #sqlitePurgeRunning = false;\n #stream: ChangeStream | undefined;\n #sqliteCatchup: SQLiteChangeLogCatchup | undefined;\n #changeLogUnavailableSince: number | undefined;\n #lastForwardedCommitWatermark: string | undefined;\n #transactionForwardStartedAt: number | undefined;\n #currentTransactionCompletion:\n | Resolver<ForwardedTransactionCompletion>\n | undefined;\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n changeDBProvider: PostgresDBProvider,\n replicaVersion: string,\n source: ChangeSource,\n replicationStatusPublisher: ReplicationStatusPublisher,\n backupConfig: BackupConfig | null,\n initialPurgeLock: PurgeLock | null,\n autoReset: boolean,\n opts: TuningOptions,\n setTimeoutFn = setTimeout,\n ) {\n this.id = `change-streamer`;\n this.#lc = lc.withContext('component', 'change-streamer');\n this.#shard = shard;\n this.#changeDBProvider = changeDBProvider;\n this.#replicaVersion = replicaVersion;\n this.#source = source;\n this.#storer = new Storer(\n lc,\n shard,\n taskID,\n discoveryAddress,\n discoveryProtocol,\n changeDBProvider,\n replicaVersion,\n consumed => this.#acker.trackPgChangeLog(consumed[2].watermark),\n err => this.stop(err),\n opts,\n );\n this.#forwarder = new Forwarder(lc, {\n flowControlConsensusTimeoutProportion:\n opts.flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodMs:\n opts.flowControlSlowSubscriberGracePeriodMs,\n });\n const serveOptions = opts.sqliteChangeLogServe;\n const writerOptions = opts.sqliteChangeLogWriter;\n const catchupOptions = opts.sqliteCatchup;\n this.#readRouter =\n serveOptions && writerOptions && catchupOptions\n ? new SQLiteChangeLogReadRouter({\n shard,\n readPercent: serveOptions.readPercent,\n coldReadPercent: serveOptions.coldReadPercent,\n retentionMs: serveOptions.retentionMs,\n failureCooldownMs: serveOptions.failureCooldownMs,\n now: serveOptions.now,\n inspect:\n serveOptions.inspect ??\n (() =>\n inspectSQLiteChangeLog(\n lc,\n catchupOptions.changeLogFile,\n writerOptions.identity,\n )),\n })\n : undefined;\n this.#reservations = backupConfig\n ? new SnapshotReservations(lc, backupConfig, taskID => {\n this.#readRouter?.release(taskID);\n this.#purgeScheduler?.resume(taskID);\n })\n : undefined;\n this.#replicationStatusPublisher = replicationStatusPublisher;\n this.#changeLogWriter = opts.sqliteChangeLogWriter\n ? new SQLiteChangeLogWriter(lc, {\n ...opts.sqliteChangeLogWriter,\n onCommit: watermark => {\n this.#sqliteCatchup?.onChangeLogCommit(watermark);\n // The commit closed the log's transaction, i.e. opened a purge\n // window (§3.3).\n this.#purgeScheduler?.onWriterIdle();\n },\n // Fail-soft deletes the file, and the reader is cached here, so it\n // has to go with it: otherwise it serves an unlinked inode while\n // every new open sees nothing.\n onDisabled: () => {\n // The writer stays disabled and the file stays absent for the life\n // of this process, so unlike a transient read/barrier failure this\n // breaker never expires.\n this.#readRouter?.trip(true);\n this.#closeSQLiteCatchup();\n this.#comparator?.stop();\n },\n onRebuilt: () => {\n this.#closeSQLiteCatchup();\n // Invalidate cycles that can still read the replaced file.\n this.#comparator?.invalidate();\n },\n })\n : undefined;\n // The purge scheduler runs on the writer's own connection, which is also\n // its gate: no writer (mode `off`) means no scheduler, and a writer that\n // has not created the file yet -- or failed soft and deleted it -- makes\n // cycles skip rather than fail, so a late-appearing file is picked up\n // without a restart.\n this.#purgeScheduler =\n opts.sqliteChangeLogPurge && this.#changeLogWriter\n ? new SQLiteChangeLogPurgeScheduler(\n lc,\n () => this.#changeLogWriter?.connection,\n () => this.#forwarder.getAcks(),\n opts.sqliteChangeLogPurge,\n )\n : undefined;\n this.#acker = new UpstreamAcker({\n trackPgChangeLog: true, // TODO: set false when retiring PG\n trackBackup: backupConfig?.litestreamVersion === 'v5',\n });\n const replicaSource = opts.sqliteChangeLogCompare\n ? replicaInitializationSource(lc, opts.sqliteChangeLogCompare.replicaFile)\n : undefined;\n this.#initializer = new ChangeLogInitializer(\n lc,\n {\n initFromPgChangeLog: true, // TODO: set false when retiring PG\n initFromReplica: replicaSource !== undefined,\n },\n {\n pgChangeLog: () =>\n this.#storer.getStartStreamInitializationParameters(),\n // Only reached when `initFromReplica` is set, i.e. when the option\n // that supplies the file is present.\n replica: () => must(replicaSource)(),\n // Use the Postgres resume point when it is present. Otherwise, let the\n // SQLite change log supply its own resume point.\n reconcileChangeLog: (resumeFrom, seed) =>\n resumeFrom\n ? this.#changeLogWriter?.reconcile(resumeFrom)\n : this.#changeLogWriter?.reconcileFromLog(seed),\n changeLog: () => this.#changeLogWriter?.connection,\n },\n );\n this.#sqliteCatchupOptions = opts.sqliteCatchup\n ? {\n ...opts.sqliteCatchup,\n // The real cleanup guard, in place of the catchup's no-op default:\n // registration and purge batches now serialize on one mutex.\n cleanupGuard:\n opts.sqliteCatchup.cleanupGuard ??\n this.#purgeScheduler?.cleanupGuard,\n }\n : undefined;\n // Compare mode requires the writer and catchup configuration.\n this.#comparator =\n opts.sqliteChangeLogCompare &&\n opts.sqliteChangeLogWriter &&\n opts.sqliteCatchup\n ? new SQLiteChangeLogComparator(\n lc,\n shard,\n opts.sqliteCatchup.changeLogFile,\n opts.sqliteChangeLogWriter.identity,\n this.#storer,\n {setTimeoutFn, ...opts.sqliteChangeLogCompare},\n )\n : undefined;\n this.#purgeLock = initialPurgeLock;\n this.#autoReset = autoReset;\n this.#state = new RunningState(this.id, undefined, setTimeoutFn);\n this.#latestStatus = {tag: 'status'};\n }\n\n async run() {\n this.#lc.info?.('starting change stream');\n\n this.#forwarder.startProgressMonitor();\n\n const lagReportInit = await this.#source.startLagReporter();\n if (lagReportInit) {\n this.#latestStatus.lagReport = {\n nextSendTimeMs: lagReportInit.nextSendTimeMs,\n };\n // Record the commit time of the initiated lag report (i.e. \"head\")\n // for the purpose of skipping over any lag reports that are re-streamed\n // by the change-source in the case of a change-streamer starting from\n // an older watermark.\n this.#latestLagReportCommitTimeMs = lagReportInit.firstCommitTimeMs;\n }\n\n // Once this change-streamer acquires \"ownership\" of the change DB,\n // it is safe to start the storer.\n await this.#storer.assumeOwnership(this.#purgeLock);\n this.#purgeLock = null;\n\n // The threshold in (estimated number of) bytes to send() on subscriber\n // websockets before `await`-ing the I/O buffers to be ready for more.\n const flushBytesThreshold = getDefaultHighWaterMark(false);\n\n while (this.#state.shouldRun()) {\n let err: unknown;\n let watermark: string | null = null;\n let unflushedBytes = 0;\n try {\n // Initialization reconciles the change log for every stream\n // connection. It completes before `startStream`, so no change can\n // arrive during reconciliation.\n const {lastWatermark, backfillRequests} =\n await this.#initializer.initialize();\n // SQLite catchup must not be eligible until this has been initialized\n // from the durable PG head. Commits observed only since process startup\n // are insufficient after a change-streamer restart.\n this.#lastForwardedCommitWatermark = lastWatermark;\n const stream = await this.#source.startStream(\n lastWatermark,\n backfillRequests,\n );\n this.#storer.run().catch(e => stream.changes.cancel(e));\n\n this.#stream = stream;\n if (\n this.#state.resetBackoff() >\n REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS\n ) {\n // After recovering from a backoff for which a replication status\n // error was published, publish an OK status\n this.#replicationStatusPublisher.publish(\n this.#lc,\n 'Replicating',\n `Replicating from ${lastWatermark}`,\n );\n }\n watermark = null;\n\n this.#acker.reset(stream.acks);\n for await (const change of stream.changes) {\n this.#acker.trackDownstream(change);\n\n const [type, msg] = change;\n switch (type) {\n case 'status':\n if (\n msg.lagReport &&\n msg.lagReport.lastTimings.commitTimeMs >=\n this.#latestLagReportCommitTimeMs\n ) {\n // Lag reports are not stored in the cdc change log, but rather\n // only forwarded on \"live\" connections. When a new subscriber\n // is catching up, it is initialized with the #latestStatus\n // from which it can measure lag while catching up.\n this.#latestStatus.lagReport = msg.lagReport;\n this.#latestLagReportCommitTimeMs =\n msg.lagReport.lastTimings.commitTimeMs;\n this.#forwarder.sendStatus(this.#latestStatus);\n }\n continue;\n case 'control':\n await this.#handleControlMessage(msg);\n continue; // control messages are not stored/forwarded\n case 'begin':\n watermark = change[2].commitWatermark;\n break;\n case 'commit':\n if (watermark !== change[2].watermark) {\n throw new UnrecoverableError(\n `commit watermark ${change[2].watermark} does not match 'begin' watermark ${watermark}`,\n );\n }\n this.#txCounter.add(1);\n break;\n default:\n if (type === 'data') {\n this.#changeCounter.add(1);\n }\n if (watermark === null) {\n throw new UnrecoverableError(\n `${type} change (${msg.tag}) received before 'begin' message`,\n );\n }\n break;\n }\n\n const json = this.#storer.store(watermark, change);\n // The SQLite change log commits at transaction boundaries, and its\n // commit for this transaction lands here -- before the forward of the\n // `commit` message, and before #recordForwardedTransactionBoundary\n // advances what #captureRequiredHead reads. No `await` separates it\n // from #storer.store() above, which is the assertable form of\n // invariant 1: the storer only enqueues, and its Postgres commit\n // cannot complete without yielding, so a synchronous SQLite commit in\n // the same loop iteration always precedes anything that can advance\n // the watermark this stream would resume from. Never throws; a write\n // failure disables the writer rather than stopping replication.\n this.#changeLogWriter?.write(change, json);\n const entry: WatermarkedChange = [watermark, change[1].tag, json];\n unflushedBytes += json.length;\n if (unflushedBytes < flushBytesThreshold) {\n // pipeline changes until flushBytesThreshold\n this.#forwarder.forward(entry);\n this.#recordForwardedTransactionBoundary(type, entry[0]);\n } else {\n // Wait for messages to clear socket buffers to ensure that they\n // make their way to subscribers. Without this `await`, the\n // messages end up being buffered in this process, which:\n // (1) results in memory pressure and increased GC activity\n // (2) prevents subscribers from processing the messages as they\n // arrive, instead getting them in a large batch after being\n // idle while they were queued (causing further delays).\n const forwarded = this.#forwarder.forwardWithFlowControl(entry);\n // forwardWithFlowControl synchronously sends the entry and updates\n // the Forwarder's transaction state before returning its flow-\n // control promise. Record the boundary before awaiting that promise\n // so registrations during the wait observe the forwarded state.\n this.#recordForwardedTransactionBoundary(type, entry[0]);\n await promiseOrAbort(\n forwarded,\n stream.changes.signal,\n this.#state.signal,\n );\n unflushedBytes = 0;\n }\n\n if (type === 'commit' || type === 'rollback') {\n watermark = null;\n }\n\n // Allow the storer to exert back pressure.\n const readyForMore = this.#storer.readyForMore();\n if (readyForMore) {\n await promiseOrAbort(\n readyForMore,\n stream.changes.signal,\n this.#state.signal,\n );\n }\n }\n } catch (e) {\n err = e;\n } finally {\n this.#stream?.changes.cancel();\n this.#stream = undefined;\n }\n\n // When the change stream is interrupted, abort any pending transaction.\n if (watermark) {\n this.#lc.warn?.(`aborting interrupted transaction ${watermark}`);\n this.#storer.abort();\n // Rolling back the log leaves no rows for the interrupted transaction,\n // so the next connection's reconciliation sees a head at or below its\n // resume watermark rather than a partial transaction.\n this.#changeLogWriter?.abort();\n // A rollback ends the log's open transaction without a commit\n // notification; wake any purge batch waiting for that window.\n this.#purgeScheduler?.onWriterIdle();\n this.#forwarder.forward([watermark, 'rollback', ROLLBACK_JSON]);\n this.#recordForwardedTransactionBoundary('rollback', watermark);\n }\n\n // Backoff and drain any pending entries in the storer before reconnecting.\n await Promise.all([\n this.#storer.stop(),\n this.#state.backoff(this.#lc, err),\n this.#state.retryDelay > REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS\n ? publishCriticalEvent(\n this.#lc,\n replicationStatusError(this.#lc, 'Replicating', err),\n )\n : promiseVoid,\n ]);\n }\n\n this.#forwarder.stopProgressMonitor();\n this.#lc.info?.('ChangeStreamer stopped');\n }\n\n async #handleControlMessage(msg: ChangeStreamControl[1]) {\n this.#lc.info?.('received control message', msg);\n const {tag} = msg;\n\n switch (tag) {\n case 'reset-required':\n await markResetRequired(\n this.#changeDBProvider('change-streamer-reset', 1),\n this.#shard,\n );\n await publishReplicationError(\n this.#lc,\n 'Replicating',\n msg.message ?? 'Resync required',\n msg.errorDetails,\n );\n if (this.#autoReset) {\n this.#lc.warn?.('shutting down for auto-reset');\n await this.stop(new AutoResetSignal());\n }\n break;\n default:\n unreachable(tag);\n }\n }\n\n async subscribe(ctx: SubscriberContext): Promise<Source<string>> {\n const {protocolVersion, id, mode, replicaVersion, watermark} = ctx;\n if (mode === 'serving') {\n this.#serving.resolve();\n }\n let cleanupSubscriber = () => {};\n const downstream = Subscription.create<string>({\n cleanup: () => cleanupSubscriber(),\n });\n // No subscriber's ACK advances the SQLite change log's head any more: the\n // writer runs in this process, so the barrier is notified from the commit\n // itself (see #changeLogWriter's onCommit).\n const subscriber = new Subscriber(\n protocolVersion,\n id,\n mode,\n watermark,\n downstream,\n () => this.#latestStatus,\n {},\n );\n const lc = this.#lc.withContext('subscriber', subscriber.id);\n const removeFromForwarder = () => {\n lc.info?.(`removing subscriber ${subscriber.id}`);\n this.#forwarder.remove(subscriber);\n };\n cleanupSubscriber = removeFromForwarder;\n if (replicaVersion !== this.#replicaVersion) {\n lc.warn?.(`rejecting subscriber at replica version ${replicaVersion}`);\n subscriber.close(\n ErrorType.WrongReplicaVersion,\n `current replica version is ${\n this.#replicaVersion\n } (requested ${replicaVersion})`,\n );\n } else {\n lc.info?.(`adding subscriber ${subscriber.id}`);\n\n const catchupFromPG = () => {\n // Keep the existing PG registration/catchup lockstep unchanged when\n // SQLite was not selected before Forwarder.add().\n cleanupSubscriber = removeFromForwarder;\n this.#forwarder.add(subscriber);\n this.#storer.catchup(subscriber, mode);\n };\n const sqliteSelection = this.#selectSQLiteCatchup(lc, ctx);\n if (!sqliteSelection) {\n catchupFromPG();\n } else {\n const {catchup, reason, coverage, logWarm} = sqliteSelection;\n cleanupSubscriber = () => catchup.remove(subscriber);\n const registration = await catchup.catchup(\n subscriber,\n () => this.#captureRequiredHead(),\n {logWarm},\n );\n switch (registration.kind) {\n case 'registered':\n lc.debug?.(\n `serving ${ctx.id} from SQLite catchup`,\n ...(coverage ? [{sqliteChangeLogCoverage: coverage}] : []),\n );\n this.#recordCatchupRoute('sqlite', reason);\n break;\n case 'uncovered':\n lc.info?.(\n `serving ${ctx.id} from PG catchup: subscriber watermark ` +\n `${ctx.watermark} is below the SQLite change-log minimum ` +\n registration.minWatermark,\n );\n this.#recordCatchupRoute('pg', 'watermark-uncovered');\n catchupFromPG();\n break;\n case 'declined':\n // Registration failed before the subscriber was committed to\n // SQLite, so PG is still available. The coordinator has already\n // tripped the breaker, which keeps the retry off SQLite for the\n // cooldown; it is deliberately not closed here, since closing it\n // would abort the catchups of every other subscriber it is\n // serving.\n lc.error?.(\n `serving ${ctx.id} from PG catchup: SQLite catchup ` +\n `registration failed`,\n registration.error,\n );\n this.#recordCatchupRoute('pg', 'registration-failed');\n catchupFromPG();\n break;\n case 'handled':\n // The coordinator closed or failed the subscriber itself, so\n // there is nothing left to route. Still counted, so that the\n // route counter sums to the number of subscriptions.\n this.#recordCatchupRoute('sqlite', 'registration-handled');\n break;\n default:\n unreachable(registration);\n }\n }\n }\n // Any snapshot reservation held by this task can be closed now that\n // it is subscribed to the change stream.\n this.#reservations?.close(ctx.taskID);\n return downstream;\n }\n\n async startSnapshotReservation(\n taskID: string,\n ): Promise<Source<SnapshotMessage>> {\n if (!this.#reservations) {\n throw new Error('backups are not configured');\n }\n const downstream = this.#reservations.open(taskID);\n\n try {\n // Wait for an in-flight SQLite purge batch before reading and\n // advertising the reservation's bounds.\n await (this.#purgeScheduler?.pause(taskID) ?? promiseVoid);\n // A concurrent retry for this task may have superseded and cancelled\n // this reservation while its purge pause was settling. Only the current\n // owner may replace the task's source pin.\n if (!this.#reservations.isCurrent(taskID, downstream)) {\n return downstream;\n }\n // Pin after the purge pause has settled, so the SQLite minimum captured\n // by the router cannot move before it is advertised. #confirmReservations\n // skips an unpinned task while this await is in flight.\n this.#readRouter?.pin(taskID);\n // If a backup has been confirmed, immediately confirm the reservation.\n await this.#confirmReservations();\n return downstream;\n } catch (e) {\n // Cancel the reservation this call opened, not whatever currently\n // holds the task's slot: an overlapping retry for the same taskID may\n // have already replaced it, and a by-taskID close would tear down the\n // replacement and release its purge pause while it is advertising\n // snapshot bounds. cancel() routes through the subscription's cleanup,\n // which closes the reservation only if this instance still owns it.\n downstream.cancel();\n throw e;\n }\n }\n\n trackBackupWatermark(watermark: string) {\n this.#backupWatermark = watermark;\n this.#acker.trackBackup(watermark);\n // The durable backup floor is an independent input to each change-log\n // implementation. SQLite receives it even when the PG log has already\n // reached this watermark.\n this.#requestSQLitePurge('deferred');\n this.#maybeSchedulePGPurge();\n this.#maybeScheduleSQLitePurge();\n\n // Confirm any waiting reservations now that a backup has been confirmed.\n // Note that this is asynchronous and best effort; if it fails, the watermark\n // is still \"tracked\" and the confirmation will be retried on the next backup.\n void this.#confirmReservations().catch(e =>\n this.#lc.warn?.(`error confirming snapshot reservation`, e),\n );\n }\n\n async #confirmReservations() {\n const backupWatermark = this.#backupWatermark;\n const reservations = this.#reservations;\n if (\n backupWatermark === undefined ||\n !reservations?.confirmationsRequired()\n ) {\n return;\n }\n\n // Resolve PG bounds before touching any pin. Everything below runs to\n // completion without awaiting, which is what keeps a reservation's\n // advertised bounds and its pin in agreement: a concurrent /snapshot\n // retry for the same task replaces the reservation and re-pins it, and\n // confirming that replacement with the previous pin's bounds is exactly\n // the mismatch pinning exists to prevent. The cost is one PG round trip\n // per call even when every reservation is pinned to SQLite.\n const pgState = await this.#getChangeLogState();\n for (const taskID of reservations.unconfirmedTaskIDs()) {\n let route = this.#readRouter?.peek(taskID);\n // startSnapshotReservation pins only after an in-flight purge has\n // completed. A task with no pin yet is confirmed by that path instead.\n if (this.#readRouter && route === undefined) {\n continue;\n }\n\n if (route?.source === 'sqlite') {\n const coverage = must(\n route.coverage,\n 'a pinned SQLite route must carry its covered range',\n );\n if (coverage.minWatermark > backupWatermark) {\n // A log seeded after this backup, most often. Holding the\n // reservation until a backup reaches the log's minimum would stall\n // a follower that PG can serve now, so move it -- pin included.\n this.#lc.info?.(\n `demoting ${taskID} to PG catchup: SQLite change-log minimum ` +\n `${coverage.minWatermark} is later than backupWatermark ` +\n backupWatermark,\n );\n this.#reservationDemotions.add(1);\n route = must(this.#readRouter).demote(taskID);\n }\n }\n\n const source = route?.source ?? 'pg';\n let minWatermark: string;\n if (route?.source === 'sqlite') {\n // Demotion above already moved every SQLite route the backup is\n // outside of, so this one covers it and confirms below.\n minWatermark = must(\n route.coverage,\n 'a pinned SQLite route must carry its covered range',\n ).minWatermark;\n } else {\n ({minWatermark} = pgState);\n }\n\n if (minWatermark <= backupWatermark) {\n reservations.confirmFor(\n taskID,\n pgState.replicaVersion,\n backupWatermark,\n source,\n );\n } else {\n // PG cannot catch a restored replica up from this backup yet. Keep\n // the reservation pending until a later backup moves the durable\n // watermark into its covered range.\n if (reservations.noteConfirmationDelayed(taskID)) {\n this.#reservationConfirmDelays.add(1);\n }\n this.#lc.error?.(\n `pg change-log minWatermark ${minWatermark} is later than ` +\n `backupWatermark ${backupWatermark}. Delaying confirmation of ` +\n `snapshot reservation until next backup.`,\n );\n }\n }\n }\n\n #maybeSchedulePGPurge(): void {\n const backupWatermark = this.#backupWatermark;\n if (\n this.#pgPurgeScheduled ||\n this.#pgPurgeRunning ||\n backupWatermark === undefined ||\n this.#pgPurgedWatermark >= backupWatermark\n ) {\n return;\n }\n this.#pgPurgeScheduled = true;\n this.#state.setTimeout(() => {\n this.#pgPurgeScheduled = false;\n this.#pgPurgeRunning = true;\n return this.#purgePGChangeLog().finally(() => {\n this.#pgPurgeRunning = false;\n this.#maybeSchedulePGPurge();\n });\n }, CLEANUP_DELAY_MS);\n }\n\n #maybeScheduleSQLitePurge(): void {\n if (\n this.#sqlitePurgeScheduled ||\n this.#sqlitePurgeRunning ||\n this.#backupWatermark === undefined ||\n this.#sqlitePurgeContinuation === undefined\n ) {\n return;\n }\n const delay =\n this.#sqlitePurgeContinuation === 'immediate' ? 0 : CLEANUP_DELAY_MS;\n this.#sqlitePurgeScheduled = true;\n this.#state.setTimeout(() => {\n this.#sqlitePurgeScheduled = false;\n this.#sqlitePurgeRunning = true;\n return this.#purgeSQLiteChangeLog().finally(() => {\n this.#sqlitePurgeRunning = false;\n this.#maybeScheduleSQLitePurge();\n });\n }, delay);\n }\n\n async #getChangeLogState(): Promise<{\n replicaVersion: string;\n minWatermark: string;\n }> {\n const minWatermark = await this.#storer.getMinWatermarkForCatchup();\n if (!minWatermark) {\n this.#lc.warn?.(\n `Unexpected empty changeLog. Resync if \"Local replica watermark\" errors arise`,\n );\n }\n return {\n replicaVersion: this.#replicaVersion,\n minWatermark: minWatermark ?? this.#replicaVersion,\n };\n }\n\n #getCleanupFloor(): {\n backupWatermark: string;\n purgeWatermark: string;\n current: string[];\n } {\n const backupWatermark = this.#backupWatermark;\n assert(\n backupWatermark !== undefined,\n 'cleanup cannot run without a backup watermark',\n );\n const current = [\n ...this.#forwarder.getAcks(),\n ...(this.#reservations?.getReservedWatermarks() ?? []),\n ];\n // The cleanup delay above is the grace period for disconnected\n // subscribers to reconnect and expose their ACKs. Once it expires, an\n // empty set places no additional constraint on the confirmed backup\n // watermark and must not pin either change log indefinitely.\n return {\n backupWatermark,\n purgeWatermark: min(backupWatermark, ...current),\n current,\n };\n }\n\n async #purgePGChangeLog(): Promise<void> {\n try {\n const {backupWatermark, purgeWatermark, current} =\n this.#getCleanupFloor();\n if (purgeWatermark < backupWatermark) {\n if (this.#loggedBehindWatermark !== purgeWatermark) {\n this.#loggedBehindWatermark = purgeWatermark;\n this.#lc.info?.(\n `At least one client is behind backup ${backupWatermark}`,\n {watermarks: current},\n );\n }\n } else {\n this.#loggedBehindWatermark = undefined;\n }\n if (purgeWatermark <= this.#pgPurgedWatermark) {\n return;\n }\n this.#lc.info?.(`Purging PG changes before ${purgeWatermark} ...`);\n const start = performance.now();\n const deleted = await this.#storer.purgeRecordsBefore(purgeWatermark);\n const elapsed = (performance.now() - start).toFixed(2);\n this.#lc.info?.(\n `Purged ${deleted} PG changes before ${purgeWatermark} (${elapsed} ms)`,\n );\n this.#pgPurgedWatermark = purgeWatermark;\n } catch (e) {\n this.#lc.warn?.(`error purging the PG change log`, e);\n }\n }\n\n async #purgeSQLiteChangeLog(): Promise<void> {\n const scheduler = this.#purgeScheduler;\n if (!scheduler) {\n return;\n }\n try {\n const {backupWatermark, purgeWatermark} = this.#getCleanupFloor();\n // Consume the request before starting. A backup notification that\n // arrives during this pass records another request, which is merged with\n // the continuation returned by this pass rather than being overwritten.\n this.#sqlitePurgeContinuation = undefined;\n if (purgeWatermark < backupWatermark) {\n // Live constraint changes are not edge-triggered. Keep evaluating the\n // floor until it reaches the durable backup watermark, independently\n // of whether the PG implementation still exists.\n this.#requestSQLitePurge('deferred');\n }\n const result = await scheduler.purge(purgeWatermark);\n this.#requestSQLitePurge(result.continuation);\n } catch (e) {\n // purge() is fail-soft, but retain the coordinator's retry if an\n // unexpected caller-boundary failure escapes it.\n this.#requestSQLitePurge('deferred');\n this.#lc.warn?.(`error purging the SQLite change log`, e);\n }\n }\n\n #requestSQLitePurge(continuation: PurgeContinuation | undefined): void {\n if (!this.#purgeScheduler || continuation === undefined) {\n return;\n }\n if (\n continuation === 'immediate' ||\n this.#sqlitePurgeContinuation === undefined\n ) {\n this.#sqlitePurgeContinuation = continuation;\n }\n }\n\n async stop(err?: unknown) {\n this.#state.stop(this.#lc, err);\n this.#stream?.changes.cancel();\n this.#purgeScheduler?.stop();\n this.#comparator?.stop();\n this.#sqliteCatchup?.close();\n this.#changeLogWriter?.close();\n await Promise.allSettled([this.#storer.stop(), this.#source.stop()]);\n }\n\n #recordForwardedTransactionBoundary(\n type: ChangeStreamData[0],\n watermark: string,\n ) {\n switch (type) {\n case 'begin':\n assert(\n this.#currentTransactionCompletion === undefined,\n 'forwarded begin while a transaction is already in progress',\n );\n this.#currentTransactionCompletion =\n resolver<ForwardedTransactionCompletion>();\n this.#transactionForwardStartedAt = performance.now();\n break;\n case 'commit': {\n const completion = this.#currentTransactionCompletion;\n assert(completion, 'forwarded commit without a pending transaction');\n this.#recordTransactionForwardDuration();\n this.#lastForwardedCommitWatermark = watermark;\n this.#currentTransactionCompletion = undefined;\n completion.resolve({kind: 'committed', watermark});\n break;\n }\n case 'rollback': {\n const completion = this.#currentTransactionCompletion;\n assert(completion, 'forwarded rollback without a pending transaction');\n const committed = this.#lastForwardedCommitWatermark;\n assert(committed, 'last forwarded commit watermark is not initialized');\n this.#transactionForwardStartedAt = undefined;\n this.#currentTransactionCompletion = undefined;\n completion.resolve({kind: 'rolled-back', watermark: committed});\n break;\n }\n }\n }\n\n #recordTransactionForwardDuration() {\n const startedAt = this.#transactionForwardStartedAt;\n this.#transactionForwardStartedAt = undefined;\n if (startedAt !== undefined) {\n this.#transactionForwardDuration.recordMs(performance.now() - startedAt, {\n sqlite_change_log: this.#changeLogWriter?.enabled ? 'on' : 'off',\n });\n }\n }\n\n /**\n * Closes and forgets the cached catchup coordinator, so that a later\n * subscription re-opens the log from scratch rather than reading a handle\n * whose file is gone.\n */\n #closeSQLiteCatchup() {\n this.#sqliteCatchup?.close();\n this.#sqliteCatchup = undefined;\n }\n\n #captureRequiredHead(): string | Promise<string> {\n const committed = this.#lastForwardedCommitWatermark;\n assert(committed, 'last forwarded commit watermark is not initialized');\n return (\n this.#currentTransactionCompletion?.promise.then(\n completion => completion.watermark,\n ) ?? committed\n );\n }\n\n #selectSQLiteCatchup(\n lc: LogContext,\n ctx: SubscriberContext,\n ): SQLiteCatchupSelection | undefined {\n const opts = this.#sqliteCatchupOptions;\n if (this.#lastForwardedCommitWatermark === undefined || !opts) {\n this.#recordCatchupRoute('pg', 'not-ready');\n return undefined;\n }\n // SQLite catchup is only for disposable serving replicas. Backup\n // subscribers retain the existing PG recovery policy until RMv2 can\n // resume the canonical replica directly from replica/backup/slot state.\n // Enforce this before the selector so no canary policy can override it.\n if (ctx.mode !== 'serving') {\n lc.info?.(`not serving backup subscriber ${ctx.id} from SQLite catchup`);\n this.#recordCatchupRoute('pg', 'ineligible-mode');\n return undefined;\n }\n\n let route: ChangeLogReadRoute | undefined;\n if (this.#readRouter) {\n route = this.#readRouter.consume(ctx.taskID);\n if (route.source === 'pg') {\n if (route.reason === 'cold-log') {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: SQLite change log is still warming`,\n {sqliteChangeLogCoverage: route.coverage},\n );\n } else if (route.reason === 'log-unavailable') {\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `${opts.changeLogFile} is unavailable or incompatible`,\n );\n } else {\n lc.debug?.(\n `serving ${ctx.id} from PG catchup: SQLite route ${route.reason}`,\n ...(route.coverage\n ? [{sqliteChangeLogCoverage: route.coverage}]\n : []),\n );\n }\n this.#recordCatchupRoute('pg', route.reason);\n return undefined;\n }\n const coverage = route.coverage;\n assert(coverage, 'a SQLite route must carry its covered range');\n if (ctx.watermark < coverage.minWatermark) {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: subscriber watermark ` +\n `${ctx.watermark} is below the SQLite change-log minimum ` +\n coverage.minWatermark,\n {sqliteChangeLogCoverage: coverage},\n );\n this.#recordCatchupRoute('pg', 'watermark-uncovered');\n return undefined;\n }\n }\n\n // `shouldUse` remains as a test hook and an optional extra policy gate.\n // Production selection comes from #readRouter.\n if (!this.#readRouter && !opts.shouldUse?.(ctx)) {\n this.#recordCatchupRoute('pg', 'selector');\n return undefined;\n }\n if (this.#readRouter && opts.shouldUse && !opts.shouldUse(ctx)) {\n this.#recordCatchupRoute('pg', 'selector');\n return undefined;\n }\n\n const catchup =\n this.#sqliteCatchup ?? this.#openSQLiteCatchup(lc, opts, ctx);\n if (catchup) {\n return {\n catchup,\n reason: route?.reason ?? 'selector',\n coverage: route?.coverage,\n // Legacy focused-test selection has no warm classification.\n logWarm:\n route === undefined ? undefined : route.reason !== 'selected-cold',\n };\n } else {\n this.#recordCatchupRoute('pg', 'log-unavailable');\n }\n return undefined;\n }\n\n #recordCatchupRoute(source: 'pg' | 'sqlite', reason: string): void {\n this.#catchupRoutes.add(1, {source, reason});\n }\n\n /**\n * Opens the change log and, if it can serve, wraps it in the coordinator that\n * subsequent subscriptions reuse.\n *\n * The log may not exist yet -- the writer creates it at the stream loop's\n * first reconcile, and deletes it when it fails soft -- or may exist without\n * content, or may be unreadable. None of those can be allowed to fail a\n * subscription:\n * this is the last point at which PG catchup is still available -- past\n * `Forwarder.add()` the subscriber is committed to SQLite -- so each declines\n * here instead. Neither the failure nor the reader is retained, so a later\n * subscription retries from scratch.\n */\n #openSQLiteCatchup(\n lc: LogContext,\n opts: SQLiteCatchupOptions,\n ctx: SubscriberContext,\n ): SQLiteChangeLogCatchup | undefined {\n let reader: SQLiteChangeLogReader | undefined;\n try {\n reader = new SQLiteChangeLogReader(this.#lc, opts.changeLogFile);\n // `plan()` doubles as the readiness check: it reports `not-ready` when\n // the writer has not created or seeded the stream table. The barrier\n // cannot wait its way out of that, since a log with no head can never\n // reach the required one.\n if (reader.plan(ctx.watermark).kind === 'not-ready') {\n reader.close();\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `${opts.changeLogFile} has no changes to serve yet`,\n );\n return undefined;\n }\n } catch (e) {\n // An absent file is the common case, since a readonly handle cannot\n // create one. A corrupt or truncated file lands here too, and is equally\n // not a reason to fail the subscription.\n reader?.close();\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `cannot read ${opts.changeLogFile}`,\n e,\n );\n return undefined;\n }\n this.#changeLogUnavailableSince = undefined;\n // Readiness is checked once, on the way to the cached coordinator: a log\n // with content keeps it. Purging preserves the latest transaction as a\n // catchup boundary, and `reconcileChangeLog` reseeds inside a single\n // transaction, so a reader never observes an emptied log.\n this.#sqliteCatchup = new SQLiteChangeLogCatchup(\n this.#lc,\n this.#forwarder,\n reader,\n {\n batchSize: opts.readBatchRows,\n barrierTimeoutMs: opts.barrierTimeoutMs,\n barrierPollIntervalMs: opts.barrierPollIntervalMs,\n cleanupGuard: opts.cleanupGuard,\n onFailure: failure => {\n this.#lc.warn?.(\n `temporarily disabling SQLite catchup after ${failure}`,\n );\n this.#readRouter?.trip();\n },\n },\n );\n return this.#sqliteCatchup;\n }\n\n /**\n * Reports falling back to PG catchup, tracking how long the log has been\n * unavailable so that a subscription that merely arrived before the writer's\n * first reconcile does not look like an incident.\n */\n #declineSQLiteCatchup(\n lc: LogContext,\n ctx: SubscriberContext,\n opts: SQLiteCatchupOptions,\n reason: string,\n error?: unknown,\n ): void {\n const now = Date.now();\n this.#changeLogUnavailableSince ??= now;\n const unavailableMs = now - this.#changeLogUnavailableSince;\n const threshold =\n opts.notReadyWarnThresholdMs ??\n DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS;\n lc[unavailableMs >= threshold ? 'warn' : 'debug']?.(\n `serving ${ctx.id} from PG catchup: ${reason} ` +\n `(unavailable for ${unavailableMs} ms)`,\n ...(error === undefined ? [] : [error]),\n );\n }\n}\n\ntype ForwardedTransactionCompletion =\n | {kind: 'committed'; watermark: string}\n | {kind: 'rolled-back'; watermark: string};\n\ntype SQLiteCatchupSelection = {\n readonly catchup: SQLiteChangeLogCatchup;\n readonly reason: string;\n readonly coverage: SQLiteChangeLogCoverage | undefined;\n /**\n * Whether the router classified the log as warm for this subscriber, or\n * undefined when selection did not run through the router (focused tests).\n */\n readonly logWarm: boolean | undefined;\n};\n\n// The delay between receiving an initial, backup-based watermark\n// and performing a check of whether to purge records before it.\n// This delay should be long enough to handle situations like the following:\n//\n// 1. `litestream restore` downloads a backup for the `replication-manager`\n// 2. `replication-manager` starts up and runs this `change-streamer`\n// 3. `zero-cache`s that are running on a different replica connect to this\n// `change-streamer` after exponential backoff retries.\n//\n// It is possible for a `zero-cache`[3] to be behind the backup restored [1].\n// This cleanup delay (30 seconds) is thus set to be a value comfortably\n// longer than the max delay for exponential backoff (10 seconds) in\n// `services/running-state.ts`. This allows the `zero-cache` [3] to reconnect\n// so that the `change-streamer` can track its progress and know when it has\n// surpassed the initial watermark of the backup [1].\nconst CLEANUP_DELAY_MS = DEFAULT_MAX_RETRY_DELAY_MS * 3;\n\nconst ROLLBACK_JSON = JSON.stringify([\n 'rollback',\n {tag: 'rollback'},\n] satisfies Rollback);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4LA,eAAsB,mBACpB,IACA,OACA,QACA,kBACA,mBACA,UACA,cACA,4BACA,mBACA,cACA,WACA,WACA,MACA,eAAe,YACiB;CAChC,MAAM,wBACJ,IACA,UACA,mBACA,OACA,WACA,aAAa,KAAA,GACb,YACF;CAOA,MAAM,oBACJ,iBACA,QAEA,SACE,KACE;EAAC;EAAK,YAAY,GAAE,qBAAqB,gBAAe;CAAC,GAKzD,SAAS,OACX,CACF;CAEF,MAAM,EAAC,mBAAkB;CACzB,OAAO,IAAI,mBACT,IACA,OACA,QACA,kBACA,mBACA,kBACA,gBACA,cACA,4BACA,cACA,WACA,WACA,MACA,YACF;AACF;AAEA,IAAM,8CAA8C;AAQpD,IAAM,mDAAmD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4IzD,IAAM,qBAAN,MAA0D;CACxD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CAUA,WAAoB,SAAS;CAE7B,aAAsB,mBACpB,eACA,gBACA,kCACF;CACA,iBAA0B,mBACxB,eACA,WACA,qDACF;CAIA,8BAAuC,4BACrC,eACA,gCACA,yIAEF;CACA,iBAA0B,mBACxB,eACA,oCACA,sEACF;CACA,wBAAiC,mBAC/B,eACA,2CACA,qPAIF;CACA,4BAAqC,mBACnC,eACA,gDACA,2LAGF;CAEA;CACA,+BAA+B;CAC/B;CACA,qBAA6B;;;;;;;CAO7B;CACA;CACA;CAGA,oBAAoB;CACpB,kBAAkB;CAClB,wBAAwB;CACxB,sBAAsB;CACtB;CACA;CACA;CACA;CACA;CACA;CAIA,YACE,IACA,OACA,QACA,kBACA,mBACA,kBACA,gBACA,QACA,4BACA,cACA,kBACA,WACA,MACA,eAAe,YACf;EACA,KAAK,KAAK;EACV,KAAKA,MAAM,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"}
|
|
1
|
+
{"version":3,"file":"change-streamer-service.js","names":["#lc","#shard","#changeDBProvider","#replicaVersion","#source","#storer","#pgChangeLogEnabled","#forwarder","#reservations","#replicationStatusPublisher","#sqliteCatchupOptions","#changeLogWriter","#purgeScheduler","#comparator","#acker","#initializer","#readRouter","#autoReset","#state","#serving","#txCounter","#changeCounter","#transactionForwardDuration","#catchupRoutes","#reservationDemotions","#reservationConfirmDelays","#sqliteCatchup","#closeSQLiteCatchup","#purgeLock","#latestStatus","#latestLagReportCommitTimeMs","#lastForwardedCommitWatermark","#stream","#handleControlMessage","#recordForwardedTransactionBoundary","#selectSQLiteCatchup","#captureRequiredHead","#recordCatchupRoute","#confirmReservations","#backupWatermark","#requestSQLitePurge","#maybeSchedulePGPurge","#maybeScheduleSQLitePurge","#getChangeLogState","#pgPurgeScheduled","#pgPurgeRunning","#pgPurgedWatermark","#purgePGChangeLog","#sqlitePurgeScheduled","#sqlitePurgeRunning","#sqlitePurgeContinuation","#purgeSQLiteChangeLog","#getCleanupFloor","#loggedBehindWatermark","#currentTransactionCompletion","#transactionForwardStartedAt","#recordTransactionForwardDuration","#fallbackOrReject","#declineSQLiteCatchup","#openSQLiteCatchup","#changeLogUnavailableSince"],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-service.ts"],"sourcesContent":["import {getDefaultHighWaterMark} from 'node:stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {defu} from 'defu';\nimport postgres, {type Options, type PostgresType} from 'postgres';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseOrAbort} from '../../../../shared/src/promise-race.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {publishCriticalEvent} from '../../observability/events.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {min} from '../../types/lexi-version.ts';\nimport type {PostgresDB} from '../../types/pg.ts';\nimport type {ShardID} from '../../types/shards.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {\n ChangeSource,\n ChangeStream,\n} from '../change-source/change-source.ts';\nimport {\n type ChangeStreamControl,\n type ChangeStreamData,\n type Rollback,\n} from '../change-source/protocol/current/downstream.ts';\nimport type {LitestreamVersion} from '../litestream/metrics.ts';\nimport {\n publishReplicationError,\n replicationStatusError,\n type ReplicationStatusPublisher,\n} from '../replicator/replication-status.ts';\nimport type {SubscriptionState} from '../replicator/schema/replication-state.ts';\nimport {\n DEFAULT_MAX_RETRY_DELAY_MS,\n RunningState,\n UnrecoverableError,\n} from '../running-state.ts';\nimport {serializeChangeStreamDataWithChange} from './change-log-codec.ts';\nimport {\n ChangeLogInitializer,\n replicaInitializationSource,\n} from './change-log-initializer.ts';\nimport {\n type ChangeStreamerService,\n type Status,\n type SubscriberContext,\n type WatermarkedChange,\n} from './change-streamer.ts';\nimport * as ErrorType from './error-type-enum.ts';\nimport {Forwarder} from './forwarder.ts';\nimport {\n AutoResetSignal,\n ensureReplicationConfig,\n markResetRequired,\n} from './schema/tables.ts';\nimport {SnapshotReservations} from './snapshot-reservations.ts';\nimport type {SnapshotMessage} from './snapshot.ts';\nimport {\n SQLiteChangeLogCatchup,\n type SQLiteChangeLogCleanupGuard,\n} from './sqlite-change-log-catchup.ts';\nimport {\n SQLiteChangeLogComparator,\n type SQLiteChangeLogCompareOptions,\n} from './sqlite-change-log-comparator.ts';\nimport {\n SQLiteChangeLogPurgeScheduler,\n type PurgeContinuation,\n type SQLiteChangeLogPurgeSchedulerOptions,\n} from './sqlite-change-log-purge-scheduler.ts';\nimport {\n inspectSQLiteChangeLog,\n SQLiteChangeLogReadRouter,\n type ChangeLogReadRoute,\n type SQLiteChangeLogCoverage,\n} from './sqlite-change-log-read-router.ts';\nimport {SQLiteChangeLogReader} from './sqlite-change-log-reader.ts';\nimport {\n SQLiteChangeLogWriter,\n type SQLiteChangeLogWriterOptions,\n} from './sqlite-change-log-writer.ts';\nimport type {PostgresDBProvider} from './storer.ts';\nimport {\n Storer,\n type PurgeLock,\n type TuningOptions as StorerOptions,\n} from './storer.ts';\nimport {Subscriber} from './subscriber.ts';\nimport {UpstreamAcker} from './upstream-acker.ts';\n\nexport type BackupConfig = {\n backupURL: string;\n litestreamVersion: LitestreamVersion;\n};\n\nexport type SQLiteCatchupOptions = {\n /** The change-log database, i.e. `changeLogFileName(replicaFile)`. */\n changeLogFile: string;\n readBatchRows: number;\n barrierTimeoutMs: number;\n /**\n * Backstop for the change-log writer's ACK, which is what normally releases\n * the barrier. Defaults to the catchup coordinator's interval.\n */\n barrierPollIntervalMs?: number | undefined;\n /**\n * Optional policy hook used by focused tests. Production canary selection is\n * supplied by `sqliteChangeLogServe` and is stable by shard plus task ID.\n * Backup subscribers are rejected before either selector is invoked.\n */\n shouldUse?: ((ctx: SubscriberContext) => boolean) | undefined;\n /**\n * Overrides the purge scheduler's guard, for tests. When absent, the\n * service supplies the scheduler's real writer-serialized guard, or the\n * catchup's no-op default when purging is not configured.\n */\n cleanupGuard?: SQLiteChangeLogCleanupGuard | undefined;\n /**\n * How long the change log may be unavailable before declining to serve from\n * it is reported as a warning rather than a debug line. Defaults to\n * {@link DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS}.\n */\n notReadyWarnThresholdMs?: number | undefined;\n};\n\nexport type SQLiteChangeLogServeOptions = {\n /** Stable percentage of eligible serving tasks routed to SQLite. */\n readPercent: number;\n /**\n * Stable percentage of eligible serving tasks routed to a log that has not\n * yet aged through {@link retentionMs}. Zero keeps a reseeded log on PG for\n * that whole window; above zero it serves that share of tasks, and a\n * reservation it cannot cover is demoted rather than held pending.\n * Defaults to zero.\n */\n coldReadPercent?: number | undefined;\n /** The warm-up window a reseeded log ages through. */\n retentionMs: number;\n /** Injectable for deterministic breaker tests. */\n failureCooldownMs?: number | undefined;\n /** Injectable for deterministic warm-up and breaker tests. */\n now?: (() => number) | undefined;\n /** Injectable readiness inspection for service-level tests. */\n inspect?: (() => SQLiteChangeLogCoverage | undefined) | undefined;\n};\n\nexport type TuningOptions = StorerOptions & {\n /**\n * Keeps the legacy Postgres change log on the initialization, persistence,\n * catchup, purge, and ACK paths. Disable only after SQLite and the replica\n * backup are authoritative for all of those responsibilities.\n */\n pgChangeLogEnabled: boolean;\n flowControlConsensusTimeoutProportion: number;\n flowControlSlowSubscriberGracePeriodMs?: number | undefined;\n sqliteCatchup?: SQLiteCatchupOptions | undefined;\n /**\n * Supplied when `sqliteChangeLogMode != off`, i.e. this is the gate on the\n * change-log writer. Absent, nothing writes the log.\n */\n sqliteChangeLogWriter?:\n | Omit<\n SQLiteChangeLogWriterOptions,\n 'onCommit' | 'onDisabled' | 'onRebuilt'\n >\n | undefined;\n /**\n * Also supplied when `sqliteChangeLogMode != off`, and deliberately not\n * gated on the read path: in `write` mode no reader ever opens, and this is\n * the configuration the purge scheduler actually ships in. The scheduler\n * runs on the writer's own connection, so with the writer absent (mode\n * `off`) it is never constructed, and a leftover file on disk is left to\n * the replicator's cleanup.\n */\n sqliteChangeLogPurge?: SQLiteChangeLogPurgeSchedulerOptions | undefined;\n /**\n * Supplied in `compare` mode and later modes.\n * `replicaFile` enables the initialization comparison.\n * The other fields configure sampled catchup comparisons.\n * Both checks are advisory, and Postgres remains authoritative.\n * The comparator also requires the writer and catchup options.\n */\n sqliteChangeLogCompare?:\n | (SQLiteChangeLogCompareOptions & {replicaFile: string})\n | undefined;\n /** Supplied only in `serve` mode. A zero percentage keeps every read on PG. */\n sqliteChangeLogServe?: SQLiteChangeLogServeOptions | undefined;\n};\n\n/**\n * Performs initialization and schema migrations to initialize a ChangeStreamerImpl.\n */\nexport async function initializeStreamer(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n changeDB: PostgresDB,\n changeSource: ChangeSource,\n replicationStatusPublisher: ReplicationStatusPublisher,\n subscriptionState: SubscriptionState,\n backupConfig: BackupConfig | null,\n purgeLock: PurgeLock | null,\n autoReset: boolean,\n opts: TuningOptions,\n setTimeoutFn = setTimeout,\n): Promise<ChangeStreamerService> {\n await ensureReplicationConfig(\n lc,\n changeDB,\n subscriptionState,\n shard,\n autoReset,\n purgeLock ?? undefined,\n setTimeoutFn,\n opts.pgChangeLogEnabled,\n );\n\n // Dynamically creates connection pools that the implementation uses to\n // isolate concurrent, long-running transactions. This works around a bug in\n // the postgres.js client where connections get swapped in certain conditions.\n //\n // https://github.com/porsager/postgres/issues/1204\n const changeDBProvider: PostgresDBProvider = (\n applicationName: string,\n max: number,\n ) =>\n postgres(\n defu(\n {max, connection: {['application_name']: applicationName}},\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 changeDB.options as unknown as Options<Record<string, PostgresType>>,\n ),\n ) as PostgresDB;\n\n const {replicaVersion} = subscriptionState;\n return new ChangeStreamerImpl(\n lc,\n shard,\n taskID,\n discoveryAddress,\n discoveryProtocol,\n changeDBProvider,\n replicaVersion,\n changeSource,\n replicationStatusPublisher,\n backupConfig,\n purgeLock,\n autoReset,\n opts,\n setTimeoutFn,\n );\n}\n\nconst REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS = 5000;\n\n// How long the change log may be unavailable before declining to serve from it\n// stops looking like normal startup ordering. Subscriptions can arrive before\n// the stream loop's first reconcile has created and seeded the log, so early\n// declines are expected. Still declining a minute later means the log is not\n// being written at all -- `sqliteChangeLogMode=off`, a writer that has failed\n// soft, or a path mismatch -- which is worth surfacing.\nconst DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS = 60_000;\n\n/**\n * Upstream-agnostic dispatch of messages in a {@link ChangeStreamMessage} to a\n * {@link Forwarder} and {@link Storer} to execute the forward-store-ack\n * procedure described in {@link ChangeStreamer}.\n *\n * ### Subscriber Catchup\n *\n * Connecting clients first need to be \"caught up\" to the current watermark\n * (from stored change log entries) before new entries are forwarded to\n * them. This is non-trivial because the replication stream may be in the\n * middle of a pending streamed Transaction for which some entries have\n * already been forwarded but are not yet committed to the store.\n *\n *\n * ```\n * ------------------------------- - - - - - - - - - - - - - - - - - - -\n * | Historic changes in storage | Pending (streamed) tx | Next tx\n * ------------------------------- - - - - - - - - - - - - - - - - - - -\n * Replication stream\n * > > > > > > > > >\n * ^ ---> required catchup ---> ^\n * Subscriber watermark Subscription begins\n * ```\n *\n * Preemptively buffering the changes of every pending transaction\n * would be wasteful and consume too much memory for large transactions.\n *\n * Instead, the streamer synchronously dispatches changes and subscriptions\n * to the {@link Forwarder} and the {@link Storer} such that the two\n * components are aligned as to where in the stream the subscription started.\n * The two components then coordinate catchup and handoff via the\n * {@link Subscriber} object with the following algorithm:\n *\n * * If the streamer is in the middle of a pending Transaction, the\n * Subscriber is \"queued\" on both the Forwarder and the Storer. In this\n * state, new changes are *not* forwarded to the Subscriber, and catchup\n * is not yet executed.\n * * Once the commit message for the pending Transaction is processed\n * by the Storer, it begins catchup on the Subscriber (with a READONLY\n * snapshot so that it does not block subsequent storage operations).\n * This catchup is thus guaranteed to load the change log entries of\n * that last Transaction.\n * * When the Forwarder processes that same commit message, it moves the\n * Subscriber from the \"queued\" to the \"active\" set of clients such that\n * the Subscriber begins receiving new changes, starting from the next\n * Transaction.\n * * The Subscriber does not forward those changes, however, if its catchup\n * is not complete. Until then, it buffers the changes in memory.\n * * Once catchup is complete, the buffered changes are immediately sent\n * and the Subscriber henceforth forwards changes as they are received.\n *\n * In the (common) case where the streamer is not in the middle of a pending\n * transaction when a subscription begins, the Storer begins catchup\n * immediately and the Forwarder directly adds the Subscriber to its active\n * set. However, the Subscriber still buffers any forwarded messages until\n * its catchup is complete.\n *\n * ### Watermarks and ordering\n *\n * The ChangeStreamerService depends on its {@link ChangeSource} to send\n * changes in contiguous [`begin`, `data` ..., `data`, `commit`] sequences\n * in commit order. This follows Postgres's Logical Replication Protocol\n * Message Flow:\n *\n * https://www.postgresql.org/docs/16/protocol-logical-replication.html#PROTOCOL-LOGICAL-MESSAGES-FLOW\n *\n * > The logical replication protocol sends individual transactions one by one.\n * > This means that all messages between a pair of Begin and Commit messages belong to the same transaction.\n *\n * In order to correctly replay (new) and filter (old) messages to subscribers\n * at different points in the replication stream, these changes must be assigned\n * watermarks such that they preserve the order in which they were received\n * from the ChangeSource.\n *\n * A previous implementation incorrectly derived these watermarks from the Postgres\n * Log Sequence Numbers (LSN) of each message. However, LSNs from concurrent,\n * non-conflicting transactions can overlap, which can result in a `begin` message\n * with an earlier LSN arriving after a `commit` message. For example, the\n * changes for these transactions:\n *\n * ```\n * LSN: 1 2 3 4 5 6 7 8 9 10\n * tx1: begin data data data commit\n * tx2: begin data data data commit\n * ```\n *\n * will arrive as:\n *\n * ```\n * begin1, data2, data4, data6, commit8, begin3, data5, data7, data9, commit10\n * ```\n *\n * Thus, LSN of non-commit messages are not suitable for tracking the sorting\n * order of the replication stream.\n *\n * Instead, the ChangeStreamer uses the following algorithm for deterministic\n * catchup and filtering of changes:\n *\n * * A `commit` message is assigned to a watermark corresponding to its LSN.\n * These are guaranteed to be in commit order by definition.\n *\n * * `begin` and `data` messages are assigned to the watermark of the\n * preceding `commit` (the previous transaction, or the replication\n * slot's starting LSN) plus 1. This guarantees that they will be sorted\n * after the previously commit transaction even if their LSNs came before it.\n * This is referred to as the `preCommitWatermark`.\n *\n * * In the ChangeLog DB, messages have a secondary sort column `pos`, which is\n * the position of the message within its transaction, with the `begin` message\n * starting at `0`. This guarantees that `begin` and `data` messages will be\n * fetched in the original ChangeSource order during catchup.\n *\n * `begin` and `data` messages share the same watermark, but this is sufficient for\n * Subscriber filtering because subscribers only know about the `commit` watermarks\n * exposed in the `Downstream` `Commit` message. The Subscriber object thus compares\n * the internal watermarks of the incoming messages against the commit watermark of\n * the caller, updating the watermark at every `Commit` message that is forwarded.\n *\n * ### Cleanup\n *\n * As mentioned in the {@link ChangeStreamer} documentation: \"the ChangeStreamer\n * uses a combination of [the \"initial\", i.e. backup-derived watermark and] ACK\n * responses from connected subscribers to determine the watermark up\n * to which it is safe to purge old change log entries.\"\n *\n * More concretely:\n *\n * * The `initial`, backup-derived watermark is the earliest to which cleanup\n * should ever happen.\n *\n * * However, it is possible for the replica backup to be *ahead* of a connected\n * subscriber; and if a network error causes that subscriber to retry from its\n * last watermark, the change streamer must support it.\n *\n * Thus, before cleaning up to an `initial` backup-derived watermark, the change\n * streamer first confirms that all connected subscribers have also passed\n * that watermark.\n */\nclass ChangeStreamerImpl implements ChangeStreamerService {\n readonly id: string;\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #changeDBProvider: PostgresDBProvider;\n readonly #replicaVersion: string;\n readonly #source: ChangeSource;\n readonly #storer: Storer;\n readonly #pgChangeLogEnabled: boolean;\n readonly #forwarder: Forwarder;\n readonly #reservations: SnapshotReservations | undefined;\n readonly #replicationStatusPublisher: ReplicationStatusPublisher;\n readonly #sqliteCatchupOptions: SQLiteCatchupOptions | undefined;\n readonly #changeLogWriter: SQLiteChangeLogWriter | undefined;\n readonly #purgeScheduler: SQLiteChangeLogPurgeScheduler | undefined;\n readonly #comparator: SQLiteChangeLogComparator | undefined;\n readonly #acker: UpstreamAcker;\n readonly #initializer: ChangeLogInitializer;\n readonly #readRouter: SQLiteChangeLogReadRouter | undefined;\n\n readonly #autoReset: boolean;\n readonly #state: RunningState;\n\n // Starting the (Postgres) ChangeStream results in killing the previous\n // Postgres subscriber, potentially creating a gap in which the old\n // change-streamer has shut down and the new change-streamer has not yet\n // been recognized as \"healthy\" (and thus does not get any requests).\n //\n // To minimize this gap, delay starting the ChangeStream until the first\n // request from a `serving` replicator, indicating that higher level\n // load-balancing / routing logic has begun routing requests to this task.\n readonly #serving = resolver();\n\n readonly #txCounter = getOrCreateCounter(\n 'replication',\n 'transactions',\n 'Count of replicated transactions',\n );\n readonly #changeCounter = getOrCreateCounter(\n 'replication',\n 'changes',\n 'Count of replicated changes (DML or DDL statements)',\n );\n // The number the SQLite change-log commit lands in. It is labeled by whether\n // the log is being written so that the cost of putting a commit on the\n // forward path is attributable rather than inferred.\n readonly #transactionForwardDuration = getOrCreateLatencyHistogram(\n 'replication',\n 'transaction_forward_duration',\n \"Time from receiving a transaction's `begin` to forwarding its `commit`, \" +\n 'i.e. the change-streamer half of forward-to-subscriber latency.',\n );\n readonly #catchupRoutes = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.catchup_routes',\n 'Catchup subscriptions by selected source and low-cardinality reason.',\n );\n readonly #reservationDemotions = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.reservation_demotions',\n 'Snapshot reservations demoted from SQLite to PG because the change ' +\n 'log could not cover the backup being restored. With PG retired ' +\n 'these followers have no fallback, so this is the rate at which one ' +\n 'would instead have to wait for a later backup.',\n );\n readonly #reservationConfirmDelays = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.reservation_confirm_delays',\n 'Snapshot reservations whose confirmation was deferred because the ' +\n \"selected source's change-log minimum was later than the backup \" +\n 'watermark. Counted once per reservation, by that source.',\n );\n\n #latestStatus: Status;\n #latestLagReportCommitTimeMs = 0;\n #backupWatermark: string | undefined;\n #pgPurgedWatermark: string = '';\n /**\n * The floor last logged as held back by a laggard. The level-triggered\n * retry re-evaluates every {@link CLEANUP_DELAY_MS}; logging only when the\n * blocking floor moves keeps a stuck subscriber from emitting the same\n * line indefinitely.\n */\n #loggedBehindWatermark: string | undefined;\n #sqlitePurgeContinuation: PurgeContinuation | undefined;\n #purgeLock: PurgeLock | null;\n // PG and SQLite intentionally own separate level-triggered loops. Neither\n // waits for, advances, or retries the other.\n #pgPurgeScheduled = false;\n #pgPurgeRunning = false;\n #sqlitePurgeScheduled = false;\n #sqlitePurgeRunning = false;\n #stream: ChangeStream | undefined;\n #sqliteCatchup: SQLiteChangeLogCatchup | undefined;\n #changeLogUnavailableSince: number | undefined;\n #lastForwardedCommitWatermark: string | undefined;\n #transactionForwardStartedAt: number | undefined;\n #currentTransactionCompletion:\n | Resolver<ForwardedTransactionCompletion>\n | undefined;\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n changeDBProvider: PostgresDBProvider,\n replicaVersion: string,\n source: ChangeSource,\n replicationStatusPublisher: ReplicationStatusPublisher,\n backupConfig: BackupConfig | null,\n initialPurgeLock: PurgeLock | null,\n autoReset: boolean,\n opts: TuningOptions,\n setTimeoutFn = setTimeout,\n ) {\n this.id = `change-streamer`;\n this.#lc = lc.withContext('component', 'change-streamer');\n this.#shard = shard;\n this.#changeDBProvider = changeDBProvider;\n this.#replicaVersion = replicaVersion;\n this.#source = source;\n this.#pgChangeLogEnabled = opts.pgChangeLogEnabled;\n if (!this.#pgChangeLogEnabled) {\n assert(\n opts.sqliteChangeLogWriter &&\n opts.sqliteChangeLogPurge &&\n opts.sqliteCatchup &&\n opts.sqliteChangeLogServe?.readPercent === 100 &&\n opts.sqliteChangeLogServe.coldReadPercent === 100 &&\n backupConfig?.litestreamVersion === 'v5',\n 'disabling the PG change log requires SQLite serving at 100 percent and a v5 backup',\n );\n assert(\n opts.sqliteChangeLogCompare === undefined,\n 'SQLite change-log comparison requires the PG change log',\n );\n }\n this.#storer = new Storer(\n lc,\n shard,\n taskID,\n discoveryAddress,\n discoveryProtocol,\n changeDBProvider,\n replicaVersion,\n consumed => this.#acker.trackPgChangeLog(consumed[2].watermark),\n err => this.stop(err),\n opts,\n );\n this.#forwarder = new Forwarder(lc, {\n flowControlConsensusTimeoutProportion:\n opts.flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodMs:\n opts.flowControlSlowSubscriberGracePeriodMs,\n });\n const serveOptions = opts.sqliteChangeLogServe;\n const writerOptions = opts.sqliteChangeLogWriter;\n const catchupOptions = opts.sqliteCatchup;\n this.#readRouter =\n serveOptions && writerOptions && catchupOptions\n ? new SQLiteChangeLogReadRouter({\n shard,\n readPercent: serveOptions.readPercent,\n coldReadPercent: serveOptions.coldReadPercent,\n retentionMs: serveOptions.retentionMs,\n failureCooldownMs: serveOptions.failureCooldownMs,\n now: serveOptions.now,\n inspect:\n serveOptions.inspect ??\n (() =>\n inspectSQLiteChangeLog(\n lc,\n catchupOptions.changeLogFile,\n writerOptions.identity,\n )),\n })\n : undefined;\n this.#reservations = backupConfig\n ? new SnapshotReservations(lc, backupConfig, taskID => {\n this.#readRouter?.release(taskID);\n this.#purgeScheduler?.resume(taskID);\n })\n : undefined;\n this.#replicationStatusPublisher = replicationStatusPublisher;\n this.#changeLogWriter = opts.sqliteChangeLogWriter\n ? new SQLiteChangeLogWriter(lc, {\n ...opts.sqliteChangeLogWriter,\n onCommit: watermark => {\n this.#sqliteCatchup?.onChangeLogCommit(watermark);\n // The commit closed the log's transaction, i.e. opened a purge\n // window (§3.3).\n this.#purgeScheduler?.onWriterIdle();\n },\n // Fail-soft deletes the file, and the reader is cached here, so it\n // has to go with it: otherwise it serves an unlinked inode while\n // every new open sees nothing.\n onDisabled: () => {\n // The writer stays disabled and the file stays absent for the life\n // of this process, so unlike a transient read/barrier failure this\n // breaker never expires.\n this.#readRouter?.trip(true);\n this.#closeSQLiteCatchup();\n this.#comparator?.stop();\n },\n onRebuilt: () => {\n this.#closeSQLiteCatchup();\n // Invalidate cycles that can still read the replaced file.\n this.#comparator?.invalidate();\n },\n })\n : undefined;\n // The purge scheduler runs on the writer's own connection, which is also\n // its gate: no writer (mode `off`) means no scheduler, and a writer that\n // has not created the file yet -- or failed soft and deleted it -- makes\n // cycles skip rather than fail, so a late-appearing file is picked up\n // without a restart.\n this.#purgeScheduler =\n opts.sqliteChangeLogPurge && this.#changeLogWriter\n ? new SQLiteChangeLogPurgeScheduler(\n lc,\n () => this.#changeLogWriter?.connection,\n () => this.#forwarder.getAcks(),\n opts.sqliteChangeLogPurge,\n )\n : undefined;\n this.#acker = new UpstreamAcker({\n trackPgChangeLog: this.#pgChangeLogEnabled,\n trackBackup: backupConfig?.litestreamVersion === 'v5',\n });\n const replicaFileForInitialization =\n opts.sqliteChangeLogCompare?.replicaFile ??\n (!this.#pgChangeLogEnabled\n ? opts.sqliteChangeLogWriter?.replicaFile\n : undefined);\n const replicaSource = replicaFileForInitialization\n ? replicaInitializationSource(lc, replicaFileForInitialization)\n : undefined;\n this.#initializer = new ChangeLogInitializer(\n lc,\n {\n initFromPgChangeLog: this.#pgChangeLogEnabled,\n initFromReplica: replicaSource !== undefined,\n },\n {\n pgChangeLog: () =>\n this.#storer.getStartStreamInitializationParameters(),\n // Only reached when `initFromReplica` is set, i.e. when the option\n // that supplies the file is present.\n replica: () => must(replicaSource)(),\n // Use the Postgres resume point when it is present. Otherwise, let the\n // SQLite change log supply its own resume point.\n reconcileChangeLog: (resumeFrom, seed) =>\n resumeFrom\n ? this.#changeLogWriter?.reconcile(resumeFrom)\n : this.#changeLogWriter?.reconcileFromLog(seed),\n changeLog: () => this.#changeLogWriter?.connection,\n },\n );\n this.#sqliteCatchupOptions = opts.sqliteCatchup\n ? {\n ...opts.sqliteCatchup,\n // The real cleanup guard, in place of the catchup's no-op default:\n // registration and purge batches now serialize on one mutex.\n cleanupGuard:\n opts.sqliteCatchup.cleanupGuard ??\n this.#purgeScheduler?.cleanupGuard,\n }\n : undefined;\n // Compare mode requires the writer and catchup configuration.\n this.#comparator =\n this.#pgChangeLogEnabled &&\n opts.sqliteChangeLogCompare &&\n opts.sqliteChangeLogWriter &&\n opts.sqliteCatchup\n ? new SQLiteChangeLogComparator(\n lc,\n shard,\n opts.sqliteCatchup.changeLogFile,\n opts.sqliteChangeLogWriter.identity,\n this.#storer,\n {setTimeoutFn, ...opts.sqliteChangeLogCompare},\n )\n : undefined;\n this.#purgeLock = initialPurgeLock;\n this.#autoReset = autoReset;\n this.#state = new RunningState(this.id, undefined, setTimeoutFn);\n this.#latestStatus = {tag: 'status'};\n }\n\n async run() {\n this.#lc.info?.('starting change stream');\n\n this.#forwarder.startProgressMonitor();\n\n const lagReportInit = await this.#source.startLagReporter();\n if (lagReportInit) {\n this.#latestStatus.lagReport = {\n nextSendTimeMs: lagReportInit.nextSendTimeMs,\n };\n // Record the commit time of the initiated lag report (i.e. \"head\")\n // for the purpose of skipping over any lag reports that are re-streamed\n // by the change-source in the case of a change-streamer starting from\n // an older watermark.\n this.#latestLagReportCommitTimeMs = lagReportInit.firstCommitTimeMs;\n }\n\n // Once this change-streamer acquires \"ownership\" of the change DB,\n // it is safe to start the storer.\n await this.#storer.assumeOwnership(this.#purgeLock);\n this.#purgeLock = null;\n\n // The threshold in (estimated number of) bytes to send() on subscriber\n // websockets before `await`-ing the I/O buffers to be ready for more.\n const flushBytesThreshold = getDefaultHighWaterMark(false);\n\n while (this.#state.shouldRun()) {\n let err: unknown;\n let watermark: string | null = null;\n let unflushedBytes = 0;\n try {\n // Initialization reconciles the change log for every stream\n // connection. It completes before `startStream`, so no change can\n // arrive during reconciliation.\n const {lastWatermark, backfillRequests} =\n await this.#initializer.initialize();\n // SQLite catchup must not be eligible until this has been initialized\n // from the selected durable head. Commits observed only since process\n // startup are insufficient after a change-streamer restart.\n this.#lastForwardedCommitWatermark = lastWatermark;\n const stream = await this.#source.startStream(\n lastWatermark,\n backfillRequests,\n );\n if (this.#pgChangeLogEnabled) {\n this.#storer.run().catch(e => stream.changes.cancel(e));\n }\n\n this.#stream = stream;\n if (\n this.#state.resetBackoff() >\n REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS\n ) {\n // After recovering from a backoff for which a replication status\n // error was published, publish an OK status\n this.#replicationStatusPublisher.publish(\n this.#lc,\n 'Replicating',\n `Replicating from ${lastWatermark}`,\n );\n }\n watermark = null;\n\n this.#acker.reset(stream.acks);\n for await (const change of stream.changes) {\n this.#acker.trackDownstream(change);\n\n const [type, msg] = change;\n switch (type) {\n case 'status':\n if (\n msg.lagReport &&\n msg.lagReport.lastTimings.commitTimeMs >=\n this.#latestLagReportCommitTimeMs\n ) {\n // Lag reports are not stored in the cdc change log, but rather\n // only forwarded on \"live\" connections. When a new subscriber\n // is catching up, it is initialized with the #latestStatus\n // from which it can measure lag while catching up.\n this.#latestStatus.lagReport = msg.lagReport;\n this.#latestLagReportCommitTimeMs =\n msg.lagReport.lastTimings.commitTimeMs;\n this.#forwarder.sendStatus(this.#latestStatus);\n }\n continue;\n case 'control':\n await this.#handleControlMessage(msg);\n continue; // control messages are not stored/forwarded\n case 'begin':\n watermark = change[2].commitWatermark;\n break;\n case 'commit':\n if (watermark !== change[2].watermark) {\n throw new UnrecoverableError(\n `commit watermark ${change[2].watermark} does not match 'begin' watermark ${watermark}`,\n );\n }\n this.#txCounter.add(1);\n break;\n default:\n if (type === 'data') {\n this.#changeCounter.add(1);\n }\n if (watermark === null) {\n throw new UnrecoverableError(\n `${type} change (${msg.tag}) received before 'begin' message`,\n );\n }\n break;\n }\n\n const serialized = serializeChangeStreamDataWithChange(change);\n const {json} = serialized;\n if (this.#pgChangeLogEnabled) {\n this.#storer.store(watermark, change, serialized);\n }\n // The SQLite change log commits at transaction boundaries, and its\n // commit for this transaction lands here -- before the forward of the\n // `commit` message, and before #recordForwardedTransactionBoundary\n // advances what #captureRequiredHead reads. No `await` separates it\n // from serialization or the optional PG enqueue above. This is the\n // assertable form of invariant 1: a synchronous SQLite commit in the\n // same loop iteration always precedes anything that can advance the\n // watermark this stream would resume from. Never throws; a write\n // failure disables the writer rather than stopping replication.\n this.#changeLogWriter?.write(change, json, serialized.change);\n const entry: WatermarkedChange = [watermark, change[1].tag, json];\n unflushedBytes += json.length;\n if (unflushedBytes < flushBytesThreshold) {\n // pipeline changes until flushBytesThreshold\n this.#forwarder.forward(entry);\n this.#recordForwardedTransactionBoundary(type, entry[0]);\n } else {\n // Wait for messages to clear socket buffers to ensure that they\n // make their way to subscribers. Without this `await`, the\n // messages end up being buffered in this process, which:\n // (1) results in memory pressure and increased GC activity\n // (2) prevents subscribers from processing the messages as they\n // arrive, instead getting them in a large batch after being\n // idle while they were queued (causing further delays).\n const forwarded = this.#forwarder.forwardWithFlowControl(entry);\n // forwardWithFlowControl synchronously sends the entry and updates\n // the Forwarder's transaction state before returning its flow-\n // control promise. Record the boundary before awaiting that promise\n // so registrations during the wait observe the forwarded state.\n this.#recordForwardedTransactionBoundary(type, entry[0]);\n await promiseOrAbort(\n forwarded,\n stream.changes.signal,\n this.#state.signal,\n );\n unflushedBytes = 0;\n }\n\n if (type === 'commit' || type === 'rollback') {\n watermark = null;\n }\n\n // Allow the PG storer to exert back pressure when it is enabled.\n const readyForMore = this.#pgChangeLogEnabled\n ? this.#storer.readyForMore()\n : undefined;\n if (readyForMore) {\n await promiseOrAbort(\n readyForMore,\n stream.changes.signal,\n this.#state.signal,\n );\n }\n }\n } catch (e) {\n err = e;\n } finally {\n this.#stream?.changes.cancel();\n this.#stream = undefined;\n }\n\n // When the change stream is interrupted, abort any pending transaction.\n if (watermark) {\n this.#lc.warn?.(`aborting interrupted transaction ${watermark}`);\n if (this.#pgChangeLogEnabled) {\n this.#storer.abort();\n }\n // Rolling back the log leaves no rows for the interrupted transaction,\n // so the next connection's reconciliation sees a head at or below its\n // resume watermark rather than a partial transaction.\n this.#changeLogWriter?.abort();\n // A rollback ends the log's open transaction without a commit\n // notification; wake any purge batch waiting for that window.\n this.#purgeScheduler?.onWriterIdle();\n this.#forwarder.forward([watermark, 'rollback', ROLLBACK_JSON]);\n this.#recordForwardedTransactionBoundary('rollback', watermark);\n }\n\n // Backoff and drain any pending entries in the storer before reconnecting.\n await Promise.all([\n this.#storer.stop(),\n this.#state.backoff(this.#lc, err),\n this.#state.retryDelay > REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS\n ? publishCriticalEvent(\n this.#lc,\n replicationStatusError(this.#lc, 'Replicating', err),\n )\n : promiseVoid,\n ]);\n }\n\n this.#forwarder.stopProgressMonitor();\n this.#lc.info?.('ChangeStreamer stopped');\n }\n\n async #handleControlMessage(msg: ChangeStreamControl[1]) {\n this.#lc.info?.('received control message', msg);\n const {tag} = msg;\n\n switch (tag) {\n case 'reset-required':\n await markResetRequired(\n this.#changeDBProvider('change-streamer-reset', 1),\n this.#shard,\n );\n await publishReplicationError(\n this.#lc,\n 'Replicating',\n msg.message ?? 'Resync required',\n msg.errorDetails,\n );\n if (this.#autoReset) {\n this.#lc.warn?.('shutting down for auto-reset');\n await this.stop(new AutoResetSignal());\n }\n break;\n default:\n unreachable(tag);\n }\n }\n\n async subscribe(ctx: SubscriberContext): Promise<Source<string>> {\n const {protocolVersion, id, mode, replicaVersion, watermark} = ctx;\n if (mode === 'serving') {\n this.#serving.resolve();\n }\n let cleanupSubscriber = () => {};\n const downstream = Subscription.create<string>({\n cleanup: () => cleanupSubscriber(),\n });\n // No subscriber's ACK advances the SQLite change log's head any more: the\n // writer runs in this process, so the barrier is notified from the commit\n // itself (see #changeLogWriter's onCommit).\n const subscriber = new Subscriber(\n protocolVersion,\n id,\n mode,\n watermark,\n downstream,\n () => this.#latestStatus,\n {},\n );\n const lc = this.#lc.withContext('subscriber', subscriber.id);\n const removeFromForwarder = () => {\n lc.info?.(`removing subscriber ${subscriber.id}`);\n this.#forwarder.remove(subscriber);\n };\n cleanupSubscriber = removeFromForwarder;\n if (replicaVersion !== this.#replicaVersion) {\n lc.warn?.(`rejecting subscriber at replica version ${replicaVersion}`);\n subscriber.close(\n ErrorType.WrongReplicaVersion,\n `current replica version is ${\n this.#replicaVersion\n } (requested ${replicaVersion})`,\n );\n } else {\n lc.info?.(`adding subscriber ${subscriber.id}`);\n\n const catchupFromPG = () => {\n assert(\n this.#pgChangeLogEnabled,\n 'cannot catch up from a disabled PG change log',\n );\n // Keep the existing PG registration/catchup lockstep unchanged when\n // SQLite was not selected before Forwarder.add().\n cleanupSubscriber = removeFromForwarder;\n this.#forwarder.add(subscriber);\n this.#storer.catchup(subscriber, mode);\n };\n const sqliteDecision = this.#selectSQLiteCatchup(lc, ctx);\n if (!sqliteDecision) {\n catchupFromPG();\n } else if (sqliteDecision.kind === 'rejected') {\n lc.warn?.(\n `${\n sqliteDecision.terminal ? 'rejecting' : 'ending'\n } subscription for ${ctx.id}: ${sqliteDecision.message}`,\n );\n if (sqliteDecision.terminal) {\n subscriber.close(ErrorType.WatermarkTooOld, sqliteDecision.message);\n } else {\n subscriber.close();\n }\n } else {\n const {catchup, reason, coverage, logWarm} = sqliteDecision;\n cleanupSubscriber = () => catchup.remove(subscriber);\n const registration = await catchup.catchup(\n subscriber,\n () => this.#captureRequiredHead(),\n {logWarm},\n );\n switch (registration.kind) {\n case 'registered':\n lc.debug?.(\n `serving ${ctx.id} from SQLite catchup`,\n ...(coverage ? [{sqliteChangeLogCoverage: coverage}] : []),\n );\n this.#recordCatchupRoute('sqlite', reason);\n break;\n case 'uncovered':\n if (this.#pgChangeLogEnabled) {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: subscriber watermark ` +\n `${ctx.watermark} is below the SQLite change-log minimum ` +\n registration.minWatermark,\n );\n this.#recordCatchupRoute('pg', 'watermark-uncovered');\n catchupFromPG();\n } else {\n const message =\n `earliest supported watermark is ` +\n `${registration.minWatermark} (requested ${ctx.watermark})`;\n lc.warn?.(\n `rejecting subscriber at watermark ${ctx.watermark}: ` +\n `the SQLite change log starts at ` +\n `${registration.minWatermark} and the PG change log is disabled`,\n );\n this.#recordCatchupRoute('none', 'watermark-uncovered');\n subscriber.close(ErrorType.WatermarkTooOld, message);\n }\n break;\n case 'declined':\n // Registration failed before the subscriber was committed to\n // SQLite. Use PG when enabled; otherwise end this subscription so\n // it retries. The coordinator has already tripped the breaker,\n // which keeps the retry off SQLite for the cooldown; it is\n // deliberately not closed here, since closing it would abort the\n // catchups of every other subscriber it is serving.\n if (this.#pgChangeLogEnabled) {\n lc.error?.(\n `serving ${ctx.id} from PG catchup: SQLite catchup ` +\n `registration failed`,\n registration.error,\n );\n this.#recordCatchupRoute('pg', 'registration-failed');\n catchupFromPG();\n } else {\n lc.error?.(\n `ending subscription for ${ctx.id} to retry SQLite catchup: ` +\n `registration failed and the PG change log is disabled`,\n registration.error,\n );\n this.#recordCatchupRoute('none', 'registration-failed');\n subscriber.fail(registration.error);\n }\n break;\n case 'handled':\n // The coordinator closed or failed the subscriber itself, so\n // there is nothing left to route. Still counted, so that the\n // route counter sums to the number of subscriptions.\n this.#recordCatchupRoute('sqlite', 'registration-handled');\n break;\n default:\n unreachable(registration);\n }\n }\n }\n // Any snapshot reservation held by this task can be closed now that\n // it is subscribed to the change stream.\n this.#reservations?.close(ctx.taskID);\n return downstream;\n }\n\n async startSnapshotReservation(\n taskID: string,\n ): Promise<Source<SnapshotMessage>> {\n if (!this.#reservations) {\n throw new Error('backups are not configured');\n }\n const downstream = this.#reservations.open(taskID);\n\n try {\n // Wait for an in-flight SQLite purge batch before reading and\n // advertising the reservation's bounds.\n await (this.#purgeScheduler?.pause(taskID) ?? promiseVoid);\n // A concurrent retry for this task may have superseded and cancelled\n // this reservation while its purge pause was settling. Only the current\n // owner may replace the task's source pin.\n if (!this.#reservations.isCurrent(taskID, downstream)) {\n return downstream;\n }\n // Pin after the purge pause has settled, so the SQLite minimum captured\n // by the router cannot move before it is advertised. #confirmReservations\n // skips an unpinned task while this await is in flight.\n this.#readRouter?.pin(taskID);\n // If a backup has been confirmed, immediately confirm the reservation.\n await this.#confirmReservations();\n return downstream;\n } catch (e) {\n // Cancel the reservation this call opened, not whatever currently\n // holds the task's slot: an overlapping retry for the same taskID may\n // have already replaced it, and a by-taskID close would tear down the\n // replacement and release its purge pause while it is advertising\n // snapshot bounds. cancel() routes through the subscription's cleanup,\n // which closes the reservation only if this instance still owns it.\n downstream.cancel();\n throw e;\n }\n }\n\n trackBackupWatermark(watermark: string) {\n this.#backupWatermark = watermark;\n this.#acker.trackBackup(watermark);\n // The durable backup floor is an independent input to each change-log\n // implementation. SQLite receives it even when the PG log has already\n // reached this watermark.\n this.#requestSQLitePurge('deferred');\n this.#maybeSchedulePGPurge();\n this.#maybeScheduleSQLitePurge();\n\n // Confirm any waiting reservations now that a backup has been confirmed.\n // Note that this is asynchronous and best effort; if it fails, the watermark\n // is still \"tracked\" and the confirmation will be retried on the next backup.\n void this.#confirmReservations().catch(e =>\n this.#lc.warn?.(`error confirming snapshot reservation`, e),\n );\n }\n\n async #confirmReservations() {\n const backupWatermark = this.#backupWatermark;\n const reservations = this.#reservations;\n if (\n backupWatermark === undefined ||\n !reservations?.confirmationsRequired()\n ) {\n return;\n }\n\n // Resolve PG bounds, when enabled, before touching any pin. Everything\n // below runs to completion without awaiting, which keeps a reservation's\n // advertised bounds and its pin in agreement: a concurrent /snapshot\n // retry for the same task replaces the reservation and re-pins it, and\n // confirming that replacement with the previous pin's bounds is exactly\n // the mismatch pinning exists to prevent.\n const pgState = this.#pgChangeLogEnabled\n ? await this.#getChangeLogState()\n : undefined;\n for (const taskID of reservations.unconfirmedTaskIDs()) {\n let route = this.#readRouter?.peek(taskID);\n // startSnapshotReservation pins only after an in-flight purge has\n // completed. A task with no pin yet is confirmed by that path instead.\n if (this.#readRouter && route === undefined) {\n continue;\n }\n\n // A transiently unavailable or broken SQLite log normally pins the\n // reservation to PG. With PG disabled, re-evaluate that choice on each\n // backup notification and keep the reservation pending until SQLite can\n // supply bounds.\n if (!this.#pgChangeLogEnabled && route?.source === 'pg') {\n this.#readRouter?.release(taskID);\n route = this.#readRouter?.pin(taskID);\n if (route?.source === 'pg') {\n if (reservations.noteConfirmationDelayed(taskID)) {\n this.#reservationConfirmDelays.add(1);\n }\n this.#lc.warn?.(\n `delaying snapshot reservation for ${taskID}: SQLite change log ` +\n `is unavailable and the PG change log is disabled`,\n );\n continue;\n }\n }\n\n if (route?.source === 'sqlite') {\n const coverage = must(\n route.coverage,\n 'a pinned SQLite route must carry its covered range',\n );\n if (\n this.#pgChangeLogEnabled &&\n coverage.minWatermark > backupWatermark\n ) {\n // A log seeded after this backup, most often. Holding the\n // reservation until a backup reaches the log's minimum would stall\n // a follower that PG can serve now, so move it -- pin included.\n this.#lc.info?.(\n `demoting ${taskID} to PG catchup: SQLite change-log minimum ` +\n `${coverage.minWatermark} is later than backupWatermark ` +\n backupWatermark,\n );\n this.#reservationDemotions.add(1);\n route = must(this.#readRouter).demote(taskID);\n }\n }\n\n const source = route?.source ?? 'pg';\n let minWatermark: string;\n if (route?.source === 'sqlite') {\n // Demotion above already moved every SQLite route the backup is\n // outside of, so this one covers it and confirms below.\n minWatermark = must(\n route.coverage,\n 'a pinned SQLite route must carry its covered range',\n ).minWatermark;\n } else {\n minWatermark = must(\n pgState,\n 'a PG reservation route requires the PG change log',\n ).minWatermark;\n }\n\n if (minWatermark <= backupWatermark) {\n reservations.confirmFor(\n taskID,\n this.#replicaVersion,\n backupWatermark,\n source,\n );\n } else {\n // PG cannot catch a restored replica up from this backup yet. Keep\n // the reservation pending until a later backup moves the durable\n // watermark into its covered range.\n if (reservations.noteConfirmationDelayed(taskID)) {\n this.#reservationConfirmDelays.add(1);\n }\n this.#lc.error?.(\n `${source} change-log minWatermark ${minWatermark} is later than ` +\n `backupWatermark ${backupWatermark}. Delaying confirmation of ` +\n `snapshot reservation until next backup.`,\n );\n }\n }\n }\n\n #maybeSchedulePGPurge(): void {\n const backupWatermark = this.#backupWatermark;\n if (\n !this.#pgChangeLogEnabled ||\n this.#pgPurgeScheduled ||\n this.#pgPurgeRunning ||\n backupWatermark === undefined ||\n this.#pgPurgedWatermark >= backupWatermark\n ) {\n return;\n }\n this.#pgPurgeScheduled = true;\n this.#state.setTimeout(() => {\n this.#pgPurgeScheduled = false;\n this.#pgPurgeRunning = true;\n return this.#purgePGChangeLog().finally(() => {\n this.#pgPurgeRunning = false;\n this.#maybeSchedulePGPurge();\n });\n }, CLEANUP_DELAY_MS);\n }\n\n #maybeScheduleSQLitePurge(): void {\n if (\n this.#sqlitePurgeScheduled ||\n this.#sqlitePurgeRunning ||\n this.#backupWatermark === undefined ||\n this.#sqlitePurgeContinuation === undefined\n ) {\n return;\n }\n const delay =\n this.#sqlitePurgeContinuation === 'immediate' ? 0 : CLEANUP_DELAY_MS;\n this.#sqlitePurgeScheduled = true;\n this.#state.setTimeout(() => {\n this.#sqlitePurgeScheduled = false;\n this.#sqlitePurgeRunning = true;\n return this.#purgeSQLiteChangeLog().finally(() => {\n this.#sqlitePurgeRunning = false;\n this.#maybeScheduleSQLitePurge();\n });\n }, delay);\n }\n\n async #getChangeLogState(): Promise<{\n replicaVersion: string;\n minWatermark: string;\n }> {\n const minWatermark = await this.#storer.getMinWatermarkForCatchup();\n if (!minWatermark) {\n this.#lc.warn?.(\n `Unexpected empty changeLog. Resync if \"Local replica watermark\" errors arise`,\n );\n }\n return {\n replicaVersion: this.#replicaVersion,\n minWatermark: minWatermark ?? this.#replicaVersion,\n };\n }\n\n #getCleanupFloor(): {\n backupWatermark: string;\n purgeWatermark: string;\n current: string[];\n } {\n const backupWatermark = this.#backupWatermark;\n assert(\n backupWatermark !== undefined,\n 'cleanup cannot run without a backup watermark',\n );\n const current = [\n ...this.#forwarder.getAcks(),\n ...(this.#reservations?.getReservedWatermarks() ?? []),\n ];\n // The cleanup delay above is the grace period for disconnected\n // subscribers to reconnect and expose their ACKs. Once it expires, an\n // empty set places no additional constraint on the confirmed backup\n // watermark and must not pin either change log indefinitely.\n return {\n backupWatermark,\n purgeWatermark: min(backupWatermark, ...current),\n current,\n };\n }\n\n async #purgePGChangeLog(): Promise<void> {\n try {\n const {backupWatermark, purgeWatermark, current} =\n this.#getCleanupFloor();\n if (purgeWatermark < backupWatermark) {\n if (this.#loggedBehindWatermark !== purgeWatermark) {\n this.#loggedBehindWatermark = purgeWatermark;\n this.#lc.info?.(\n `At least one client is behind backup ${backupWatermark}`,\n {watermarks: current},\n );\n }\n } else {\n this.#loggedBehindWatermark = undefined;\n }\n if (purgeWatermark <= this.#pgPurgedWatermark) {\n return;\n }\n this.#lc.info?.(`Purging PG changes before ${purgeWatermark} ...`);\n const start = performance.now();\n const deleted = await this.#storer.purgeRecordsBefore(purgeWatermark);\n const elapsed = (performance.now() - start).toFixed(2);\n this.#lc.info?.(\n `Purged ${deleted} PG changes before ${purgeWatermark} (${elapsed} ms)`,\n );\n this.#pgPurgedWatermark = purgeWatermark;\n } catch (e) {\n this.#lc.warn?.(`error purging the PG change log`, e);\n }\n }\n\n async #purgeSQLiteChangeLog(): Promise<void> {\n const scheduler = this.#purgeScheduler;\n if (!scheduler) {\n return;\n }\n try {\n const {backupWatermark, purgeWatermark} = this.#getCleanupFloor();\n // Consume the request before starting. A backup notification that\n // arrives during this pass records another request, which is merged with\n // the continuation returned by this pass rather than being overwritten.\n this.#sqlitePurgeContinuation = undefined;\n if (purgeWatermark < backupWatermark) {\n // Live constraint changes are not edge-triggered. Keep evaluating the\n // floor until it reaches the durable backup watermark, independently\n // of whether the PG implementation still exists.\n this.#requestSQLitePurge('deferred');\n }\n const result = await scheduler.purge(purgeWatermark);\n this.#requestSQLitePurge(result.continuation);\n } catch (e) {\n // purge() is fail-soft, but retain the coordinator's retry if an\n // unexpected caller-boundary failure escapes it.\n this.#requestSQLitePurge('deferred');\n this.#lc.warn?.(`error purging the SQLite change log`, e);\n }\n }\n\n #requestSQLitePurge(continuation: PurgeContinuation | undefined): void {\n if (!this.#purgeScheduler || continuation === undefined) {\n return;\n }\n if (\n continuation === 'immediate' ||\n this.#sqlitePurgeContinuation === undefined\n ) {\n this.#sqlitePurgeContinuation = continuation;\n }\n }\n\n async stop(err?: unknown) {\n this.#state.stop(this.#lc, err);\n this.#stream?.changes.cancel();\n this.#purgeScheduler?.stop();\n this.#comparator?.stop();\n this.#sqliteCatchup?.close();\n this.#changeLogWriter?.close();\n await Promise.allSettled([this.#storer.stop(), this.#source.stop()]);\n }\n\n #recordForwardedTransactionBoundary(\n type: ChangeStreamData[0],\n watermark: string,\n ) {\n switch (type) {\n case 'begin':\n assert(\n this.#currentTransactionCompletion === undefined,\n 'forwarded begin while a transaction is already in progress',\n );\n this.#currentTransactionCompletion =\n resolver<ForwardedTransactionCompletion>();\n this.#transactionForwardStartedAt = performance.now();\n break;\n case 'commit': {\n const completion = this.#currentTransactionCompletion;\n assert(completion, 'forwarded commit without a pending transaction');\n this.#recordTransactionForwardDuration();\n this.#lastForwardedCommitWatermark = watermark;\n this.#currentTransactionCompletion = undefined;\n completion.resolve({kind: 'committed', watermark});\n break;\n }\n case 'rollback': {\n const completion = this.#currentTransactionCompletion;\n assert(completion, 'forwarded rollback without a pending transaction');\n const committed = this.#lastForwardedCommitWatermark;\n assert(committed, 'last forwarded commit watermark is not initialized');\n this.#transactionForwardStartedAt = undefined;\n this.#currentTransactionCompletion = undefined;\n completion.resolve({kind: 'rolled-back', watermark: committed});\n break;\n }\n }\n }\n\n #recordTransactionForwardDuration() {\n const startedAt = this.#transactionForwardStartedAt;\n this.#transactionForwardStartedAt = undefined;\n if (startedAt !== undefined) {\n this.#transactionForwardDuration.recordMs(performance.now() - startedAt, {\n sqlite_change_log: this.#changeLogWriter?.enabled ? 'on' : 'off',\n });\n }\n }\n\n /**\n * Closes and forgets the cached catchup coordinator, so that a later\n * subscription re-opens the log from scratch rather than reading a handle\n * whose file is gone.\n */\n #closeSQLiteCatchup() {\n this.#sqliteCatchup?.close();\n this.#sqliteCatchup = undefined;\n }\n\n #captureRequiredHead(): string | Promise<string> {\n const committed = this.#lastForwardedCommitWatermark;\n assert(committed, 'last forwarded commit watermark is not initialized');\n return (\n this.#currentTransactionCompletion?.promise.then(\n completion => completion.watermark,\n ) ?? committed\n );\n }\n\n #selectSQLiteCatchup(\n lc: LogContext,\n ctx: SubscriberContext,\n ): SQLiteCatchupDecision | undefined {\n const opts = this.#sqliteCatchupOptions;\n if (this.#lastForwardedCommitWatermark === undefined || !opts) {\n return this.#fallbackOrReject(\n 'not-ready',\n 'SQLite catchup is not ready and the PG change log is disabled',\n );\n }\n // While the PG change log is available, backup subscribers retain its\n // existing recovery policy and cannot be moved by a canary selector.\n // Once PG is disabled, the backup replicator must use SQLite catchup: it\n // advances the canonical replica that Litestream backs up.\n if (ctx.mode === 'backup' && this.#pgChangeLogEnabled) {\n lc.info?.(`not serving backup subscriber ${ctx.id} from SQLite catchup`);\n return this.#fallbackOrReject(\n 'ineligible-mode',\n 'backup subscribers retain the PG change-log recovery policy',\n );\n }\n\n let route: ChangeLogReadRoute | undefined;\n if (this.#readRouter) {\n route = this.#readRouter.consume(ctx.taskID);\n if (route.source === 'pg') {\n if (route.reason === 'cold-log') {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: SQLite change log is still warming`,\n {sqliteChangeLogCoverage: route.coverage},\n );\n } else if (route.reason === 'log-unavailable') {\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `${opts.changeLogFile} is unavailable or incompatible`,\n );\n } else {\n lc.debug?.(\n `serving ${ctx.id} from PG catchup: SQLite route ${route.reason}`,\n ...(route.coverage\n ? [{sqliteChangeLogCoverage: route.coverage}]\n : []),\n );\n }\n return this.#fallbackOrReject(\n route.reason,\n `SQLite catchup route ${route.reason} is unavailable and the PG ` +\n `change log is disabled`,\n );\n }\n const coverage = route.coverage;\n assert(coverage, 'a SQLite route must carry its covered range');\n if (ctx.watermark < coverage.minWatermark) {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: subscriber watermark ` +\n `${ctx.watermark} is below the SQLite change-log minimum ` +\n coverage.minWatermark,\n {sqliteChangeLogCoverage: coverage},\n );\n return this.#fallbackOrReject(\n 'watermark-uncovered',\n `earliest supported watermark is ${coverage.minWatermark} ` +\n `(requested ${ctx.watermark})`,\n true,\n );\n }\n }\n\n // `shouldUse` remains as a test hook and an optional extra policy gate.\n // Production selection comes from #readRouter.\n if (!this.#readRouter && !opts.shouldUse?.(ctx)) {\n return this.#fallbackOrReject(\n 'selector',\n 'SQLite catchup was not selected and the PG change log is disabled',\n );\n }\n if (this.#readRouter && opts.shouldUse && !opts.shouldUse(ctx)) {\n return this.#fallbackOrReject(\n 'selector',\n 'SQLite catchup was not selected and the PG change log is disabled',\n );\n }\n\n const catchup =\n this.#sqliteCatchup ?? this.#openSQLiteCatchup(lc, opts, ctx);\n if (catchup) {\n return {\n kind: 'selected',\n catchup,\n reason: route?.reason ?? 'selector',\n coverage: route?.coverage,\n // Legacy focused-test selection has no warm classification.\n logWarm:\n route === undefined ? undefined : route.reason !== 'selected-cold',\n };\n } else {\n return this.#fallbackOrReject(\n 'log-unavailable',\n 'SQLite change log is unavailable and the PG change log is disabled',\n );\n }\n }\n\n #fallbackOrReject(\n reason: string,\n message: string,\n terminal = false,\n ): SQLiteCatchupRejection | undefined {\n this.#recordCatchupRoute(this.#pgChangeLogEnabled ? 'pg' : 'none', reason);\n return this.#pgChangeLogEnabled\n ? undefined\n : {kind: 'rejected', message, terminal};\n }\n\n #recordCatchupRoute(source: 'pg' | 'sqlite' | 'none', reason: string): void {\n this.#catchupRoutes.add(1, {source, reason});\n }\n\n /**\n * Opens the change log and, if it can serve, wraps it in the coordinator that\n * subsequent subscriptions reuse.\n *\n * The log may not exist yet -- the writer creates it at the stream loop's\n * first reconcile, and deletes it when it fails soft -- or may exist without\n * content, or may be unreadable. None of those can be allowed to fail a\n * subscription:\n * this is the last point at which PG catchup can be selected when it is\n * enabled -- past `Forwarder.add()` the subscriber is committed to SQLite --\n * so each declines here instead. With PG disabled, the caller ends the\n * subscription so that it retries. Neither the failure nor the reader is\n * retained, so a later subscription retries from scratch.\n */\n #openSQLiteCatchup(\n lc: LogContext,\n opts: SQLiteCatchupOptions,\n ctx: SubscriberContext,\n ): SQLiteChangeLogCatchup | undefined {\n let reader: SQLiteChangeLogReader | undefined;\n try {\n reader = new SQLiteChangeLogReader(this.#lc, opts.changeLogFile);\n // `plan()` doubles as the readiness check: it reports `not-ready` when\n // the writer has not created or seeded the stream table. The barrier\n // cannot wait its way out of that, since a log with no head can never\n // reach the required one.\n if (reader.plan(ctx.watermark).kind === 'not-ready') {\n reader.close();\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `${opts.changeLogFile} has no changes to serve yet`,\n );\n return undefined;\n }\n } catch (e) {\n // An absent file is the common case, since a readonly handle cannot\n // create one. A corrupt or truncated file lands here too, and is equally\n // not a reason to fail the subscription.\n reader?.close();\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `cannot read ${opts.changeLogFile}`,\n e,\n );\n return undefined;\n }\n this.#changeLogUnavailableSince = undefined;\n // Readiness is checked once, on the way to the cached coordinator: a log\n // with content keeps it. Purging preserves the latest transaction as a\n // catchup boundary, and `reconcileChangeLog` reseeds inside a single\n // transaction, so a reader never observes an emptied log.\n this.#sqliteCatchup = new SQLiteChangeLogCatchup(\n this.#lc,\n this.#forwarder,\n reader,\n {\n batchSize: opts.readBatchRows,\n barrierTimeoutMs: opts.barrierTimeoutMs,\n barrierPollIntervalMs: opts.barrierPollIntervalMs,\n cleanupGuard: opts.cleanupGuard,\n onFailure: failure => {\n this.#lc.warn?.(\n `temporarily disabling SQLite catchup after ${failure}`,\n );\n this.#readRouter?.trip();\n },\n },\n );\n return this.#sqliteCatchup;\n }\n\n /**\n * Reports falling back to PG catchup, tracking how long the log has been\n * unavailable so that a subscription that merely arrived before the writer's\n * first reconcile does not look like an incident.\n */\n #declineSQLiteCatchup(\n lc: LogContext,\n ctx: SubscriberContext,\n opts: SQLiteCatchupOptions,\n reason: string,\n error?: unknown,\n ): void {\n const now = Date.now();\n this.#changeLogUnavailableSince ??= now;\n const unavailableMs = now - this.#changeLogUnavailableSince;\n const threshold =\n opts.notReadyWarnThresholdMs ??\n DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS;\n lc[unavailableMs >= threshold ? 'warn' : 'debug']?.(\n (this.#pgChangeLogEnabled\n ? `serving ${ctx.id} from PG catchup: `\n : `cannot serve ${ctx.id} from SQLite catchup: `) +\n `${reason} ` +\n `(unavailable for ${unavailableMs} ms)`,\n ...(error === undefined ? [] : [error]),\n );\n }\n}\n\ntype ForwardedTransactionCompletion =\n | {kind: 'committed'; watermark: string}\n | {kind: 'rolled-back'; watermark: string};\n\ntype SQLiteCatchupSelection = {\n readonly kind: 'selected';\n readonly catchup: SQLiteChangeLogCatchup;\n readonly reason: string;\n readonly coverage: SQLiteChangeLogCoverage | undefined;\n /**\n * Whether the router classified the log as warm for this subscriber, or\n * undefined when selection did not run through the router (focused tests).\n */\n readonly logWarm: boolean | undefined;\n};\n\ntype SQLiteCatchupRejection = {\n readonly kind: 'rejected';\n readonly message: string;\n /** A terminal rejection forces the view-syncer to restore a fresh replica. */\n readonly terminal: boolean;\n};\n\ntype SQLiteCatchupDecision = SQLiteCatchupSelection | SQLiteCatchupRejection;\n\n// The delay between receiving an initial, backup-based watermark\n// and performing a check of whether to purge records before it.\n// This delay should be long enough to handle situations like the following:\n//\n// 1. `litestream restore` downloads a backup for the `replication-manager`\n// 2. `replication-manager` starts up and runs this `change-streamer`\n// 3. `zero-cache`s that are running on a different replica connect to this\n// `change-streamer` after exponential backoff retries.\n//\n// It is possible for a `zero-cache`[3] to be behind the backup restored [1].\n// This cleanup delay (30 seconds) is thus set to be a value comfortably\n// longer than the max delay for exponential backoff (10 seconds) in\n// `services/running-state.ts`. This allows the `zero-cache` [3] to reconnect\n// so that the `change-streamer` can track its progress and know when it has\n// surpassed the initial watermark of the backup [1].\nconst CLEANUP_DELAY_MS = DEFAULT_MAX_RETRY_DELAY_MS * 3;\n\nconst ROLLBACK_JSON = JSON.stringify([\n 'rollback',\n {tag: 'rollback'},\n] satisfies Rollback);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmMA,eAAsB,mBACpB,IACA,OACA,QACA,kBACA,mBACA,UACA,cACA,4BACA,mBACA,cACA,WACA,WACA,MACA,eAAe,YACiB;CAChC,MAAM,wBACJ,IACA,UACA,mBACA,OACA,WACA,aAAa,KAAA,GACb,cACA,KAAK,kBACP;CAOA,MAAM,oBACJ,iBACA,QAEA,SACE,KACE;EAAC;EAAK,YAAY,GAAE,qBAAqB,gBAAe;CAAC,GAKzD,SAAS,OACX,CACF;CAEF,MAAM,EAAC,mBAAkB;CACzB,OAAO,IAAI,mBACT,IACA,OACA,QACA,kBACA,mBACA,kBACA,gBACA,cACA,4BACA,cACA,WACA,WACA,MACA,YACF;AACF;AAEA,IAAM,8CAA8C;AAQpD,IAAM,mDAAmD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4IzD,IAAM,qBAAN,MAA0D;CACxD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CAUA,WAAoB,SAAS;CAE7B,aAAsB,mBACpB,eACA,gBACA,kCACF;CACA,iBAA0B,mBACxB,eACA,WACA,qDACF;CAIA,8BAAuC,4BACrC,eACA,gCACA,yIAEF;CACA,iBAA0B,mBACxB,eACA,oCACA,sEACF;CACA,wBAAiC,mBAC/B,eACA,2CACA,qPAIF;CACA,4BAAqC,mBACnC,eACA,gDACA,2LAGF;CAEA;CACA,+BAA+B;CAC/B;CACA,qBAA6B;;;;;;;CAO7B;CACA;CACA;CAGA,oBAAoB;CACpB,kBAAkB;CAClB,wBAAwB;CACxB,sBAAsB;CACtB;CACA;CACA;CACA;CACA;CACA;CAIA,YACE,IACA,OACA,QACA,kBACA,mBACA,kBACA,gBACA,QACA,4BACA,cACA,kBACA,WACA,MACA,eAAe,YACf;EACA,KAAK,KAAK;EACV,KAAKA,MAAM,GAAG,YAAY,aAAa,iBAAiB;EACxD,KAAKC,SAAS;EACd,KAAKC,oBAAoB;EACzB,KAAKC,kBAAkB;EACvB,KAAKC,UAAU;EACf,KAAKE,sBAAsB,KAAK;EAChC,IAAI,CAAC,KAAKA,qBAAqB;GAC7B,OACE,KAAK,yBACH,KAAK,wBACL,KAAK,iBACL,KAAK,sBAAsB,gBAAgB,OAC3C,KAAK,qBAAqB,oBAAoB,OAC9C,cAAc,sBAAsB,MACtC,oFACF;GACA,OACE,KAAK,2BAA2B,KAAA,GAChC,yDACF;EACF;EACA,KAAKD,UAAU,IAAI,OACjB,IACA,OACA,QACA,kBACA,mBACA,kBACA,iBACA,aAAY,KAAKS,OAAO,iBAAiB,SAAS,GAAG,SAAS,IAC9D,QAAO,KAAK,KAAK,GAAG,GACpB,IACF;EACA,KAAKP,aAAa,IAAI,UAAU,IAAI;GAClC,uCACE,KAAK;GACP,wCACE,KAAK;EACT,CAAC;EACD,MAAM,eAAe,KAAK;EAC1B,MAAM,gBAAgB,KAAK;EAC3B,MAAM,iBAAiB,KAAK;EAC5B,KAAKS,cACH,gBAAgB,iBAAiB,iBAC7B,IAAI,0BAA0B;GAC5B;GACA,aAAa,aAAa;GAC1B,iBAAiB,aAAa;GAC9B,aAAa,aAAa;GAC1B,mBAAmB,aAAa;GAChC,KAAK,aAAa;GAClB,SACE,aAAa,kBAEX,uBACE,IACA,eAAe,eACf,cAAc,QAChB;EACN,CAAC,IACD,KAAA;EACN,KAAKR,gBAAgB,eACjB,IAAI,qBAAqB,IAAI,eAAc,WAAU;GACnD,KAAKQ,aAAa,QAAQ,MAAM;GAChC,KAAKJ,iBAAiB,OAAO,MAAM;EACrC,CAAC,IACD,KAAA;EACJ,KAAKH,8BAA8B;EACnC,KAAKE,mBAAmB,KAAK,wBACzB,IAAI,sBAAsB,IAAI;GAC5B,GAAG,KAAK;GACR,WAAU,cAAa;IACrB,KAAKe,gBAAgB,kBAAkB,SAAS;IAGhD,KAAKd,iBAAiB,aAAa;GACrC;GAIA,kBAAkB;IAIhB,KAAKI,aAAa,KAAK,IAAI;IAC3B,KAAKW,oBAAoB;IACzB,KAAKd,aAAa,KAAK;GACzB;GACA,iBAAiB;IACf,KAAKc,oBAAoB;IAEzB,KAAKd,aAAa,WAAW;GAC/B;EACF,CAAC,IACD,KAAA;EAMJ,KAAKD,kBACH,KAAK,wBAAwB,KAAKD,mBAC9B,IAAI,8BACF,UACM,KAAKA,kBAAkB,kBACvB,KAAKJ,WAAW,QAAQ,GAC9B,KAAK,oBACP,IACA,KAAA;EACN,KAAKO,SAAS,IAAI,cAAc;GAC9B,kBAAkB,KAAKR;GACvB,aAAa,cAAc,sBAAsB;EACnD,CAAC;EACD,MAAM,+BACJ,KAAK,wBAAwB,gBAC5B,CAAC,KAAKA,sBACH,KAAK,uBAAuB,cAC5B,KAAA;EACN,MAAM,gBAAgB,+BAClB,4BAA4B,IAAI,4BAA4B,IAC5D,KAAA;EACJ,KAAKS,eAAe,IAAI,qBACtB,IACA;GACE,qBAAqB,KAAKT;GAC1B,iBAAiB,kBAAkB,KAAA;EACrC,GACA;GACE,mBACE,KAAKD,QAAQ,uCAAuC;GAGtD,eAAe,KAAK,aAAa,EAAE;GAGnC,qBAAqB,YAAY,SAC/B,aACI,KAAKM,kBAAkB,UAAU,UAAU,IAC3C,KAAKA,kBAAkB,iBAAiB,IAAI;GAClD,iBAAiB,KAAKA,kBAAkB;EAC1C,CACF;EACA,KAAKD,wBAAwB,KAAK,gBAC9B;GACE,GAAG,KAAK;GAGR,cACE,KAAK,cAAc,gBACnB,KAAKE,iBAAiB;EAC1B,IACA,KAAA;EAEJ,KAAKC,cACH,KAAKP,uBACL,KAAK,0BACL,KAAK,yBACL,KAAK,gBACD,IAAI,0BACF,IACA,OACA,KAAK,cAAc,eACnB,KAAK,sBAAsB,UAC3B,KAAKD,SACL;GAAC;GAAc,GAAG,KAAK;EAAsB,CAC/C,IACA,KAAA;EACN,KAAKuB,aAAa;EAClB,KAAKX,aAAa;EAClB,KAAKC,SAAS,IAAI,aAAa,KAAK,IAAI,KAAA,GAAW,YAAY;EAC/D,KAAKW,gBAAgB,EAAC,KAAK,SAAQ;CACrC;CAEA,MAAM,MAAM;EACV,KAAK7B,IAAI,OAAO,wBAAwB;EAExC,KAAKO,WAAW,qBAAqB;EAErC,MAAM,gBAAgB,MAAM,KAAKH,QAAQ,iBAAiB;EAC1D,IAAI,eAAe;GACjB,KAAKyB,cAAc,YAAY,EAC7B,gBAAgB,cAAc,eAChC;GAKA,KAAKC,+BAA+B,cAAc;EACpD;EAIA,MAAM,KAAKzB,QAAQ,gBAAgB,KAAKuB,UAAU;EAClD,KAAKA,aAAa;EAIlB,MAAM,sBAAsB,wBAAwB,KAAK;EAEzD,OAAO,KAAKV,OAAO,UAAU,GAAG;GAC9B,IAAI;GACJ,IAAI,YAA2B;GAC/B,IAAI,iBAAiB;GACrB,IAAI;IAIF,MAAM,EAAC,eAAe,qBACpB,MAAM,KAAKH,aAAa,WAAW;IAIrC,KAAKgB,gCAAgC;IACrC,MAAM,SAAS,MAAM,KAAK3B,QAAQ,YAChC,eACA,gBACF;IACA,IAAI,KAAKE,qBACP,KAAKD,QAAQ,IAAI,EAAE,OAAM,MAAK,OAAO,QAAQ,OAAO,CAAC,CAAC;IAGxD,KAAK2B,UAAU;IACf,IACE,KAAKd,OAAO,aAAa,IACzB,6CAIA,KAAKT,4BAA4B,QAC/B,KAAKT,KACL,eACA,oBAAoB,eACtB;IAEF,YAAY;IAEZ,KAAKc,OAAO,MAAM,OAAO,IAAI;IAC7B,WAAW,MAAM,UAAU,OAAO,SAAS;KACzC,KAAKA,OAAO,gBAAgB,MAAM;KAElC,MAAM,CAAC,MAAM,OAAO;KACpB,QAAQ,MAAR;MACE,KAAK;OACH,IACE,IAAI,aACJ,IAAI,UAAU,YAAY,gBACxB,KAAKgB,8BACP;QAKA,KAAKD,cAAc,YAAY,IAAI;QACnC,KAAKC,+BACH,IAAI,UAAU,YAAY;QAC5B,KAAKvB,WAAW,WAAW,KAAKsB,aAAa;OAC/C;OACA;MACF,KAAK;OACH,MAAM,KAAKI,sBAAsB,GAAG;OACpC;MACF,KAAK;OACH,YAAY,OAAO,GAAG;OACtB;MACF,KAAK;OACH,IAAI,cAAc,OAAO,GAAG,WAC1B,MAAM,IAAI,mBACR,oBAAoB,OAAO,GAAG,UAAU,oCAAoC,WAC9E;OAEF,KAAKb,WAAW,IAAI,CAAC;OACrB;MACF;OACE,IAAI,SAAS,QACX,KAAKC,eAAe,IAAI,CAAC;OAE3B,IAAI,cAAc,MAChB,MAAM,IAAI,mBACR,GAAG,KAAK,WAAW,IAAI,IAAI,kCAC7B;OAEF;KACJ;KAEA,MAAM,aAAa,oCAAoC,MAAM;KAC7D,MAAM,EAAC,SAAQ;KACf,IAAI,KAAKf,qBACP,KAAKD,QAAQ,MAAM,WAAW,QAAQ,UAAU;KAWlD,KAAKM,kBAAkB,MAAM,QAAQ,MAAM,WAAW,MAAM;KAC5D,MAAM,QAA2B;MAAC;MAAW,OAAO,GAAG;MAAK;KAAI;KAChE,kBAAkB,KAAK;KACvB,IAAI,iBAAiB,qBAAqB;MAExC,KAAKJ,WAAW,QAAQ,KAAK;MAC7B,KAAK2B,oCAAoC,MAAM,MAAM,EAAE;KACzD,OAAO;MAQL,MAAM,YAAY,KAAK3B,WAAW,uBAAuB,KAAK;MAK9D,KAAK2B,oCAAoC,MAAM,MAAM,EAAE;MACvD,MAAM,eACJ,WACA,OAAO,QAAQ,QACf,KAAKhB,OAAO,MACd;MACA,iBAAiB;KACnB;KAEA,IAAI,SAAS,YAAY,SAAS,YAChC,YAAY;KAId,MAAM,eAAe,KAAKZ,sBACtB,KAAKD,QAAQ,aAAa,IAC1B,KAAA;KACJ,IAAI,cACF,MAAM,eACJ,cACA,OAAO,QAAQ,QACf,KAAKa,OAAO,MACd;IAEJ;GACF,SAAS,GAAG;IACV,MAAM;GACR,UAAU;IACR,KAAKc,SAAS,QAAQ,OAAO;IAC7B,KAAKA,UAAU,KAAA;GACjB;GAGA,IAAI,WAAW;IACb,KAAKhC,IAAI,OAAO,oCAAoC,WAAW;IAC/D,IAAI,KAAKM,qBACP,KAAKD,QAAQ,MAAM;IAKrB,KAAKM,kBAAkB,MAAM;IAG7B,KAAKC,iBAAiB,aAAa;IACnC,KAAKL,WAAW,QAAQ;KAAC;KAAW;KAAY;IAAa,CAAC;IAC9D,KAAK2B,oCAAoC,YAAY,SAAS;GAChE;GAGA,MAAM,QAAQ,IAAI;IAChB,KAAK7B,QAAQ,KAAK;IAClB,KAAKa,OAAO,QAAQ,KAAKlB,KAAK,GAAG;IACjC,KAAKkB,OAAO,aAAa,8CACrB,qBACE,KAAKlB,KACL,uBAAuB,KAAKA,KAAK,eAAe,GAAG,CACrD,IACA;GACN,CAAC;EACH;EAEA,KAAKO,WAAW,oBAAoB;EACpC,KAAKP,IAAI,OAAO,wBAAwB;CAC1C;CAEA,MAAMiC,sBAAsB,KAA6B;EACvD,KAAKjC,IAAI,OAAO,4BAA4B,GAAG;EAC/C,MAAM,EAAC,QAAO;EAEd,QAAQ,KAAR;GACE,KAAK;IACH,MAAM,kBACJ,KAAKE,kBAAkB,yBAAyB,CAAC,GACjD,KAAKD,MACP;IACA,MAAM,wBACJ,KAAKD,KACL,eACA,IAAI,WAAW,mBACf,IAAI,YACN;IACA,IAAI,KAAKiB,YAAY;KACnB,KAAKjB,IAAI,OAAO,8BAA8B;KAC9C,MAAM,KAAK,KAAK,IAAI,gBAAgB,CAAC;IACvC;IACA;GACF,SACE,YAAY,GAAG;EACnB;CACF;CAEA,MAAM,UAAU,KAAiD;EAC/D,MAAM,EAAC,iBAAiB,IAAI,MAAM,gBAAgB,cAAa;EAC/D,IAAI,SAAS,WACX,KAAKmB,SAAS,QAAQ;EAExB,IAAI,0BAA0B,CAAC;EAC/B,MAAM,aAAa,aAAa,OAAe,EAC7C,eAAe,kBAAkB,EACnC,CAAC;EAID,MAAM,aAAa,IAAI,WACrB,iBACA,IACA,MACA,WACA,kBACM,KAAKU,eACX,CAAC,CACH;EACA,MAAM,KAAK,KAAK7B,IAAI,YAAY,cAAc,WAAW,EAAE;EAC3D,MAAM,4BAA4B;GAChC,GAAG,OAAO,uBAAuB,WAAW,IAAI;GAChD,KAAKO,WAAW,OAAO,UAAU;EACnC;EACA,oBAAoB;EACpB,IAAI,mBAAmB,KAAKJ,iBAAiB;GAC3C,GAAG,OAAO,2CAA2C,gBAAgB;GACrE,WAAW,MACT,GACA,8BACE,KAAKA,gBACN,cAAc,eAAe,EAChC;EACF,OAAO;GACL,GAAG,OAAO,qBAAqB,WAAW,IAAI;GAE9C,MAAM,sBAAsB;IAC1B,OACE,KAAKG,qBACL,+CACF;IAGA,oBAAoB;IACpB,KAAKC,WAAW,IAAI,UAAU;IAC9B,KAAKF,QAAQ,QAAQ,YAAY,IAAI;GACvC;GACA,MAAM,iBAAiB,KAAK8B,qBAAqB,IAAI,GAAG;GACxD,IAAI,CAAC,gBACH,cAAc;QACT,IAAI,eAAe,SAAS,YAAY;IAC7C,GAAG,OACD,GACE,eAAe,WAAW,cAAc,SACzC,oBAAoB,IAAI,GAAG,IAAI,eAAe,SACjD;IACA,IAAI,eAAe,UACjB,WAAW,MAAM,GAA2B,eAAe,OAAO;SAElE,WAAW,MAAM;GAErB,OAAO;IACL,MAAM,EAAC,SAAS,QAAQ,UAAU,YAAW;IAC7C,0BAA0B,QAAQ,OAAO,UAAU;IACnD,MAAM,eAAe,MAAM,QAAQ,QACjC,kBACM,KAAKC,qBAAqB,GAChC,EAAC,QAAO,CACV;IACA,QAAQ,aAAa,MAArB;KACE,KAAK;MACH,GAAG,QACD,WAAW,IAAI,GAAG,uBAClB,GAAI,WAAW,CAAC,EAAC,yBAAyB,SAAQ,CAAC,IAAI,CAAC,CAC1D;MACA,KAAKC,oBAAoB,UAAU,MAAM;MACzC;KACF,KAAK;MACH,IAAI,KAAK/B,qBAAqB;OAC5B,GAAG,OACD,WAAW,IAAI,GAAG,yCACb,IAAI,UAAU,4CACjB,aAAa,YACjB;OACA,KAAK+B,oBAAoB,MAAM,qBAAqB;OACpD,cAAc;MAChB,OAAO;OACL,MAAM,UACJ,mCACG,aAAa,aAAa,cAAc,IAAI,UAAU;OAC3D,GAAG,OACD,qCAAqC,IAAI,UAAU,oCAE9C,aAAa,aAAa,mCACjC;OACA,KAAKA,oBAAoB,QAAQ,qBAAqB;OACtD,WAAW,MAAM,GAA2B,OAAO;MACrD;MACA;KACF,KAAK;MAOH,IAAI,KAAK/B,qBAAqB;OAC5B,GAAG,QACD,WAAW,IAAI,GAAG,uDAElB,aAAa,KACf;OACA,KAAK+B,oBAAoB,MAAM,qBAAqB;OACpD,cAAc;MAChB,OAAO;OACL,GAAG,QACD,2BAA2B,IAAI,GAAG,kFAElC,aAAa,KACf;OACA,KAAKA,oBAAoB,QAAQ,qBAAqB;OACtD,WAAW,KAAK,aAAa,KAAK;MACpC;MACA;KACF,KAAK;MAIH,KAAKA,oBAAoB,UAAU,sBAAsB;MACzD;KACF,SACE,YAAY,YAAY;IAC5B;GACF;EACF;EAGA,KAAK7B,eAAe,MAAM,IAAI,MAAM;EACpC,OAAO;CACT;CAEA,MAAM,yBACJ,QACkC;EAClC,IAAI,CAAC,KAAKA,eACR,MAAM,IAAI,MAAM,4BAA4B;EAE9C,MAAM,aAAa,KAAKA,cAAc,KAAK,MAAM;EAEjD,IAAI;GAGF,OAAO,KAAKI,iBAAiB,MAAM,MAAM,KAAK;GAI9C,IAAI,CAAC,KAAKJ,cAAc,UAAU,QAAQ,UAAU,GAClD,OAAO;GAKT,KAAKQ,aAAa,IAAI,MAAM;GAE5B,MAAM,KAAKsB,qBAAqB;GAChC,OAAO;EACT,SAAS,GAAG;GAOV,WAAW,OAAO;GAClB,MAAM;EACR;CACF;CAEA,qBAAqB,WAAmB;EACtC,KAAKC,mBAAmB;EACxB,KAAKzB,OAAO,YAAY,SAAS;EAIjC,KAAK0B,oBAAoB,UAAU;EACnC,KAAKC,sBAAsB;EAC3B,KAAKC,0BAA0B;EAK/B,KAAUJ,qBAAqB,EAAE,OAAM,MACrC,KAAKtC,IAAI,OAAO,yCAAyC,CAAC,CAC5D;CACF;CAEA,MAAMsC,uBAAuB;EAC3B,MAAM,kBAAkB,KAAKC;EAC7B,MAAM,eAAe,KAAK/B;EAC1B,IACE,oBAAoB,KAAA,KACpB,CAAC,cAAc,sBAAsB,GAErC;EASF,MAAM,UAAU,KAAKF,sBACjB,MAAM,KAAKqC,mBAAmB,IAC9B,KAAA;EACJ,KAAK,MAAM,UAAU,aAAa,mBAAmB,GAAG;GACtD,IAAI,QAAQ,KAAK3B,aAAa,KAAK,MAAM;GAGzC,IAAI,KAAKA,eAAe,UAAU,KAAA,GAChC;GAOF,IAAI,CAAC,KAAKV,uBAAuB,OAAO,WAAW,MAAM;IACvD,KAAKU,aAAa,QAAQ,MAAM;IAChC,QAAQ,KAAKA,aAAa,IAAI,MAAM;IACpC,IAAI,OAAO,WAAW,MAAM;KAC1B,IAAI,aAAa,wBAAwB,MAAM,GAC7C,KAAKS,0BAA0B,IAAI,CAAC;KAEtC,KAAKzB,IAAI,OACP,qCAAqC,OAAO,qEAE9C;KACA;IACF;GACF;GAEA,IAAI,OAAO,WAAW,UAAU;IAC9B,MAAM,WAAW,KACf,MAAM,UACN,oDACF;IACA,IACE,KAAKM,uBACL,SAAS,eAAe,iBACxB;KAIA,KAAKN,IAAI,OACP,YAAY,OAAO,4CACd,SAAS,aAAa,mCACzB,eACJ;KACA,KAAKwB,sBAAsB,IAAI,CAAC;KAChC,QAAQ,KAAK,KAAKR,WAAW,EAAE,OAAO,MAAM;IAC9C;GACF;GAEA,MAAM,SAAS,OAAO,UAAU;GAChC,IAAI;GACJ,IAAI,OAAO,WAAW,UAGpB,eAAe,KACb,MAAM,UACN,oDACF,EAAE;QAEF,eAAe,KACb,SACA,mDACF,EAAE;GAGJ,IAAI,gBAAgB,iBAClB,aAAa,WACX,QACA,KAAKb,iBACL,iBACA,MACF;QACK;IAIL,IAAI,aAAa,wBAAwB,MAAM,GAC7C,KAAKsB,0BAA0B,IAAI,CAAC;IAEtC,KAAKzB,IAAI,QACP,GAAG,OAAO,2BAA2B,aAAa,iCAC7B,gBAAgB,mEAEvC;GACF;EACF;CACF;CAEA,wBAA8B;EAC5B,MAAM,kBAAkB,KAAKuC;EAC7B,IACE,CAAC,KAAKjC,uBACN,KAAKsC,qBACL,KAAKC,mBACL,oBAAoB,KAAA,KACpB,KAAKC,sBAAsB,iBAE3B;EAEF,KAAKF,oBAAoB;EACzB,KAAK1B,OAAO,iBAAiB;GAC3B,KAAK0B,oBAAoB;GACzB,KAAKC,kBAAkB;GACvB,OAAO,KAAKE,kBAAkB,EAAE,cAAc;IAC5C,KAAKF,kBAAkB;IACvB,KAAKJ,sBAAsB;GAC7B,CAAC;EACH,GAAG,gBAAgB;CACrB;CAEA,4BAAkC;EAChC,IACE,KAAKO,yBACL,KAAKC,uBACL,KAAKV,qBAAqB,KAAA,KAC1B,KAAKW,6BAA6B,KAAA,GAElC;EAEF,MAAM,QACJ,KAAKA,6BAA6B,cAAc,IAAI;EACtD,KAAKF,wBAAwB;EAC7B,KAAK9B,OAAO,iBAAiB;GAC3B,KAAK8B,wBAAwB;GAC7B,KAAKC,sBAAsB;GAC3B,OAAO,KAAKE,sBAAsB,EAAE,cAAc;IAChD,KAAKF,sBAAsB;IAC3B,KAAKP,0BAA0B;GACjC,CAAC;EACH,GAAG,KAAK;CACV;CAEA,MAAMC,qBAGH;EACD,MAAM,eAAe,MAAM,KAAKtC,QAAQ,0BAA0B;EAClE,IAAI,CAAC,cACH,KAAKL,IAAI,OACP,8EACF;EAEF,OAAO;GACL,gBAAgB,KAAKG;GACrB,cAAc,gBAAgB,KAAKA;EACrC;CACF;CAEA,mBAIE;EACA,MAAM,kBAAkB,KAAKoC;EAC7B,OACE,oBAAoB,KAAA,GACpB,+CACF;EACA,MAAM,UAAU,CACd,GAAG,KAAKhC,WAAW,QAAQ,GAC3B,GAAI,KAAKC,eAAe,sBAAsB,KAAK,CAAC,CACtD;EAKA,OAAO;GACL;GACA,gBAAgB,IAAI,iBAAiB,GAAG,OAAO;GAC/C;EACF;CACF;CAEA,MAAMuC,oBAAmC;EACvC,IAAI;GACF,MAAM,EAAC,iBAAiB,gBAAgB,YACtC,KAAKK,iBAAiB;GACxB,IAAI,iBAAiB;QACf,KAAKC,2BAA2B,gBAAgB;KAClD,KAAKA,yBAAyB;KAC9B,KAAKrD,IAAI,OACP,wCAAwC,mBACxC,EAAC,YAAY,QAAO,CACtB;IACF;UAEA,KAAKqD,yBAAyB,KAAA;GAEhC,IAAI,kBAAkB,KAAKP,oBACzB;GAEF,KAAK9C,IAAI,OAAO,6BAA6B,eAAe,KAAK;GACjE,MAAM,QAAQ,YAAY,IAAI;GAC9B,MAAM,UAAU,MAAM,KAAKK,QAAQ,mBAAmB,cAAc;GACpE,MAAM,WAAW,YAAY,IAAI,IAAI,OAAO,QAAQ,CAAC;GACrD,KAAKL,IAAI,OACP,UAAU,QAAQ,qBAAqB,eAAe,IAAI,QAAQ,KACpE;GACA,KAAK8C,qBAAqB;EAC5B,SAAS,GAAG;GACV,KAAK9C,IAAI,OAAO,mCAAmC,CAAC;EACtD;CACF;CAEA,MAAMmD,wBAAuC;EAC3C,MAAM,YAAY,KAAKvC;EACvB,IAAI,CAAC,WACH;EAEF,IAAI;GACF,MAAM,EAAC,iBAAiB,mBAAkB,KAAKwC,iBAAiB;GAIhE,KAAKF,2BAA2B,KAAA;GAChC,IAAI,iBAAiB,iBAInB,KAAKV,oBAAoB,UAAU;GAErC,MAAM,SAAS,MAAM,UAAU,MAAM,cAAc;GACnD,KAAKA,oBAAoB,OAAO,YAAY;EAC9C,SAAS,GAAG;GAGV,KAAKA,oBAAoB,UAAU;GACnC,KAAKxC,IAAI,OAAO,uCAAuC,CAAC;EAC1D;CACF;CAEA,oBAAoB,cAAmD;EACrE,IAAI,CAAC,KAAKY,mBAAmB,iBAAiB,KAAA,GAC5C;EAEF,IACE,iBAAiB,eACjB,KAAKsC,6BAA6B,KAAA,GAElC,KAAKA,2BAA2B;CAEpC;CAEA,MAAM,KAAK,KAAe;EACxB,KAAKhC,OAAO,KAAK,KAAKlB,KAAK,GAAG;EAC9B,KAAKgC,SAAS,QAAQ,OAAO;EAC7B,KAAKpB,iBAAiB,KAAK;EAC3B,KAAKC,aAAa,KAAK;EACvB,KAAKa,gBAAgB,MAAM;EAC3B,KAAKf,kBAAkB,MAAM;EAC7B,MAAM,QAAQ,WAAW,CAAC,KAAKN,QAAQ,KAAK,GAAG,KAAKD,QAAQ,KAAK,CAAC,CAAC;CACrE;CAEA,oCACE,MACA,WACA;EACA,QAAQ,MAAR;GACE,KAAK;IACH,OACE,KAAKkD,kCAAkC,KAAA,GACvC,4DACF;IACA,KAAKA,gCACH,SAAyC;IAC3C,KAAKC,+BAA+B,YAAY,IAAI;IACpD;GACF,KAAK,UAAU;IACb,MAAM,aAAa,KAAKD;IACxB,OAAO,YAAY,gDAAgD;IACnE,KAAKE,kCAAkC;IACvC,KAAKzB,gCAAgC;IACrC,KAAKuB,gCAAgC,KAAA;IACrC,WAAW,QAAQ;KAAC,MAAM;KAAa;IAAS,CAAC;IACjD;GACF;GACA,KAAK,YAAY;IACf,MAAM,aAAa,KAAKA;IACxB,OAAO,YAAY,kDAAkD;IACrE,MAAM,YAAY,KAAKvB;IACvB,OAAO,WAAW,oDAAoD;IACtE,KAAKwB,+BAA+B,KAAA;IACpC,KAAKD,gCAAgC,KAAA;IACrC,WAAW,QAAQ;KAAC,MAAM;KAAe,WAAW;IAAS,CAAC;IAC9D;GACF;EACF;CACF;CAEA,oCAAoC;EAClC,MAAM,YAAY,KAAKC;EACvB,KAAKA,+BAA+B,KAAA;EACpC,IAAI,cAAc,KAAA,GAChB,KAAKjC,4BAA4B,SAAS,YAAY,IAAI,IAAI,WAAW,EACvE,mBAAmB,KAAKX,kBAAkB,UAAU,OAAO,MAC7D,CAAC;CAEL;;;;;;CAOA,sBAAsB;EACpB,KAAKe,gBAAgB,MAAM;EAC3B,KAAKA,iBAAiB,KAAA;CACxB;CAEA,uBAAiD;EAC/C,MAAM,YAAY,KAAKK;EACvB,OAAO,WAAW,oDAAoD;EACtE,OACE,KAAKuB,+BAA+B,QAAQ,MAC1C,eAAc,WAAW,SAC3B,KAAK;CAET;CAEA,qBACE,IACA,KACmC;EACnC,MAAM,OAAO,KAAK5C;EAClB,IAAI,KAAKqB,kCAAkC,KAAA,KAAa,CAAC,MACvD,OAAO,KAAK0B,kBACV,aACA,+DACF;EAMF,IAAI,IAAI,SAAS,YAAY,KAAKnD,qBAAqB;GACrD,GAAG,OAAO,iCAAiC,IAAI,GAAG,qBAAqB;GACvE,OAAO,KAAKmD,kBACV,mBACA,6DACF;EACF;EAEA,IAAI;EACJ,IAAI,KAAKzC,aAAa;GACpB,QAAQ,KAAKA,YAAY,QAAQ,IAAI,MAAM;GAC3C,IAAI,MAAM,WAAW,MAAM;IACzB,IAAI,MAAM,WAAW,YACnB,GAAG,OACD,WAAW,IAAI,GAAG,uDAClB,EAAC,yBAAyB,MAAM,SAAQ,CAC1C;SACK,IAAI,MAAM,WAAW,mBAC1B,KAAK0C,sBACH,IACA,KACA,MACA,GAAG,KAAK,cAAc,gCACxB;SAEA,GAAG,QACD,WAAW,IAAI,GAAG,iCAAiC,MAAM,UACzD,GAAI,MAAM,WACN,CAAC,EAAC,yBAAyB,MAAM,SAAQ,CAAC,IAC1C,CAAC,CACP;IAEF,OAAO,KAAKD,kBACV,MAAM,QACN,wBAAwB,MAAM,OAAO,kDAEvC;GACF;GACA,MAAM,WAAW,MAAM;GACvB,OAAO,UAAU,6CAA6C;GAC9D,IAAI,IAAI,YAAY,SAAS,cAAc;IACzC,GAAG,OACD,WAAW,IAAI,GAAG,yCACb,IAAI,UAAU,4CACjB,SAAS,cACX,EAAC,yBAAyB,SAAQ,CACpC;IACA,OAAO,KAAKA,kBACV,uBACA,mCAAmC,SAAS,aAAa,cACzC,IAAI,UAAU,IAC9B,IACF;GACF;EACF;EAIA,IAAI,CAAC,KAAKzC,eAAe,CAAC,KAAK,YAAY,GAAG,GAC5C,OAAO,KAAKyC,kBACV,YACA,mEACF;EAEF,IAAI,KAAKzC,eAAe,KAAK,aAAa,CAAC,KAAK,UAAU,GAAG,GAC3D,OAAO,KAAKyC,kBACV,YACA,mEACF;EAGF,MAAM,UACJ,KAAK/B,kBAAkB,KAAKiC,mBAAmB,IAAI,MAAM,GAAG;EAC9D,IAAI,SACF,OAAO;GACL,MAAM;GACN;GACA,QAAQ,OAAO,UAAU;GACzB,UAAU,OAAO;GAEjB,SACE,UAAU,KAAA,IAAY,KAAA,IAAY,MAAM,WAAW;EACvD;OAEA,OAAO,KAAKF,kBACV,mBACA,oEACF;CAEJ;CAEA,kBACE,QACA,SACA,WAAW,OACyB;EACpC,KAAKpB,oBAAoB,KAAK/B,sBAAsB,OAAO,QAAQ,MAAM;EACzE,OAAO,KAAKA,sBACR,KAAA,IACA;GAAC,MAAM;GAAY;GAAS;EAAQ;CAC1C;CAEA,oBAAoB,QAAkC,QAAsB;EAC1E,KAAKiB,eAAe,IAAI,GAAG;GAAC;GAAQ;EAAM,CAAC;CAC7C;;;;;;;;;;;;;;;CAgBA,mBACE,IACA,MACA,KACoC;EACpC,IAAI;EACJ,IAAI;GACF,SAAS,IAAI,sBAAsB,KAAKvB,KAAK,KAAK,aAAa;GAK/D,IAAI,OAAO,KAAK,IAAI,SAAS,EAAE,SAAS,aAAa;IACnD,OAAO,MAAM;IACb,KAAK0D,sBACH,IACA,KACA,MACA,GAAG,KAAK,cAAc,6BACxB;IACA;GACF;EACF,SAAS,GAAG;GAIV,QAAQ,MAAM;GACd,KAAKA,sBACH,IACA,KACA,MACA,eAAe,KAAK,iBACpB,CACF;GACA;EACF;EACA,KAAKE,6BAA6B,KAAA;EAKlC,KAAKlC,iBAAiB,IAAI,uBACxB,KAAK1B,KACL,KAAKO,YACL,QACA;GACE,WAAW,KAAK;GAChB,kBAAkB,KAAK;GACvB,uBAAuB,KAAK;GAC5B,cAAc,KAAK;GACnB,YAAW,YAAW;IACpB,KAAKP,IAAI,OACP,8CAA8C,SAChD;IACA,KAAKgB,aAAa,KAAK;GACzB;EACF,CACF;EACA,OAAO,KAAKU;CACd;;;;;;CAOA,sBACE,IACA,KACA,MACA,QACA,OACM;EACN,MAAM,MAAM,KAAK,IAAI;EACrB,KAAKkC,+BAA+B;EACpC,MAAM,gBAAgB,MAAM,KAAKA;EAIjC,GAAG,kBAFD,KAAK,2BACL,oDAC8B,SAAS,YACtC,KAAKtD,sBACF,WAAW,IAAI,GAAG,sBAClB,gBAAgB,IAAI,GAAG,2BACzB,GAAG,OAAO,oBACU,cAAc,OACpC,GAAI,UAAU,KAAA,IAAY,CAAC,IAAI,CAAC,KAAK,CACvC;CACF;AACF;AA0CA,IAAM,mBAAmB,6BAA6B;AAEtD,IAAM,gBAAgB,KAAK,UAAU,CACnC,YACA,EAAC,KAAK,WAAU,CAClB,CAAoB"}
|
|
@@ -48,7 +48,7 @@ interface PurgeLock {
|
|
|
48
48
|
}
|
|
49
49
|
export declare function setupCDCTables(lc: LogContext, db: postgres.TransactionSql, shard: ShardID): Promise<void>;
|
|
50
50
|
export declare function markResetRequired(sql: PostgresDB, shard: ShardID): Promise<void>;
|
|
51
|
-
export declare function ensureReplicationConfig(lc: LogContext, db: PostgresDB, subscriptionState: Pick<SubscriptionState, 'publications' | 'replicaVersion' | 'watermark'>, shard: ShardID, autoReset: boolean, purgeLock?: PurgeLock, setTimeoutFn?: typeof setTimeout): Promise<void>;
|
|
51
|
+
export declare function ensureReplicationConfig(lc: LogContext, db: PostgresDB, subscriptionState: Pick<SubscriptionState, 'publications' | 'replicaVersion' | 'watermark'>, shard: ShardID, autoReset: boolean, purgeLock?: PurgeLock, setTimeoutFn?: typeof setTimeout, pgChangeLogEnabled?: boolean): Promise<void>;
|
|
52
52
|
export declare const CHANGE_STREAMER_APP_NAME = "zero-change-streamer";
|
|
53
53
|
export declare class AutoResetSignal extends AbortError {
|
|
54
54
|
readonly name = "AutoResetSignal";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-streamer/schema/tables.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AAErC,OAAO,EAAC,UAAU,EAAC,MAAM,0CAA0C,CAAC;AAGpE,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAY,KAAK,OAAO,EAAC,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EACV,UAAU,EACV,MAAM,EACN,aAAa,EACd,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,8CAA8C,CAAC;AAOpF,eAAO,MAAM,SAAS,QAAQ,CAAC;AAM/B,MAAM,MAAM,cAAc,GAAG;IAG3B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAmBF;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAAC;AAEF,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,UASzD;AAED,wBAAsB,6BAA6B,CACjD,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAIxB;AAED;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC,CAAC;AAaF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,UAiBlD;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,UAAU,CAAC;CACtB,CAAC;AAYF,UAAU,SAAS;IACjB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,QAAQ,CAAC,cAAc,EAC3B,KAAK,EAAE,OAAO,iBAIf;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,iBAKtE;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,UAAU,EACd,iBAAiB,EAAE,IAAI,CACrB,iBAAiB,EACjB,cAAc,GAAG,gBAAgB,GAAG,WAAW,CAChD,EACD,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,OAAO,EAClB,SAAS,CAAC,EAAE,SAAS,EACrB,YAAY,GAAE,OAAO,UAAuB,
|
|
1
|
+
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-streamer/schema/tables.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AAErC,OAAO,EAAC,UAAU,EAAC,MAAM,0CAA0C,CAAC;AAGpE,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAY,KAAK,OAAO,EAAC,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EACV,UAAU,EACV,MAAM,EACN,aAAa,EACd,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,8CAA8C,CAAC;AAOpF,eAAO,MAAM,SAAS,QAAQ,CAAC;AAM/B,MAAM,MAAM,cAAc,GAAG;IAG3B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAmBF;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAAC;AAEF,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,UASzD;AAED,wBAAsB,6BAA6B,CACjD,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAIxB;AAED;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC,CAAC;AAaF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,UAiBlD;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,UAAU,CAAC;CACtB,CAAC;AAYF,UAAU,SAAS;IACjB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,QAAQ,CAAC,cAAc,EAC3B,KAAK,EAAE,OAAO,iBAIf;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,iBAKtE;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,UAAU,EACd,iBAAiB,EAAE,IAAI,CACrB,iBAAiB,EACjB,cAAc,GAAG,gBAAgB,GAAG,WAAW,CAChD,EACD,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,OAAO,EAClB,SAAS,CAAC,EAAE,SAAS,EACrB,YAAY,GAAE,OAAO,UAAuB,EAC5C,kBAAkB,UAAO,iBAgI1B;AAMD,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAE/D,qBAAa,eAAgB,SAAQ,UAAU;IAC7C,QAAQ,CAAC,IAAI,qBAAqB;CACnC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,OAAO,iBA4Cf"}
|
|
@@ -77,7 +77,7 @@ async function markResetRequired(sql, shard) {
|
|
|
77
77
|
UPDATE ${sql(cdcSchema(shard))}."replicationConfig"
|
|
78
78
|
SET "resetRequired" = true`;
|
|
79
79
|
}
|
|
80
|
-
async function ensureReplicationConfig(lc, db, subscriptionState, shard, autoReset, purgeLock, setTimeoutFn = setTimeout) {
|
|
80
|
+
async function ensureReplicationConfig(lc, db, subscriptionState, shard, autoReset, purgeLock, setTimeoutFn = setTimeout, pgChangeLogEnabled = true) {
|
|
81
81
|
const { publications, replicaVersion, watermark } = subscriptionState;
|
|
82
82
|
const replicaConfig = {
|
|
83
83
|
publications,
|
|
@@ -102,12 +102,12 @@ async function ensureReplicationConfig(lc, db, subscriptionState, shard, autoRes
|
|
|
102
102
|
lc.info?.(`Data in cdc tables @${replicaVersion} is incompatible with replica @${replicaConfig.replicaVersion}. Clearing tables.`);
|
|
103
103
|
purgeLock?.release();
|
|
104
104
|
needsTruncate = true;
|
|
105
|
-
stmts.push(sql`TRUNCATE TABLE ${sql(schema)}."replicationState"`, sql`TRUNCATE TABLE ${sql(schema)}."changeLog"
|
|
105
|
+
stmts.push(sql`TRUNCATE TABLE ${sql(schema)}."replicationState"`, ...pgChangeLogEnabled ? [sql`TRUNCATE TABLE ${sql(schema)}."changeLog"`] : [], sql`TRUNCATE TABLE ${sql(schema)}."replicationConfig"`, sql`TRUNCATE TABLE ${sql(schema)}."tableMetadata"`, sql`TRUNCATE TABLE ${sql(schema)}."backfilling"`);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
if (results.length === 0 || needsTruncate) {
|
|
109
109
|
const watermark = replicaConfig.replicaVersion;
|
|
110
|
-
const initialTx = [{
|
|
110
|
+
const initialTx = pgChangeLogEnabled ? [{
|
|
111
111
|
watermark,
|
|
112
112
|
pos: 0,
|
|
113
113
|
change: { tag: "begin" }
|
|
@@ -115,7 +115,7 @@ async function ensureReplicationConfig(lc, db, subscriptionState, shard, autoRes
|
|
|
115
115
|
watermark,
|
|
116
116
|
pos: 1,
|
|
117
117
|
change: { tag: "commit" }
|
|
118
|
-
}];
|
|
118
|
+
}] : [];
|
|
119
119
|
stmts.push(sql`INSERT INTO ${sql(schema)}."replicationConfig" ${sql(replicaConfig)}`, sql`INSERT INTO ${sql(schema)}."replicationState" ${sql(replicationState)}
|
|
120
120
|
ON CONFLICT (lock) DO UPDATE SET ${sql(replicationState)}`, ...initialTx.map((change) => sql`INSERT INTO ${sql(schema)}."changeLog" ${sql(change)}`));
|
|
121
121
|
if (needsTruncate) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-streamer/schema/tables.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {ident} from 'pg-format';\nimport type postgres from 'postgres';\nimport {type PendingQuery, type Row} from 'postgres';\nimport {AbortError} from '../../../../../shared/src/abort-error.ts';\nimport {equals} from '../../../../../shared/src/set-utils.ts';\nimport {runTx} from '../../../db/run-transaction.ts';\nimport {type PostgresDB} from '../../../types/pg.ts';\nimport {cdcSchema, type ShardID} from '../../../types/shards.ts';\nimport type {\n BackfillID,\n Change,\n TableMetadata,\n} from '../../change-source/protocol/current/data.ts';\nimport type {SubscriptionState} from '../../replicator/schema/replication-state.ts';\n\n// For readability in the sql statements.\nfunction schema(shard: ShardID) {\n return ident(cdcSchema(shard));\n}\n\nexport const PG_SCHEMA = 'cdc';\n\nfunction createSchema(shard: ShardID) {\n return /*sql*/ `CREATE SCHEMA IF NOT EXISTS ${schema(shard)};`;\n}\n\nexport type ChangeLogEntry = {\n // A strictly monotonically increasing, lexicographically sortable\n // value that uniquely identifies a position in the change stream.\n watermark: string;\n change: Change;\n};\n\ntype FullChangeLogEntry = ChangeLogEntry & {pos: number};\n\nfunction createChangeLogTable(shard: ShardID) {\n // Note: The \"change\" column used to be JSONB, but that was problematic in that\n // it does not handle the NULL unicode character.\n // https://vladimir.varank.in/notes/2021/01/you-dont-insert-unicode-null-character-as-postgres-jsonb/\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"changeLog\" (\n watermark TEXT,\n pos INT8,\n change JSON NOT NULL,\n precommit TEXT, -- Only exists on commit entries. Purely for debugging.\n PRIMARY KEY (watermark, pos)\n );\n`;\n}\n\n/**\n * Tracks the watermark from which to resume the change stream and the\n * current owner (task ID) acting as the single writer to the changeLog.\n */\nexport type ReplicationState = {\n lastWatermark: string;\n owner: string | null;\n ownerAddress: string | null;\n};\n\nexport function createReplicationStateTable(shard: ShardID) {\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"replicationState\" (\n \"lastWatermark\" TEXT NOT NULL,\n \"owner\" TEXT,\n \"ownerAddress\" TEXT,\n \"lock\" INTEGER PRIMARY KEY DEFAULT 1 CHECK (lock=1)\n );\n`;\n}\n\nexport async function discoverChangeStreamerAddress(\n shard: ShardID,\n sql: PostgresDB,\n): Promise<string | null> {\n const result = await sql<{ownerAddress: string | null}[]> /*sql*/ `\n SELECT \"ownerAddress\" FROM ${sql(cdcSchema(shard))}.\"replicationState\"`;\n return result[0].ownerAddress;\n}\n\n/**\n * This mirrors the analogously named table in the SQLite replica\n * (`services/replicator/schema/replication-state.ts`), and is used\n * to detect when the replica has been reset and is no longer compatible\n * with the current ChangeLog.\n */\nexport type ReplicationConfig = {\n replicaVersion: string;\n publications: readonly string[];\n};\n\nfunction createReplicationConfigTable(shard: ShardID) {\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"replicationConfig\" (\n \"replicaVersion\" TEXT NOT NULL,\n \"publications\" TEXT[] NOT NULL,\n \"resetRequired\" BOOL,\n \"lock\" INTEGER PRIMARY KEY DEFAULT 1 CHECK (lock=1)\n );\n`;\n}\n\nexport function createBackfillTables(shard: ShardID) {\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"tableMetadata\" (\n \"schema\" TEXT NOT NULL,\n \"table\" TEXT NOT NULL,\n \"metadata\" JSONB NOT NULL,\n PRIMARY KEY(\"schema\", \"table\")\n );\n\n CREATE TABLE ${schema(shard)}.\"backfilling\" (\n \"schema\" TEXT NOT NULL,\n \"table\" TEXT NOT NULL,\n \"column\" TEXT NOT NULL,\n \"backfill\" JSONB NOT NULL,\n PRIMARY KEY(\"schema\", \"table\", \"column\")\n );\n `;\n}\n\nexport type TableMetadataRow = {\n schema: string;\n table: string;\n metadata: TableMetadata;\n};\n\nexport type BackfillingColumn = {\n schema: string;\n table: string;\n column: string;\n backfill: BackfillID;\n};\n\nfunction createTables(shard: ShardID) {\n return (\n createSchema(shard) +\n createChangeLogTable(shard) +\n createReplicationStateTable(shard) +\n createReplicationConfigTable(shard) +\n createBackfillTables(shard)\n );\n}\n\ninterface PurgeLock {\n release(): Promise<void>;\n}\n\nexport async function setupCDCTables(\n lc: LogContext,\n db: postgres.TransactionSql,\n shard: ShardID,\n) {\n lc.info?.(`Setting up CDC tables`);\n await db.unsafe(createTables(shard));\n}\n\nexport async function markResetRequired(sql: PostgresDB, shard: ShardID) {\n const schema = cdcSchema(shard);\n await sql`\n UPDATE ${sql(schema)}.\"replicationConfig\"\n SET \"resetRequired\" = true`;\n}\n\nexport async function ensureReplicationConfig(\n lc: LogContext,\n db: PostgresDB,\n subscriptionState: Pick<\n SubscriptionState,\n 'publications' | 'replicaVersion' | 'watermark'\n >,\n shard: ShardID,\n autoReset: boolean,\n purgeLock?: PurgeLock,\n setTimeoutFn: typeof setTimeout = setTimeout,\n) {\n const {publications, replicaVersion, watermark} = subscriptionState;\n const replicaConfig = {publications, replicaVersion};\n const replicationState: ReplicationState = {\n lastWatermark: replicaVersion,\n owner: null,\n ownerAddress: null,\n };\n const schema = cdcSchema(shard);\n\n await runTx(db, async sql => {\n const stmts: PendingQuery<Row[]>[] = [];\n let needsTruncate = false;\n const results = await sql<\n {\n replicaVersion: string;\n publications: string[];\n resetRequired: boolean | null;\n }[]\n > /*sql*/ `\n SELECT \"replicaVersion\", \"publications\", \"resetRequired\" \n FROM ${sql(schema)}.\"replicationConfig\"`;\n\n if (results.length) {\n const {replicaVersion, publications} = results[0];\n if (\n replicaVersion !== replicaConfig.replicaVersion ||\n !equals(new Set(publications), new Set(replicaConfig.publications))\n ) {\n if (replicaConfig.replicaVersion !== watermark) {\n throw new AutoResetSignal(\n `Cannot reset change db@${replicaVersion} to ` +\n `service replica@${replicaConfig.replicaVersion} ` +\n `from watermark ${watermark}`,\n );\n }\n lc.info?.(\n `Data in cdc tables @${replicaVersion} is incompatible ` +\n `with replica @${replicaConfig.replicaVersion}. Clearing tables.`,\n );\n // Release any purge lock held by the caller; the changeLog is\n // incompatible with the replica and needs to be truncated.\n void purgeLock?.release();\n\n // Note: The order of TRUNCATE matters. The replicationState table is\n // truncated before the changeLog table, in order to acquire\n // (exclusive) locks on the tables in the same order that the\n // storer.ts acquires locks when writing the changeLog, namely:\n //\n // 1. SELECT ... FROM replicationState FOR UPDATE;\n // 2. INSERT INTO changeLog ...;\n // ...\n // n. UPDATE replicationState ...;\n needsTruncate = true;\n stmts.push(\n sql`TRUNCATE TABLE ${sql(schema)}.\"replicationState\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"changeLog\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"replicationConfig\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"tableMetadata\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"backfilling\"`,\n );\n }\n }\n // Initialize (or re-initialize TRUNCATED) tables\n if (results.length === 0 || needsTruncate) {\n // The storer uses the earliest changeLog entry as the safe watermark\n // from which subscribers can be resumed. These initial entries ensure\n // that subscribers can start from a freshly synced replica, even if\n // new changes have been replicated and not purged from the changeLog.\n //\n // TODO: Replace this with an explicit `firstWatermark` column in the\n // change db.\n const watermark = replicaConfig.replicaVersion;\n const initialTx: FullChangeLogEntry[] = [\n {watermark, pos: 0, change: {tag: 'begin'}},\n {watermark, pos: 1, change: {tag: 'commit'}},\n ];\n\n stmts.push(\n sql`INSERT INTO ${sql(schema)}.\"replicationConfig\" ${sql(replicaConfig)}`,\n sql`INSERT INTO ${sql(schema)}.\"replicationState\" ${sql(replicationState)} \n ON CONFLICT (lock) DO UPDATE SET ${sql(replicationState)}`,\n ...initialTx.map(\n change => sql`INSERT INTO ${sql(schema)}.\"changeLog\" ${sql(change)}`,\n ),\n );\n\n if (needsTruncate) {\n // The TRUNCATE statements require ACCESS EXCLUSIVE locks, which may\n // be blocked by old storer catchup reads. Race against a timeout\n // that terminates the blocking backends if the TRUNCATE takes too\n // long.\n const timer = setTimeoutFn(async () => {\n lc.info?.(\n 'ensureReplicationConfig blocked, terminating lock holders',\n );\n await terminateChangeDBLockHolders(lc, db, shard);\n }, LOCK_HOLDER_TERMINATE_TIMEOUT_MS);\n try {\n return await Promise.all(stmts);\n } finally {\n clearTimeout(timer);\n }\n }\n return Promise.all(stmts);\n }\n\n const {resetRequired} = results[0];\n if (resetRequired) {\n if (autoReset) {\n throw new AutoResetSignal('reset required by replication stream');\n }\n lc.error?.(\n '\\n\\n\\n' +\n 'Reset required but --auto-reset is not enabled.\\n' +\n 'This can happen for upstream databases that do not support event triggers.\\n' +\n 'To correct this, see https://zero.rocicorp.dev/docs/connecting-to-postgres#schema-changes' +\n '\\n\\n\\n',\n );\n }\n\n return [];\n });\n}\n\n// The time to wait for a TRUNCATE in ensureReplicationConfig before\n// terminating blocking backends via terminateChangeDBLockHolders.\nconst LOCK_HOLDER_TERMINATE_TIMEOUT_MS = 5_000;\n\nexport const CHANGE_STREAMER_APP_NAME = 'zero-change-streamer';\n\nexport class AutoResetSignal extends AbortError {\n readonly name = 'AutoResetSignal';\n}\n\n/**\n * Terminates zero-cache backends that are blocking the current backend\n * from acquiring locks on CDC tables (e.g., during TRUNCATE).\n *\n * This is used during change-DB takeover when the new replication-manager's\n * `ensureReplicationConfig` needs to TRUNCATE tables, but the old\n * replication-manager's storer is still reading from them (e.g., large\n * catchup cursors).\n *\n * The function:\n * 1. Finds backends waiting for a lock on a TRUNCATE in {schema}\n * 2. Uses `pg_blocking_pids()` to identify which backends are blocking them\n * 3. Terminates blocking backends that have `application_name = 'zero-change-streamer'`\n *\n * Must be called on a **separate connection** from the one that is blocked,\n * since the blocked connection is inside a pending transaction.\n */\nexport async function terminateChangeDBLockHolders(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardID,\n) {\n const schema = cdcSchema(shard);\n\n // Step 1: Find backends that are blocked waiting for a lock,\n // whose query involves a TRUNCATE on this shard's CDC schema.\n const blocked = await db<{pid: number}[]>`\n SELECT pid FROM pg_stat_activity\n WHERE wait_event_type = 'Lock'\n AND application_name = ${CHANGE_STREAMER_APP_NAME}\n AND query LIKE ${'%TRUNCATE%' + schema + '%'}`;\n\n if (blocked.length === 0) {\n lc.info?.('no blocked TRUNCATE backends found');\n return;\n }\n\n const blockedPids = blocked.map(r => r.pid);\n lc.info?.(`found blocked TRUNCATE backends: ${JSON.stringify(blockedPids)}`);\n\n // Step 2: For each blocked backend, find and terminate its blockers\n // that are zero-change-streamer connections.\n const terminated = await db<\n {pid: number; applicationName: string; query: string; terminated: boolean}[]\n >`\n SELECT pid, application_name as \"applicationName\", query,\n pg_terminate_backend(pid) as terminated\n FROM pg_stat_activity\n WHERE pid = ANY(\n SELECT unnest(pg_blocking_pids(blocked.pid))\n FROM unnest(${blockedPids}::int[]) AS blocked(pid)\n )\n AND application_name = ${CHANGE_STREAMER_APP_NAME}\n AND pid != ALL(${blockedPids}::int[])`;\n\n if (terminated.length === 0) {\n lc.info?.(`no ${CHANGE_STREAMER_APP_NAME} blockers found to terminate`);\n } else {\n for (const {pid, applicationName, query, terminated: ok} of terminated) {\n lc.info?.(\n `terminated blocking backend pid=${pid} app=${applicationName} ok=${ok} query=${query.slice(0, 200)}`,\n );\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tables.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-streamer/schema/tables.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {ident} from 'pg-format';\nimport type postgres from 'postgres';\nimport {type PendingQuery, type Row} from 'postgres';\nimport {AbortError} from '../../../../../shared/src/abort-error.ts';\nimport {equals} from '../../../../../shared/src/set-utils.ts';\nimport {runTx} from '../../../db/run-transaction.ts';\nimport {type PostgresDB} from '../../../types/pg.ts';\nimport {cdcSchema, type ShardID} from '../../../types/shards.ts';\nimport type {\n BackfillID,\n Change,\n TableMetadata,\n} from '../../change-source/protocol/current/data.ts';\nimport type {SubscriptionState} from '../../replicator/schema/replication-state.ts';\n\n// For readability in the sql statements.\nfunction schema(shard: ShardID) {\n return ident(cdcSchema(shard));\n}\n\nexport const PG_SCHEMA = 'cdc';\n\nfunction createSchema(shard: ShardID) {\n return /*sql*/ `CREATE SCHEMA IF NOT EXISTS ${schema(shard)};`;\n}\n\nexport type ChangeLogEntry = {\n // A strictly monotonically increasing, lexicographically sortable\n // value that uniquely identifies a position in the change stream.\n watermark: string;\n change: Change;\n};\n\ntype FullChangeLogEntry = ChangeLogEntry & {pos: number};\n\nfunction createChangeLogTable(shard: ShardID) {\n // Note: The \"change\" column used to be JSONB, but that was problematic in that\n // it does not handle the NULL unicode character.\n // https://vladimir.varank.in/notes/2021/01/you-dont-insert-unicode-null-character-as-postgres-jsonb/\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"changeLog\" (\n watermark TEXT,\n pos INT8,\n change JSON NOT NULL,\n precommit TEXT, -- Only exists on commit entries. Purely for debugging.\n PRIMARY KEY (watermark, pos)\n );\n`;\n}\n\n/**\n * Tracks the watermark from which to resume the change stream and the\n * current owner (task ID) acting as the single writer to the changeLog.\n */\nexport type ReplicationState = {\n lastWatermark: string;\n owner: string | null;\n ownerAddress: string | null;\n};\n\nexport function createReplicationStateTable(shard: ShardID) {\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"replicationState\" (\n \"lastWatermark\" TEXT NOT NULL,\n \"owner\" TEXT,\n \"ownerAddress\" TEXT,\n \"lock\" INTEGER PRIMARY KEY DEFAULT 1 CHECK (lock=1)\n );\n`;\n}\n\nexport async function discoverChangeStreamerAddress(\n shard: ShardID,\n sql: PostgresDB,\n): Promise<string | null> {\n const result = await sql<{ownerAddress: string | null}[]> /*sql*/ `\n SELECT \"ownerAddress\" FROM ${sql(cdcSchema(shard))}.\"replicationState\"`;\n return result[0].ownerAddress;\n}\n\n/**\n * This mirrors the analogously named table in the SQLite replica\n * (`services/replicator/schema/replication-state.ts`), and is used\n * to detect when the replica has been reset and is no longer compatible\n * with the current ChangeLog.\n */\nexport type ReplicationConfig = {\n replicaVersion: string;\n publications: readonly string[];\n};\n\nfunction createReplicationConfigTable(shard: ShardID) {\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"replicationConfig\" (\n \"replicaVersion\" TEXT NOT NULL,\n \"publications\" TEXT[] NOT NULL,\n \"resetRequired\" BOOL,\n \"lock\" INTEGER PRIMARY KEY DEFAULT 1 CHECK (lock=1)\n );\n`;\n}\n\nexport function createBackfillTables(shard: ShardID) {\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"tableMetadata\" (\n \"schema\" TEXT NOT NULL,\n \"table\" TEXT NOT NULL,\n \"metadata\" JSONB NOT NULL,\n PRIMARY KEY(\"schema\", \"table\")\n );\n\n CREATE TABLE ${schema(shard)}.\"backfilling\" (\n \"schema\" TEXT NOT NULL,\n \"table\" TEXT NOT NULL,\n \"column\" TEXT NOT NULL,\n \"backfill\" JSONB NOT NULL,\n PRIMARY KEY(\"schema\", \"table\", \"column\")\n );\n `;\n}\n\nexport type TableMetadataRow = {\n schema: string;\n table: string;\n metadata: TableMetadata;\n};\n\nexport type BackfillingColumn = {\n schema: string;\n table: string;\n column: string;\n backfill: BackfillID;\n};\n\nfunction createTables(shard: ShardID) {\n return (\n createSchema(shard) +\n createChangeLogTable(shard) +\n createReplicationStateTable(shard) +\n createReplicationConfigTable(shard) +\n createBackfillTables(shard)\n );\n}\n\ninterface PurgeLock {\n release(): Promise<void>;\n}\n\nexport async function setupCDCTables(\n lc: LogContext,\n db: postgres.TransactionSql,\n shard: ShardID,\n) {\n lc.info?.(`Setting up CDC tables`);\n await db.unsafe(createTables(shard));\n}\n\nexport async function markResetRequired(sql: PostgresDB, shard: ShardID) {\n const schema = cdcSchema(shard);\n await sql`\n UPDATE ${sql(schema)}.\"replicationConfig\"\n SET \"resetRequired\" = true`;\n}\n\nexport async function ensureReplicationConfig(\n lc: LogContext,\n db: PostgresDB,\n subscriptionState: Pick<\n SubscriptionState,\n 'publications' | 'replicaVersion' | 'watermark'\n >,\n shard: ShardID,\n autoReset: boolean,\n purgeLock?: PurgeLock,\n setTimeoutFn: typeof setTimeout = setTimeout,\n pgChangeLogEnabled = true,\n) {\n const {publications, replicaVersion, watermark} = subscriptionState;\n const replicaConfig = {publications, replicaVersion};\n const replicationState: ReplicationState = {\n lastWatermark: replicaVersion,\n owner: null,\n ownerAddress: null,\n };\n const schema = cdcSchema(shard);\n\n await runTx(db, async sql => {\n const stmts: PendingQuery<Row[]>[] = [];\n let needsTruncate = false;\n const results = await sql<\n {\n replicaVersion: string;\n publications: string[];\n resetRequired: boolean | null;\n }[]\n > /*sql*/ `\n SELECT \"replicaVersion\", \"publications\", \"resetRequired\" \n FROM ${sql(schema)}.\"replicationConfig\"`;\n\n if (results.length) {\n const {replicaVersion, publications} = results[0];\n if (\n replicaVersion !== replicaConfig.replicaVersion ||\n !equals(new Set(publications), new Set(replicaConfig.publications))\n ) {\n if (replicaConfig.replicaVersion !== watermark) {\n throw new AutoResetSignal(\n `Cannot reset change db@${replicaVersion} to ` +\n `service replica@${replicaConfig.replicaVersion} ` +\n `from watermark ${watermark}`,\n );\n }\n lc.info?.(\n `Data in cdc tables @${replicaVersion} is incompatible ` +\n `with replica @${replicaConfig.replicaVersion}. Clearing tables.`,\n );\n // Release any purge lock held by the caller; the changeLog is\n // incompatible with the replica and needs to be truncated.\n void purgeLock?.release();\n\n // Note: The order of TRUNCATE matters. The replicationState table is\n // truncated before the changeLog table, in order to acquire\n // (exclusive) locks on the tables in the same order that the\n // storer.ts acquires locks when writing the changeLog, namely:\n //\n // 1. SELECT ... FROM replicationState FOR UPDATE;\n // 2. INSERT INTO changeLog ...;\n // ...\n // n. UPDATE replicationState ...;\n needsTruncate = true;\n stmts.push(\n sql`TRUNCATE TABLE ${sql(schema)}.\"replicationState\"`,\n ...(pgChangeLogEnabled\n ? [sql`TRUNCATE TABLE ${sql(schema)}.\"changeLog\"`]\n : []),\n sql`TRUNCATE TABLE ${sql(schema)}.\"replicationConfig\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"tableMetadata\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"backfilling\"`,\n );\n }\n }\n // Initialize (or re-initialize TRUNCATED) tables\n if (results.length === 0 || needsTruncate) {\n // When enabled, the PG storer uses the earliest changeLog entry as the\n // safe watermark from which subscribers can be resumed. These initial\n // entries ensure that subscribers can start from a freshly synced\n // replica, even if new changes have been replicated and not purged.\n //\n // TODO: Replace this with an explicit `firstWatermark` column in the\n // change db.\n const watermark = replicaConfig.replicaVersion;\n const initialTx: FullChangeLogEntry[] = pgChangeLogEnabled\n ? [\n {watermark, pos: 0, change: {tag: 'begin'}},\n {watermark, pos: 1, change: {tag: 'commit'}},\n ]\n : [];\n\n stmts.push(\n sql`INSERT INTO ${sql(schema)}.\"replicationConfig\" ${sql(replicaConfig)}`,\n sql`INSERT INTO ${sql(schema)}.\"replicationState\" ${sql(replicationState)} \n ON CONFLICT (lock) DO UPDATE SET ${sql(replicationState)}`,\n ...initialTx.map(\n change => sql`INSERT INTO ${sql(schema)}.\"changeLog\" ${sql(change)}`,\n ),\n );\n\n if (needsTruncate) {\n // The TRUNCATE statements require ACCESS EXCLUSIVE locks, which may\n // be blocked by old storer catchup reads. Race against a timeout\n // that terminates the blocking backends if the TRUNCATE takes too\n // long.\n const timer = setTimeoutFn(async () => {\n lc.info?.(\n 'ensureReplicationConfig blocked, terminating lock holders',\n );\n await terminateChangeDBLockHolders(lc, db, shard);\n }, LOCK_HOLDER_TERMINATE_TIMEOUT_MS);\n try {\n return await Promise.all(stmts);\n } finally {\n clearTimeout(timer);\n }\n }\n return Promise.all(stmts);\n }\n\n const {resetRequired} = results[0];\n if (resetRequired) {\n if (autoReset) {\n throw new AutoResetSignal('reset required by replication stream');\n }\n lc.error?.(\n '\\n\\n\\n' +\n 'Reset required but --auto-reset is not enabled.\\n' +\n 'This can happen for upstream databases that do not support event triggers.\\n' +\n 'To correct this, see https://zero.rocicorp.dev/docs/connecting-to-postgres#schema-changes' +\n '\\n\\n\\n',\n );\n }\n\n return [];\n });\n}\n\n// The time to wait for a TRUNCATE in ensureReplicationConfig before\n// terminating blocking backends via terminateChangeDBLockHolders.\nconst LOCK_HOLDER_TERMINATE_TIMEOUT_MS = 5_000;\n\nexport const CHANGE_STREAMER_APP_NAME = 'zero-change-streamer';\n\nexport class AutoResetSignal extends AbortError {\n readonly name = 'AutoResetSignal';\n}\n\n/**\n * Terminates zero-cache backends that are blocking the current backend\n * from acquiring locks on CDC tables (e.g., during TRUNCATE).\n *\n * This is used during change-DB takeover when the new replication-manager's\n * `ensureReplicationConfig` needs to TRUNCATE tables, but the old\n * replication-manager's storer is still reading from them (e.g., large\n * catchup cursors).\n *\n * The function:\n * 1. Finds backends waiting for a lock on a TRUNCATE in {schema}\n * 2. Uses `pg_blocking_pids()` to identify which backends are blocking them\n * 3. Terminates blocking backends that have `application_name = 'zero-change-streamer'`\n *\n * Must be called on a **separate connection** from the one that is blocked,\n * since the blocked connection is inside a pending transaction.\n */\nexport async function terminateChangeDBLockHolders(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardID,\n) {\n const schema = cdcSchema(shard);\n\n // Step 1: Find backends that are blocked waiting for a lock,\n // whose query involves a TRUNCATE on this shard's CDC schema.\n const blocked = await db<{pid: number}[]>`\n SELECT pid FROM pg_stat_activity\n WHERE wait_event_type = 'Lock'\n AND application_name = ${CHANGE_STREAMER_APP_NAME}\n AND query LIKE ${'%TRUNCATE%' + schema + '%'}`;\n\n if (blocked.length === 0) {\n lc.info?.('no blocked TRUNCATE backends found');\n return;\n }\n\n const blockedPids = blocked.map(r => r.pid);\n lc.info?.(`found blocked TRUNCATE backends: ${JSON.stringify(blockedPids)}`);\n\n // Step 2: For each blocked backend, find and terminate its blockers\n // that are zero-change-streamer connections.\n const terminated = await db<\n {pid: number; applicationName: string; query: string; terminated: boolean}[]\n >`\n SELECT pid, application_name as \"applicationName\", query,\n pg_terminate_backend(pid) as terminated\n FROM pg_stat_activity\n WHERE pid = ANY(\n SELECT unnest(pg_blocking_pids(blocked.pid))\n FROM unnest(${blockedPids}::int[]) AS blocked(pid)\n )\n AND application_name = ${CHANGE_STREAMER_APP_NAME}\n AND pid != ALL(${blockedPids}::int[])`;\n\n if (terminated.length === 0) {\n lc.info?.(`no ${CHANGE_STREAMER_APP_NAME} blockers found to terminate`);\n } else {\n for (const {pid, applicationName, query, terminated: ok} of terminated) {\n lc.info?.(\n `terminated blocking backend pid=${pid} app=${applicationName} ok=${ok} query=${query.slice(0, 200)}`,\n );\n }\n }\n}\n"],"mappings":";;;;;;;;AAiBA,SAAS,OAAO,OAAgB;CAC9B,OAAO,MAAM,UAAU,KAAK,CAAC;AAC/B;AAIA,SAAS,aAAa,OAAgB;CACpC,OAAe,+BAA+B,OAAO,KAAK,EAAE;AAC9D;AAWA,SAAS,qBAAqB,OAAgB;CAI5C,OAAe;iBACA,OAAO,KAAK,EAAE;;;;;;;;AAQ/B;AAYA,SAAgB,4BAA4B,OAAgB;CAC1D,OAAe;iBACA,OAAO,KAAK,EAAE;;;;;;;AAO/B;AAEA,eAAsB,8BACpB,OACA,KACwB;CAGxB,QAAO,MAFc,GAA6C;iCACnC,IAAI,UAAU,KAAK,CAAC,EAAE,sBACvC,GAAG;AACnB;AAaA,SAAS,6BAA6B,OAAgB;CACpD,OAAe;iBACA,OAAO,KAAK,EAAE;;;;;;;AAO/B;AAEA,SAAgB,qBAAqB,OAAgB;CACnD,OAAe;iBACA,OAAO,KAAK,EAAE;;;;;;;iBAOd,OAAO,KAAK,EAAE;;;;;;;;AAQ/B;AAeA,SAAS,aAAa,OAAgB;CACpC,OACE,aAAa,KAAK,IAClB,qBAAqB,KAAK,IAC1B,4BAA4B,KAAK,IACjC,6BAA6B,KAAK,IAClC,qBAAqB,KAAK;AAE9B;AAMA,eAAsB,eACpB,IACA,IACA,OACA;CACA,GAAG,OAAO,uBAAuB;CACjC,MAAM,GAAG,OAAO,aAAa,KAAK,CAAC;AACrC;AAEA,eAAsB,kBAAkB,KAAiB,OAAgB;CAEvE,MAAM,GAAG;WACA,IAFM,UAAU,KAEZ,CAAM,EAAE;;AAEvB;AAEA,eAAsB,wBACpB,IACA,IACA,mBAIA,OACA,WACA,WACA,eAAkC,YAClC,qBAAqB,MACrB;CACA,MAAM,EAAC,cAAc,gBAAgB,cAAa;CAClD,MAAM,gBAAgB;EAAC;EAAc;CAAc;CACnD,MAAM,mBAAqC;EACzC,eAAe;EACf,OAAO;EACP,cAAc;CAChB;CACA,MAAM,SAAS,UAAU,KAAK;CAE9B,MAAM,MAAM,IAAI,OAAM,QAAO;EAC3B,MAAM,QAA+B,CAAC;EACtC,IAAI,gBAAgB;EACpB,MAAM,UAAU,MAAM,GAMZ;;aAED,IAAI,MAAM,EAAE;EAErB,IAAI,QAAQ,QAAQ;GAClB,MAAM,EAAC,gBAAgB,iBAAgB,QAAQ;GAC/C,IACE,mBAAmB,cAAc,kBACjC,CAAC,OAAO,IAAI,IAAI,YAAY,GAAG,IAAI,IAAI,cAAc,YAAY,CAAC,GAClE;IACA,IAAI,cAAc,mBAAmB,WACnC,MAAM,IAAI,gBACR,0BAA0B,eAAe,sBACpB,cAAc,eAAe,kBAC9B,WACtB;IAEF,GAAG,OACD,uBAAuB,eAAe,iCACnB,cAAc,eAAe,mBAClD;IAGA,WAAgB,QAAQ;IAWxB,gBAAgB;IAChB,MAAM,KACJ,GAAG,kBAAkB,IAAI,MAAM,EAAE,sBACjC,GAAI,qBACA,CAAC,GAAG,kBAAkB,IAAI,MAAM,EAAE,aAAa,IAC/C,CAAC,GACL,GAAG,kBAAkB,IAAI,MAAM,EAAE,uBACjC,GAAG,kBAAkB,IAAI,MAAM,EAAE,mBACjC,GAAG,kBAAkB,IAAI,MAAM,EAAE,eACnC;GACF;EACF;EAEA,IAAI,QAAQ,WAAW,KAAK,eAAe;GAQzC,MAAM,YAAY,cAAc;GAChC,MAAM,YAAkC,qBACpC,CACE;IAAC;IAAW,KAAK;IAAG,QAAQ,EAAC,KAAK,QAAO;GAAC,GAC1C;IAAC;IAAW,KAAK;IAAG,QAAQ,EAAC,KAAK,SAAQ;GAAC,CAC7C,IACA,CAAC;GAEL,MAAM,KACJ,GAAG,eAAe,IAAI,MAAM,EAAE,uBAAuB,IAAI,aAAa,KACtE,GAAG,eAAe,IAAI,MAAM,EAAE,uBAAuB,IAAI,gBAAgB,EAAE;iDAClC,IAAI,gBAAgB,KAC7D,GAAG,UAAU,KACX,WAAU,GAAG,eAAe,IAAI,MAAM,EAAE,eAAe,IAAI,MAAM,GACnE,CACF;GAEA,IAAI,eAAe;IAKjB,MAAM,QAAQ,aAAa,YAAY;KACrC,GAAG,OACD,2DACF;KACA,MAAM,6BAA6B,IAAI,IAAI,KAAK;IAClD,GAAG,gCAAgC;IACnC,IAAI;KACF,OAAO,MAAM,QAAQ,IAAI,KAAK;IAChC,UAAU;KACR,aAAa,KAAK;IACpB;GACF;GACA,OAAO,QAAQ,IAAI,KAAK;EAC1B;EAEA,MAAM,EAAC,kBAAiB,QAAQ;EAChC,IAAI,eAAe;GACjB,IAAI,WACF,MAAM,IAAI,gBAAgB,sCAAsC;GAElE,GAAG,QACD,oOAKF;EACF;EAEA,OAAO,CAAC;CACV,CAAC;AACH;AAIA,IAAM,mCAAmC;AAEzC,IAAa,2BAA2B;AAExC,IAAa,kBAAb,cAAqC,WAAW;CAC9C,OAAgB;AAClB;;;;;;;;;;;;;;;;;;AAmBA,eAAsB,6BACpB,IACA,IACA,OACA;CAKA,MAAM,UAAU,MAAM,EAAmB;;;iCAGV,yBAAyB;yBACjC,eARR,UAAU,KAQa,IAAS;CAE/C,IAAI,QAAQ,WAAW,GAAG;EACxB,GAAG,OAAO,oCAAoC;EAC9C;CACF;CAEA,MAAM,cAAc,QAAQ,KAAI,MAAK,EAAE,GAAG;CAC1C,GAAG,OAAO,oCAAoC,KAAK,UAAU,WAAW,GAAG;CAI3E,MAAM,aAAa,MAAM,EAExB;;;;;;wBAMqB,YAAY;;+BAEL,yBAAyB;uBACjC,YAAY;CAEjC,IAAI,WAAW,WAAW,GACxB,GAAG,OAAO,MAAM,yBAAyB,6BAA6B;MAEtE,KAAK,MAAM,EAAC,KAAK,iBAAiB,OAAO,YAAY,QAAO,YAC1D,GAAG,OACD,mCAAmC,IAAI,OAAO,gBAAgB,MAAM,GAAG,SAAS,MAAM,MAAM,GAAG,GAAG,GACpG;AAGN"}
|
|
@@ -25,6 +25,11 @@ export type SQLiteChangeLogWriterOptions = {
|
|
|
25
25
|
onRebuilt?: (() => void) | undefined;
|
|
26
26
|
/** Overridable for tests. */
|
|
27
27
|
now?: (() => number) | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Percentage of transactions that retain the rollout-only per-message
|
|
30
|
+
* timing and source-vs-stored hash validation. Defaults to 1.
|
|
31
|
+
*/
|
|
32
|
+
shadowValidationPercent?: number | undefined;
|
|
28
33
|
};
|
|
29
34
|
/**
|
|
30
35
|
* Appends the canonical change stream to the SQLite change log, from the
|
|
@@ -98,7 +103,7 @@ export declare class SQLiteChangeLogWriter {
|
|
|
98
103
|
* Writes one stream message, committing at transaction boundaries. Never
|
|
99
104
|
* throws: a write failure disables the writer instead of failing the stream.
|
|
100
105
|
*/
|
|
101
|
-
write(change: ChangeStreamData, json: string): void;
|
|
106
|
+
write(change: ChangeStreamData, json: string, storedChange?: string): void;
|
|
102
107
|
/**
|
|
103
108
|
* Discards the open transaction, if any. Called when the change stream is
|
|
104
109
|
* interrupted mid-transaction.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-change-log-writer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"sqlite-change-log-writer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAKjD,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,8BAA8B,CAAC;AAM3D,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iDAAiD,CAAC;AAKtF,OAAO,EAUL,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAE1B,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAML,KAAK,iCAAiC,EACvC,MAAM,kDAAkD,CAAC;AAG1D,MAAM,MAAM,4BAA4B,GAAG;IACzC,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,QAAQ,EAAE,iBAAiB,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IACrD;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IACtC,6EAA6E;IAC7E,SAAS,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IACrC,6BAA6B;IAC7B,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;IACjC;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9C,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,qBAAqB;;IAYhC,YAAY,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,4BAA4B,EAY7D;IAED,qEAAqE;IACrE,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;;;;OAOG;IACH,IAAI,UAAU,IAAI,QAAQ,GAAG,SAAS,CAErC;IAED,2EAA2E;IAC3E,KAAK,IAAI,iCAAiC,GAAG,SAAS,CAErD;IAED;;;;;;;;;;;;;OAaG;IACH,SAAS,CACP,UAAU,EAAE,oBAAoB,GAC/B,oBAAoB,GAAG,SAAS,CAElC;IAED;;;;;OAKG;IACH,gBAAgB,CACd,IAAI,EAAE,MAAM,oBAAoB,GAC/B,oBAAoB,GAAG,SAAS,CAElC;IA2GD;;;OAGG;IACH,KAAK,CACH,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,MAAM,EACZ,YAAY,SAA8C,GACzD,IAAI,CA4FN;IAeD;;;OAGG;IACH,KAAK,IAAI,IAAI,CAYZ;IAED,KAAK,IAAI,IAAI,CAYZ;CA6BF"}
|