@rocicorp/zero 1.10.0-head-e669b21e-20260821 → 1.10.0-head-4ace2015-20260821
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/zero/package.js +1 -1
- 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 +2 -1
- package/out/zero-cache/src/config/normalize.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +4 -4
- package/out/zero-cache/src/config/zero-config.js +9 -8
- 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 +1 -3
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/worker-urls.d.ts +0 -1
- package/out/zero-cache/src/server/worker-urls.d.ts.map +1 -1
- package/out/zero-cache/src/server/worker-urls.js +1 -2
- package/out/zero-cache/src/server/worker-urls.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts +1 -4
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js +11 -18
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.d.ts +29 -0
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.js +154 -0
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.js.map +1 -0
- package/out/zero-cache/src/types/processes.js +1 -4
- package/out/zero-cache/src/types/processes.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zqlite/src/db.js +2 -2
- package/out/zqlite/src/sqlite-cost-model.js +5 -5
- package/out/zqlite/src/table-source.js +3 -3
- package/package.json +1 -1
- package/out/zero-cache/src/server/backup-watermark-poller.js +0 -31
- package/out/zero-cache/src/server/backup-watermark-poller.js.map +0 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-poller.d.ts +0 -57
- package/out/zero-cache/src/services/litestream/vfs-watermark-poller.d.ts.map +0 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-poller.js +0 -137
- package/out/zero-cache/src/services/litestream/vfs-watermark-poller.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-streamer.js","names":[],"sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"sourcesContent":["import {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {DatabaseInitError} from '../../../zqlite/src/db.ts';\nimport {getServerContext} from '../config/server-context.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {deleteLiteDB} from '../db/delete-lite-db.ts';\nimport {registerSQLiteCorruptionDiagnosticTarget} from '../db/sqlite-corruption.ts';\nimport {warmupConnections} from '../db/warmup.ts';\nimport {initEventSink, publishCriticalEvent} from '../observability/events.ts';\nimport {getOrCreateGauge} from '../observability/metrics.ts';\nimport {initializeCustomChangeSource} from '../services/change-source/custom/change-source.ts';\nimport {initializePostgresChangeSource} from '../services/change-source/pg/change-source.ts';\nimport {createBackupCleanupMonitor} from '../services/change-streamer/backup-cleanup-monitor-factory.ts';\nimport {ChangeStreamerHttpServer} from '../services/change-streamer/change-streamer-http.ts';\nimport {initializeStreamer} from '../services/change-streamer/change-streamer-service.ts';\nimport type {ChangeStreamerService} from '../services/change-streamer/change-streamer.ts';\nimport {initChangeStreamerSchema} from '../services/change-streamer/schema/init.ts';\nimport {AutoResetSignal} from '../services/change-streamer/schema/tables.ts';\nimport {PurgeLocker} from '../services/change-streamer/storer.ts';\nimport {\n exitAfter,\n ProcessManager,\n runUntilKilled,\n} from '../services/life-cycle.ts';\nimport {startReplicaBackupProcess} from '../services/litestream/commands.ts';\nimport {\n changeLogFileName,\n deleteChangeLogDB,\n} from '../services/replicator/change-log-db.ts';\nimport {\n replicationStatusError,\n ReplicationStatusPublisher,\n} from '../services/replicator/replication-status.ts';\nimport {sqliteFileBytes} from '../services/replicator/sqlite-change-log-observability.ts';\nimport {connectPgClient} from '../types/pg.ts';\nimport {\n childWorker,\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {getShardConfig} from '../types/shards.ts';\nimport type {ReplicaFileMode} from '../workers/replicator.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\nimport {REPLICATOR_URL} from './worker-urls.ts';\n\n// Default LogContext, overridden in runWorker\nlet lc = new LogContext('info', {}, consoleLogSink);\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...argv: string[]\n): Promise<void> {\n const config = getNormalizedZeroConfig({env, argv});\n const {\n taskID,\n changeStreamer: {\n port,\n address,\n protocol,\n startupDelayMs,\n backPressureLimitHeapProportion,\n flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodSeconds,\n sqliteChangeLogMode,\n sqliteChangeLogReadPercent,\n sqliteChangeLogComparePercent,\n sqliteChangeLogRetentionMs,\n sqliteChangeLogReadBatchRows,\n sqliteChangeLogPurgeBatchRows,\n sqliteChangeLogBarrierTimeoutMs,\n },\n autoReset,\n replicationLag,\n litestream,\n upstream,\n change,\n replica,\n initialSync,\n keepaliveTimeoutMs,\n sqliteCorruptionChecks,\n } = config;\n\n startOtelAuto(\n createLogContext(config, 'change-streamer', 0, false),\n 'change-streamer',\n 0,\n );\n lc = createLogContext(config, 'change-streamer');\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: 'change-streamer replica',\n dbPath: replica.file,\n },\n sqliteCorruptionChecks,\n );\n initEventSink(lc, config);\n\n // Kick off DB connection warmup in the background.\n const changeDB = await connectPgClient(\n lc,\n change.db,\n 'change-streamer',\n {max: change.maxConns},\n {sendStringAsJson: true},\n );\n void warmupConnections(lc, changeDB, 'change').catch(() => {});\n\n const shard = getShardConfig(config);\n\n // Ensure the change DB schema is initialized/up-to-date.\n await initChangeStreamerSchema(lc, changeDB, shard);\n\n // When restoring from litestream, acquire a lock to prevent change-log\n // purges. This ensures that (this) change-streamer will be able to resume\n // from the backup.\n let purgeLock =\n litestream.backupURL && litestream.executable\n ? await new PurgeLocker(lc, shard, changeDB).acquire()\n : null;\n const restoreOptions = {litestream, constraints: purgeLock ?? undefined};\n\n let changeStreamer: ChangeStreamerService | undefined;\n let backupURL: string | undefined;\n\n const context = getServerContext(config);\n const sqliteChangeLogEnabled = sqliteChangeLogMode !== 'off';\n // The modes are cumulative, so `serve` implies `compare`.\n const sqliteChangeLogComparing =\n sqliteChangeLogMode === 'compare' || sqliteChangeLogMode === 'serve';\n if (sqliteChangeLogEnabled) {\n const changeLogFile = changeLogFileName(replica.file);\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: 'change-streamer change-log',\n dbPath: changeLogFile,\n },\n sqliteCorruptionChecks,\n );\n // The log's bytes are accounted for in the process that writes them. This\n // is local disk only — the log is excluded from the litestream backup — and\n // it is a whole-database total, because a table that left the replica left\n // through its wal: the replica's footprint is what shrank and this is where\n // it went.\n getOrCreateGauge('replica', 'sqlite_change_log.file_bytes', {\n description:\n `The SQLite change log's total on-disk footprint: its main db file ` +\n `plus its wal sidecars.`,\n unit: 'By',\n }).addCallback(async o =>\n o.observe(await sqliteFileBytes(lc, changeLogFile)),\n );\n }\n\n let waitForFirstBackupBeforeServing = false;\n for (const first of [true, false]) {\n try {\n // Note: This performs initial sync of the replica if necessary.\n const {\n changeSource,\n subscriptionState,\n destinationBackupURL,\n replicaID,\n waitForBackupBeforeServing,\n } =\n upstream.type === 'pg'\n ? await initializePostgresChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n {\n ...initialSync,\n replicationSlotFailover: upstream.pgReplicationSlotFailover,\n },\n context,\n replicationLag.reportIntervalMs,\n restoreOptions,\n {backupV5: litestream.backupUsingV5},\n purgeLock,\n upstream.pgStreamInboundTimeoutMs,\n )\n : await initializeCustomChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n context,\n restoreOptions,\n );\n\n const replicationStatusPublisher =\n ReplicationStatusPublisher.forReplicaFile(replica.file);\n\n changeStreamer = await initializeStreamer(\n lc,\n shard,\n taskID,\n address,\n protocol,\n changeDB,\n changeSource,\n replicationStatusPublisher,\n subscriptionState,\n destinationBackupURL\n ? {\n backupURL: destinationBackupURL,\n litestreamVersion: litestream.backupUsingV5 ? 'v5' : 'legacy',\n }\n : null,\n purgeLock,\n autoReset ?? false,\n {\n backPressureLimitHeapProportion,\n flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodMs:\n flowControlSlowSubscriberGracePeriodSeconds > 0\n ? flowControlSlowSubscriberGracePeriodSeconds * 1000\n : undefined,\n statementTimeoutMs: change.statementTimeoutMs,\n changeLogBatchSize: change.logBatchSize,\n sqliteCatchup: {\n changeLogFile: changeLogFileName(replica.file),\n readBatchRows: sqliteChangeLogReadBatchRows,\n barrierTimeoutMs: sqliteChangeLogBarrierTimeoutMs,\n },\n // The presence of these options is the writer's gate. This process\n // performs the restore and the initial sync, so the replica's\n // identity is in hand at the moment the log is opened.\n sqliteChangeLogWriter: sqliteChangeLogEnabled\n ? {\n replicaFile: replica.file,\n identity: {\n // RMv2 supplies the epoch; until then the generation and the\n // replica ID are the whole of the identity.\n epoch: null,\n generation: subscriptionState.replicaVersion,\n replicaID,\n },\n }\n : undefined,\n // The purge scheduler shares the writer's gate -- mode, not the\n // read path -- because `write` mode, with no read selector at all,\n // is the configuration it ships in.\n sqliteChangeLogPurge: sqliteChangeLogEnabled\n ? {\n retentionMs: sqliteChangeLogRetentionMs,\n batchRows: sqliteChangeLogPurgeBatchRows,\n }\n : undefined,\n // Compare mode runs both advisory checks. Postgres remains authoritative.\n sqliteChangeLogCompare: sqliteChangeLogComparing\n ? {\n replicaFile: replica.file,\n comparePercent: sqliteChangeLogComparePercent,\n retentionMs: sqliteChangeLogRetentionMs,\n readBatchRows: sqliteChangeLogReadBatchRows,\n }\n : undefined,\n // Slice 11 lands dark by default: serve mode constructs the stable\n // router, while readPercent=0 keeps every catchup on PG and emits\n // eligibility metrics before any canary traffic is enabled.\n sqliteChangeLogServe:\n sqliteChangeLogMode === 'serve'\n ? {\n readPercent: sqliteChangeLogReadPercent,\n retentionMs: sqliteChangeLogRetentionMs,\n }\n : undefined,\n },\n setTimeout,\n );\n backupURL = destinationBackupURL;\n waitForFirstBackupBeforeServing = waitForBackupBeforeServing;\n break;\n } catch (e) {\n if (first && e instanceof AutoResetSignal) {\n lc.warn?.(`resetting replica ${replica.file}`, e);\n // TODO: Make deleteLiteDB work with litestream. It will probably have to be\n // a semantic wipe instead of a file delete.\n deleteLiteDB(replica.file);\n // The change log carries the identity of the replica it was written\n // beside, and the retry performs a fresh initial sync with a new\n // replicaVersion. Reconciliation would catch that on its own (as\n // 'identity-mismatch') but only after the writer opened a file that is\n // known here to be garbage.\n deleteChangeLogDB(replica.file);\n // Release the purge lock before retrying. This is safe because the\n // purge lock exists to preserve change-log entries so the new\n // change-streamer can resume from the backup replica's watermark.\n // An AutoResetSignal means we cant resume from the backup replica\n // (e.g. its replication slot is gone), so the change-log entries the lock\n // was protecting are no longer needed. The retry performs a fresh\n // initial sync with a new replication slot, independent of the old\n // change-log. Releasing is also necessary to avoid a\n // self-deadlock when CHANGE_DB == UPSTREAM_DB:\n // CREATE_REPLICATION_SLOT waits for all older transactions to\n // finish, including this lock's open transaction.\n await purgeLock?.release();\n purgeLock = null;\n continue; // execute again with a fresh initial-sync\n }\n if (e instanceof DatabaseInitError) {\n throw new Error(\n `Cannot open ZERO_REPLICA_FILE at \"${replica.file}\". Please check that the path is valid.`,\n {cause: e},\n );\n }\n throw e;\n }\n }\n // impossible: upstream must have advanced in order for replication to be stuck.\n assert(changeStreamer, `resetting replica did not advance replicaVersion`);\n\n const processes = new ProcessManager(lc, parent);\n if (backupURL) {\n lc.info?.('setting up backup to', backupURL);\n litestream.backupURL = backupURL;\n const {promise: backupStarted, resolve} = resolver();\n\n // Start a backup replicator and corresponding litestream backup process.\n processes\n .addWorker(\n childWorker(REPLICATOR_URL, env, 'backup' satisfies ReplicaFileMode),\n 'supporting',\n 'backup-replicator',\n )\n // Wait for the replicator's first message (i.e. \"ready\") before starting\n // litestream backup in order to avoid contending on the sqlite lock\n // when the replicator first prepares the db file.\n .once('message', () => {\n processes.addSubprocess(\n startReplicaBackupProcess(lc, litestream, replica.file),\n 'supporting',\n 'litestream',\n );\n resolve();\n });\n await backupStarted;\n }\n\n const backupMonitor = createBackupCleanupMonitor({\n lc,\n processes,\n config,\n replicaFile: replica.file,\n changeStreamer,\n env,\n });\n\n const changeStreamerWebServer = new ChangeStreamerHttpServer(\n lc,\n {port, keepaliveTimeoutMs, startupDelayMs},\n parent,\n changeStreamer,\n );\n\n if (waitForFirstBackupBeforeServing) {\n const start = performance.now();\n lc.info?.(`awaiting initial backup ...`);\n\n void backupMonitor.firstBackupReceived().then(() => {\n const elapsed = performance.now() - start;\n lc.info?.(`initial backup confirmed after ${elapsed.toFixed(2)}ms`);\n parent.send(['ready', {ready: true}]);\n });\n } else {\n parent.send(['ready', {ready: true}]);\n }\n\n // Note: The changeStreamer itself is not started here; it is started by the\n // changeStreamerWebServer after a delay to ensure that routing\n // routing elements have registered the server before the\n // change-streamer takes over the replication slot.\n // TODO: Remove this delay and start the changeStreamer normally here once\n // transitioned to RMv2, since changeStreamer startup will no longer\n // disrupt an existing task.\n try {\n await runUntilKilled(lc, parent, changeStreamerWebServer, backupMonitor);\n } catch (err) {\n processes.logErrorAndExit(err, 'change-streamer');\n } finally {\n await processes.shutdown();\n }\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(\n () => lc,\n () =>\n runWorker(\n must(parentWorker),\n process.env,\n ...process.argv.slice(2),\n ).catch(async e => {\n await publishCriticalEvent(\n lc,\n replicationStatusError(lc, 'Initializing', e),\n );\n throw e;\n }),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"change-streamer.js","names":[],"sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"sourcesContent":["import {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {DatabaseInitError} from '../../../zqlite/src/db.ts';\nimport {getServerContext} from '../config/server-context.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {deleteLiteDB} from '../db/delete-lite-db.ts';\nimport {registerSQLiteCorruptionDiagnosticTarget} from '../db/sqlite-corruption.ts';\nimport {warmupConnections} from '../db/warmup.ts';\nimport {initEventSink, publishCriticalEvent} from '../observability/events.ts';\nimport {getOrCreateGauge} from '../observability/metrics.ts';\nimport {initializeCustomChangeSource} from '../services/change-source/custom/change-source.ts';\nimport {initializePostgresChangeSource} from '../services/change-source/pg/change-source.ts';\nimport {createBackupCleanupMonitor} from '../services/change-streamer/backup-cleanup-monitor-factory.ts';\nimport {ChangeStreamerHttpServer} from '../services/change-streamer/change-streamer-http.ts';\nimport {initializeStreamer} from '../services/change-streamer/change-streamer-service.ts';\nimport type {ChangeStreamerService} from '../services/change-streamer/change-streamer.ts';\nimport {initChangeStreamerSchema} from '../services/change-streamer/schema/init.ts';\nimport {AutoResetSignal} from '../services/change-streamer/schema/tables.ts';\nimport {PurgeLocker} from '../services/change-streamer/storer.ts';\nimport {\n exitAfter,\n ProcessManager,\n runUntilKilled,\n} from '../services/life-cycle.ts';\nimport {startReplicaBackupProcess} from '../services/litestream/commands.ts';\nimport {\n changeLogFileName,\n deleteChangeLogDB,\n} from '../services/replicator/change-log-db.ts';\nimport {\n replicationStatusError,\n ReplicationStatusPublisher,\n} from '../services/replicator/replication-status.ts';\nimport {sqliteFileBytes} from '../services/replicator/sqlite-change-log-observability.ts';\nimport {connectPgClient} from '../types/pg.ts';\nimport {\n childWorker,\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {getShardConfig} from '../types/shards.ts';\nimport type {ReplicaFileMode} from '../workers/replicator.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\nimport {REPLICATOR_URL} from './worker-urls.ts';\n\n// Default LogContext, overridden in runWorker\nlet lc = new LogContext('info', {}, consoleLogSink);\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...argv: string[]\n): Promise<void> {\n const config = getNormalizedZeroConfig({env, argv});\n const {\n taskID,\n changeStreamer: {\n port,\n address,\n protocol,\n startupDelayMs,\n backPressureLimitHeapProportion,\n flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodSeconds,\n sqliteChangeLogMode,\n sqliteChangeLogReadPercent,\n sqliteChangeLogComparePercent,\n sqliteChangeLogRetentionMs,\n sqliteChangeLogReadBatchRows,\n sqliteChangeLogPurgeBatchRows,\n sqliteChangeLogBarrierTimeoutMs,\n },\n autoReset,\n replicationLag,\n litestream,\n upstream,\n change,\n replica,\n initialSync,\n keepaliveTimeoutMs,\n sqliteCorruptionChecks,\n } = config;\n\n startOtelAuto(\n createLogContext(config, 'change-streamer', 0, false),\n 'change-streamer',\n 0,\n );\n lc = createLogContext(config, 'change-streamer');\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: 'change-streamer replica',\n dbPath: replica.file,\n },\n sqliteCorruptionChecks,\n );\n initEventSink(lc, config);\n\n // Kick off DB connection warmup in the background.\n const changeDB = await connectPgClient(\n lc,\n change.db,\n 'change-streamer',\n {max: change.maxConns},\n {sendStringAsJson: true},\n );\n void warmupConnections(lc, changeDB, 'change').catch(() => {});\n\n const shard = getShardConfig(config);\n\n // Ensure the change DB schema is initialized/up-to-date.\n await initChangeStreamerSchema(lc, changeDB, shard);\n\n // When restoring from litestream, acquire a lock to prevent change-log\n // purges. This ensures that (this) change-streamer will be able to resume\n // from the backup.\n let purgeLock =\n litestream.backupURL && litestream.executable\n ? await new PurgeLocker(lc, shard, changeDB).acquire()\n : null;\n const restoreOptions = {litestream, constraints: purgeLock ?? undefined};\n\n let changeStreamer: ChangeStreamerService | undefined;\n let backupURL: string | undefined;\n\n const context = getServerContext(config);\n const sqliteChangeLogEnabled = sqliteChangeLogMode !== 'off';\n // The modes are cumulative, so `serve` implies `compare`.\n const sqliteChangeLogComparing =\n sqliteChangeLogMode === 'compare' || sqliteChangeLogMode === 'serve';\n if (sqliteChangeLogEnabled) {\n const changeLogFile = changeLogFileName(replica.file);\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: 'change-streamer change-log',\n dbPath: changeLogFile,\n },\n sqliteCorruptionChecks,\n );\n // The log's bytes are accounted for in the process that writes them. This\n // is local disk only — the log is excluded from the litestream backup — and\n // it is a whole-database total, because a table that left the replica left\n // through its wal: the replica's footprint is what shrank and this is where\n // it went.\n getOrCreateGauge('replica', 'sqlite_change_log.file_bytes', {\n description:\n `The SQLite change log's total on-disk footprint: its main db file ` +\n `plus its wal sidecars.`,\n unit: 'By',\n }).addCallback(async o =>\n o.observe(await sqliteFileBytes(lc, changeLogFile)),\n );\n }\n\n let waitForFirstBackupBeforeServing = false;\n for (const first of [true, false]) {\n try {\n // Note: This performs initial sync of the replica if necessary.\n const {\n changeSource,\n subscriptionState,\n destinationBackupURL,\n replicaID,\n waitForBackupBeforeServing,\n } =\n upstream.type === 'pg'\n ? await initializePostgresChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n {\n ...initialSync,\n replicationSlotFailover: upstream.pgReplicationSlotFailover,\n },\n context,\n replicationLag.reportIntervalMs,\n restoreOptions,\n {backupV5: litestream.backupUsingV5},\n purgeLock,\n upstream.pgStreamInboundTimeoutMs,\n )\n : await initializeCustomChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n context,\n restoreOptions,\n );\n\n const replicationStatusPublisher =\n ReplicationStatusPublisher.forReplicaFile(replica.file);\n\n changeStreamer = await initializeStreamer(\n lc,\n shard,\n taskID,\n address,\n protocol,\n changeDB,\n changeSource,\n replicationStatusPublisher,\n subscriptionState,\n destinationBackupURL\n ? {\n backupURL: destinationBackupURL,\n litestreamVersion: litestream.backupUsingV5 ? 'v5' : 'legacy',\n }\n : null,\n purgeLock,\n autoReset ?? false,\n {\n backPressureLimitHeapProportion,\n flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodMs:\n flowControlSlowSubscriberGracePeriodSeconds > 0\n ? flowControlSlowSubscriberGracePeriodSeconds * 1000\n : undefined,\n statementTimeoutMs: change.statementTimeoutMs,\n changeLogBatchSize: change.logBatchSize,\n sqliteCatchup: {\n changeLogFile: changeLogFileName(replica.file),\n readBatchRows: sqliteChangeLogReadBatchRows,\n barrierTimeoutMs: sqliteChangeLogBarrierTimeoutMs,\n },\n // The presence of these options is the writer's gate. This process\n // performs the restore and the initial sync, so the replica's\n // identity is in hand at the moment the log is opened.\n sqliteChangeLogWriter: sqliteChangeLogEnabled\n ? {\n replicaFile: replica.file,\n identity: {\n // RMv2 supplies the epoch; until then the generation and the\n // replica ID are the whole of the identity.\n epoch: null,\n generation: subscriptionState.replicaVersion,\n replicaID,\n },\n }\n : undefined,\n // The purge scheduler shares the writer's gate -- mode, not the\n // read path -- because `write` mode, with no read selector at all,\n // is the configuration it ships in.\n sqliteChangeLogPurge: sqliteChangeLogEnabled\n ? {\n retentionMs: sqliteChangeLogRetentionMs,\n batchRows: sqliteChangeLogPurgeBatchRows,\n }\n : undefined,\n // Compare mode runs both advisory checks. Postgres remains authoritative.\n sqliteChangeLogCompare: sqliteChangeLogComparing\n ? {\n replicaFile: replica.file,\n comparePercent: sqliteChangeLogComparePercent,\n retentionMs: sqliteChangeLogRetentionMs,\n readBatchRows: sqliteChangeLogReadBatchRows,\n }\n : undefined,\n // Slice 11 lands dark by default: serve mode constructs the stable\n // router, while readPercent=0 keeps every catchup on PG and emits\n // eligibility metrics before any canary traffic is enabled.\n sqliteChangeLogServe:\n sqliteChangeLogMode === 'serve'\n ? {\n readPercent: sqliteChangeLogReadPercent,\n retentionMs: sqliteChangeLogRetentionMs,\n }\n : undefined,\n },\n setTimeout,\n );\n backupURL = destinationBackupURL;\n waitForFirstBackupBeforeServing = waitForBackupBeforeServing;\n break;\n } catch (e) {\n if (first && e instanceof AutoResetSignal) {\n lc.warn?.(`resetting replica ${replica.file}`, e);\n // TODO: Make deleteLiteDB work with litestream. It will probably have to be\n // a semantic wipe instead of a file delete.\n deleteLiteDB(replica.file);\n // The change log carries the identity of the replica it was written\n // beside, and the retry performs a fresh initial sync with a new\n // replicaVersion. Reconciliation would catch that on its own (as\n // 'identity-mismatch') but only after the writer opened a file that is\n // known here to be garbage.\n deleteChangeLogDB(replica.file);\n // Release the purge lock before retrying. This is safe because the\n // purge lock exists to preserve change-log entries so the new\n // change-streamer can resume from the backup replica's watermark.\n // An AutoResetSignal means we cant resume from the backup replica\n // (e.g. its replication slot is gone), so the change-log entries the lock\n // was protecting are no longer needed. The retry performs a fresh\n // initial sync with a new replication slot, independent of the old\n // change-log. Releasing is also necessary to avoid a\n // self-deadlock when CHANGE_DB == UPSTREAM_DB:\n // CREATE_REPLICATION_SLOT waits for all older transactions to\n // finish, including this lock's open transaction.\n await purgeLock?.release();\n purgeLock = null;\n continue; // execute again with a fresh initial-sync\n }\n if (e instanceof DatabaseInitError) {\n throw new Error(\n `Cannot open ZERO_REPLICA_FILE at \"${replica.file}\". Please check that the path is valid.`,\n {cause: e},\n );\n }\n throw e;\n }\n }\n // impossible: upstream must have advanced in order for replication to be stuck.\n assert(changeStreamer, `resetting replica did not advance replicaVersion`);\n\n const processes = new ProcessManager(lc, parent);\n if (backupURL) {\n lc.info?.('setting up backup to', backupURL);\n litestream.backupURL = backupURL;\n const {promise: backupStarted, resolve} = resolver();\n\n // Start a backup replicator and corresponding litestream backup process.\n processes\n .addWorker(\n childWorker(REPLICATOR_URL, env, 'backup' satisfies ReplicaFileMode),\n 'supporting',\n 'backup-replicator',\n )\n // Wait for the replicator's first message (i.e. \"ready\") before starting\n // litestream backup in order to avoid contending on the sqlite lock\n // when the replicator first prepares the db file.\n .once('message', () => {\n processes.addSubprocess(\n startReplicaBackupProcess(lc, litestream, replica.file),\n 'supporting',\n 'litestream',\n );\n resolve();\n });\n await backupStarted;\n }\n\n const backupMonitor = createBackupCleanupMonitor({\n lc,\n config,\n replicaFile: replica.file,\n changeStreamer,\n });\n\n const changeStreamerWebServer = new ChangeStreamerHttpServer(\n lc,\n {port, keepaliveTimeoutMs, startupDelayMs},\n parent,\n changeStreamer,\n );\n\n if (waitForFirstBackupBeforeServing) {\n const start = performance.now();\n lc.info?.(`awaiting initial backup ...`);\n\n void backupMonitor.firstBackupReceived().then(() => {\n const elapsed = performance.now() - start;\n lc.info?.(`initial backup confirmed after ${elapsed.toFixed(2)}ms`);\n parent.send(['ready', {ready: true}]);\n });\n } else {\n parent.send(['ready', {ready: true}]);\n }\n\n // Note: The changeStreamer itself is not started here; it is started by the\n // changeStreamerWebServer after a delay to ensure that routing\n // routing elements have registered the server before the\n // change-streamer takes over the replication slot.\n // TODO: Remove this delay and start the changeStreamer normally here once\n // transitioned to RMv2, since changeStreamer startup will no longer\n // disrupt an existing task.\n try {\n await runUntilKilled(lc, parent, changeStreamerWebServer, backupMonitor);\n } catch (err) {\n processes.logErrorAndExit(err, 'change-streamer');\n } finally {\n await processes.shutdown();\n }\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(\n () => lc,\n () =>\n runWorker(\n must(parentWorker),\n process.env,\n ...process.argv.slice(2),\n ).catch(async e => {\n await publishCriticalEvent(\n lc,\n replicationStatusError(lc, 'Initializing', e),\n );\n throw e;\n }),\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker-urls.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/worker-urls.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,mBAAmB,KAAkC,CAAC;AACnE,eAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"worker-urls.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/worker-urls.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,mBAAmB,KAAkC,CAAC;AACnE,eAAO,MAAM,QAAQ,KAAuB,CAAC;AAC7C,eAAO,MAAM,WAAW,KAA0B,CAAC;AACnD,eAAO,MAAM,UAAU,KAAyB,CAAC;AACjD,eAAO,MAAM,cAAc,KAA6B,CAAC;AACzD,eAAO,MAAM,iBAAiB,KAAgC,CAAC;AAC/D,eAAO,MAAM,UAAU,KAAyB,CAAC;AACjD,eAAO,MAAM,gBAAgB,KAA+B,CAAC"}
|
|
@@ -6,7 +6,6 @@ function resolve(path) {
|
|
|
6
6
|
return new URL(path, url);
|
|
7
7
|
}
|
|
8
8
|
var CHANGE_STREAMER_URL = resolve("./change-streamer.ts");
|
|
9
|
-
var BACKUP_WATERMARK_POLLER_URL = resolve("./backup-watermark-poller.ts");
|
|
10
9
|
var MAIN_URL = resolve("./main.ts");
|
|
11
10
|
resolve("./mutator.ts");
|
|
12
11
|
var REAPER_URL = resolve("./reaper.ts");
|
|
@@ -15,6 +14,6 @@ var SHADOW_SYNCER_URL = resolve("./shadow-syncer.ts");
|
|
|
15
14
|
var SYNCER_URL = resolve("./syncer.ts");
|
|
16
15
|
var WRITE_WORKER_URL = resolve("./write-worker.ts");
|
|
17
16
|
//#endregion
|
|
18
|
-
export {
|
|
17
|
+
export { CHANGE_STREAMER_URL, MAIN_URL, REAPER_URL, REPLICATOR_URL, SHADOW_SYNCER_URL, SYNCER_URL, WRITE_WORKER_URL };
|
|
19
18
|
|
|
20
19
|
//# sourceMappingURL=worker-urls.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker-urls.js","names":[],"sources":["../../../../../zero-cache/src/server/worker-urls.ts"],"sourcesContent":["// This module provides URLs for worker files.\n\nconst tsRe = /\\.ts$/;\n\nfunction resolve(path: string): URL {\n const {url} = import.meta;\n if (url.endsWith('.js')) {\n // When compiled, change .ts to .js\n path = path.replace(tsRe, '.js');\n }\n return new URL(path, url);\n}\n\n// These URLs are part of the build process. See ../../zero/tool/build.ts\n// All these urls must be relative to this file and be located in the same directory.\n\nexport const CHANGE_STREAMER_URL = resolve('./change-streamer.ts');\nexport const
|
|
1
|
+
{"version":3,"file":"worker-urls.js","names":[],"sources":["../../../../../zero-cache/src/server/worker-urls.ts"],"sourcesContent":["// This module provides URLs for worker files.\n\nconst tsRe = /\\.ts$/;\n\nfunction resolve(path: string): URL {\n const {url} = import.meta;\n if (url.endsWith('.js')) {\n // When compiled, change .ts to .js\n path = path.replace(tsRe, '.js');\n }\n return new URL(path, url);\n}\n\n// These URLs are part of the build process. See ../../zero/tool/build.ts\n// All these urls must be relative to this file and be located in the same directory.\n\nexport const CHANGE_STREAMER_URL = resolve('./change-streamer.ts');\nexport const MAIN_URL = resolve('./main.ts');\nexport const MUTATOR_URL = resolve('./mutator.ts');\nexport const REAPER_URL = resolve('./reaper.ts');\nexport const REPLICATOR_URL = resolve('./replicator.ts');\nexport const SHADOW_SYNCER_URL = resolve('./shadow-syncer.ts');\nexport const SYNCER_URL = resolve('./syncer.ts');\nexport const WRITE_WORKER_URL = resolve('./write-worker.ts');\n"],"mappings":";AAEA,IAAM,OAAO;AAEb,SAAS,QAAQ,MAAmB;CAClC,MAAM,EAAC,QAAO,OAAO;CACrB,IAAI,IAAI,SAAS,KAAK,GAEpB,OAAO,KAAK,QAAQ,MAAM,KAAK;CAEjC,OAAO,IAAI,IAAI,MAAM,GAAG;AAC1B;AAKA,IAAa,sBAAsB,QAAQ,sBAAsB;AACjE,IAAa,WAAW,QAAQ,WAAW;AAChB,QAAQ,cAAc;AACjD,IAAa,aAAa,QAAQ,aAAa;AAC/C,IAAa,iBAAiB,QAAQ,iBAAiB;AACvD,IAAa,oBAAoB,QAAQ,oBAAoB;AAC7D,IAAa,aAAa,QAAQ,aAAa;AAC/C,IAAa,mBAAmB,QAAQ,mBAAmB"}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import type { LogContext } from '@rocicorp/logger';
|
|
2
2
|
import type { NormalizedZeroConfig } from '../../config/normalize.ts';
|
|
3
|
-
import type { ProcessManager } from '../life-cycle.ts';
|
|
4
3
|
import { BackupMonitor } from './backup-monitor.ts';
|
|
5
4
|
import type { ChangeStreamerService } from './change-streamer.ts';
|
|
6
5
|
import { type BackupStateVerifier } from './litestream3-prometheus-poller.ts';
|
|
7
6
|
export type BackupCleanupMonitorFactoryOptions = {
|
|
8
7
|
lc: LogContext;
|
|
9
8
|
config: NormalizedZeroConfig;
|
|
10
|
-
processes: ProcessManager;
|
|
11
9
|
replicaFile: string;
|
|
12
10
|
changeStreamer: ChangeStreamerService;
|
|
13
11
|
verifyBackupState?: BackupStateVerifier | undefined;
|
|
14
|
-
env?: NodeJS.ProcessEnv | undefined;
|
|
15
12
|
};
|
|
16
|
-
export declare function createBackupCleanupMonitor({ lc, config,
|
|
13
|
+
export declare function createBackupCleanupMonitor({ lc, config, replicaFile, changeStreamer, verifyBackupState, }: BackupCleanupMonitorFactoryOptions): BackupMonitor;
|
|
17
14
|
//# sourceMappingURL=backup-cleanup-monitor-factory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backup-cleanup-monitor-factory.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"backup-cleanup-monitor-factory.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,2BAA2B,CAAC;AAGpE,OAAO,EAAyB,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC1E,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,sBAAsB,CAAC;AAChE,OAAO,EACL,KAAK,mBAAmB,EAEzB,MAAM,oCAAoC,CAAC;AAI5C,MAAM,MAAM,kCAAkC,GAAG;IAC/C,EAAE,EAAE,UAAU,CAAC;IACf,MAAM,EAAE,oBAAoB,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,qBAAqB,CAAC;IACtC,iBAAiB,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;CACrD,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,EACzC,EAAE,EACF,MAAM,EACN,WAAW,EACX,cAAc,EACd,iBAAiB,GAClB,EAAE,kCAAkC,GAAG,aAAa,CAyCpD"}
|
|
@@ -1,33 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { BACKUP_WATERMARK_POLLER_URL } from "../../server/worker-urls.js";
|
|
3
|
-
import { getVfsEnv } from "../litestream/vfs-watermark-poller.js";
|
|
4
|
-
import { Subscription } from "../../types/subscription.js";
|
|
1
|
+
import { must } from "../../../../shared/src/must.js";
|
|
5
2
|
import { getLastBackupTime } from "../litestream/commands.js";
|
|
6
3
|
import { BackupMonitor } from "./backup-monitor.js";
|
|
7
4
|
import { Litestream3PrometheusPoller } from "./litestream3-prometheus-poller.js";
|
|
8
5
|
import { ReplicaPoller } from "./replica-poller.js";
|
|
6
|
+
import { VfsWatermarkPoller } from "./vfs-watermark-poller.js";
|
|
9
7
|
//#region ../zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.ts
|
|
10
|
-
function createBackupCleanupMonitor({ lc, config,
|
|
11
|
-
const { litestream, replica } = config;
|
|
8
|
+
function createBackupCleanupMonitor({ lc, config, replicaFile, changeStreamer, verifyBackupState }) {
|
|
9
|
+
const { log, litestream, replica } = config;
|
|
12
10
|
const { backupURL } = litestream;
|
|
13
11
|
let stream;
|
|
14
12
|
if (!backupURL) stream = new ReplicaPoller(lc, replicaFile).start();
|
|
15
13
|
else if (config.litestream.backupUsingV5) {
|
|
16
|
-
const { logLevel, endpoint, region,
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const { logLevel, endpoint, region, vfsQueryExecutable, vfsPollIntervalMs: remotePollIntervalMs } = litestream;
|
|
15
|
+
stream = new VfsWatermarkPoller(lc, replicaFile, {
|
|
16
|
+
executable: must(vfsQueryExecutable, `litestream-vfs-query-executable must be defined`),
|
|
17
|
+
remotePollIntervalMs,
|
|
19
18
|
backupURL,
|
|
20
|
-
endpoint,
|
|
21
19
|
region,
|
|
20
|
+
endpoint,
|
|
22
21
|
logLevel,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
26
|
-
processes.addWorker(forkChildWorker(BACKUP_WATERMARK_POLLER_URL, {
|
|
27
|
-
...env,
|
|
28
|
-
...vfsEnv
|
|
29
|
-
}), "supporting", "backup-watermark-poller").onMessageType("backupWatermarkUpdate", (msg) => sub.push(msg));
|
|
30
|
-
stream = sub;
|
|
22
|
+
logFormat: log.format
|
|
23
|
+
}).start();
|
|
31
24
|
} else {
|
|
32
25
|
const { port: metricsPort } = litestream;
|
|
33
26
|
stream = new Litestream3PrometheusPoller(lc, replicaFile, backupURL, `http://localhost:${metricsPort}/metrics`, verifyBackupState ?? (() => getLastBackupTime(lc, litestream, replica.file))).start();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backup-cleanup-monitor-factory.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport
|
|
1
|
+
{"version":3,"file":"backup-cleanup-monitor-factory.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {must} from '../../../../shared/src/must.ts';\nimport type {NormalizedZeroConfig} from '../../config/normalize.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {getLastBackupTime} from '../litestream/commands.ts';\nimport {type BackedUpWatermark, BackupMonitor} from './backup-monitor.ts';\nimport type {ChangeStreamerService} from './change-streamer.ts';\nimport {\n type BackupStateVerifier,\n Litestream3PrometheusPoller,\n} from './litestream3-prometheus-poller.ts';\nimport {ReplicaPoller} from './replica-poller.ts';\nimport {VfsWatermarkPoller} from './vfs-watermark-poller.ts';\n\nexport type BackupCleanupMonitorFactoryOptions = {\n lc: LogContext;\n config: NormalizedZeroConfig;\n replicaFile: string;\n changeStreamer: ChangeStreamerService;\n verifyBackupState?: BackupStateVerifier | undefined;\n};\n\nexport function createBackupCleanupMonitor({\n lc,\n config,\n replicaFile,\n changeStreamer,\n verifyBackupState,\n}: BackupCleanupMonitorFactoryOptions): BackupMonitor {\n const {log, litestream, replica} = config;\n const {backupURL} = litestream;\n\n let stream: Source<BackedUpWatermark>;\n\n if (!backupURL) {\n stream = new ReplicaPoller(lc, replicaFile).start();\n } else if (config.litestream.backupUsingV5) {\n const {\n logLevel,\n endpoint,\n region,\n vfsQueryExecutable,\n vfsPollIntervalMs: remotePollIntervalMs,\n } = litestream;\n stream = new VfsWatermarkPoller(lc, replicaFile, {\n executable: must(\n vfsQueryExecutable,\n `litestream-vfs-query-executable must be defined`,\n ),\n remotePollIntervalMs,\n backupURL,\n region,\n endpoint,\n logLevel,\n logFormat: log.format,\n }).start();\n } else {\n const {port: metricsPort} = litestream;\n stream = new Litestream3PrometheusPoller(\n lc,\n replicaFile,\n backupURL,\n `http://localhost:${metricsPort}/metrics`,\n verifyBackupState ??\n (() => getLastBackupTime(lc, litestream, replica.file)),\n ).start();\n }\n\n return new BackupMonitor(lc, stream, changeStreamer, replicaFile);\n}\n"],"mappings":";;;;;;;AAsBA,SAAgB,2BAA2B,EACzC,IACA,QACA,aACA,gBACA,qBACoD;CACpD,MAAM,EAAC,KAAK,YAAY,YAAW;CACnC,MAAM,EAAC,cAAa;CAEpB,IAAI;CAEJ,IAAI,CAAC,WACH,SAAS,IAAI,cAAc,IAAI,WAAW,EAAE,MAAM;MAC7C,IAAI,OAAO,WAAW,eAAe;EAC1C,MAAM,EACJ,UACA,UACA,QACA,oBACA,mBAAmB,yBACjB;EACJ,SAAS,IAAI,mBAAmB,IAAI,aAAa;GAC/C,YAAY,KACV,oBACA,iDACF;GACA;GACA;GACA;GACA;GACA;GACA,WAAW,IAAI;EACjB,CAAC,EAAE,MAAM;CACX,OAAO;EACL,MAAM,EAAC,MAAM,gBAAe;EAC5B,SAAS,IAAI,4BACX,IACA,aACA,WACA,oBAAoB,YAAY,WAChC,4BACS,kBAAkB,IAAI,YAAY,QAAQ,IAAI,EACzD,EAAE,MAAM;CACV;CAEA,OAAO,IAAI,cAAc,IAAI,QAAQ,gBAAgB,WAAW;AAClE"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { LogContext } from '@rocicorp/logger';
|
|
2
|
+
import type { Source } from '../../types/streams.ts';
|
|
3
|
+
import type { BackedUpWatermark } from './backup-monitor.ts';
|
|
4
|
+
export type VfsPollerConfig = {
|
|
5
|
+
executable: string;
|
|
6
|
+
remotePollIntervalMs: number;
|
|
7
|
+
remoteQueryTimeoutMs?: number;
|
|
8
|
+
backupURL: string;
|
|
9
|
+
region?: string | undefined;
|
|
10
|
+
endpoint?: string | undefined;
|
|
11
|
+
logLevel?: string;
|
|
12
|
+
logFormat?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare class VfsWatermarkPoller {
|
|
15
|
+
#private;
|
|
16
|
+
constructor(lc: LogContext, replicaFile: string, pollerConfig: VfsPollerConfig);
|
|
17
|
+
/**
|
|
18
|
+
* Starts polling for watermarks and pushes new ones to returned stream.
|
|
19
|
+
* Must only be called once.
|
|
20
|
+
*/
|
|
21
|
+
start(): Source<BackedUpWatermark>;
|
|
22
|
+
readonly checkLocalWatermark: () => void;
|
|
23
|
+
}
|
|
24
|
+
export declare function buildLitestreamVfsReplicaURL({ backupURL, endpoint, region, }: {
|
|
25
|
+
readonly backupURL: string;
|
|
26
|
+
readonly endpoint?: string | undefined;
|
|
27
|
+
readonly region?: string | undefined;
|
|
28
|
+
}): string;
|
|
29
|
+
//# sourceMappingURL=vfs-watermark-poller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vfs-watermark-poller.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/vfs-watermark-poller.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAIjD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,wBAAwB,CAAC;AAGnD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AAe3D,MAAM,MAAM,eAAe,GAAG;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAyBF,qBAAa,kBAAkB;;gBAmB3B,EAAE,EAAE,UAAU,EACd,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,eAAe;IAyB/B;;;OAGG;IACH,KAAK,IAAI,MAAM,CAAC,iBAAiB,CAAC;IAYlC,QAAQ,CAAC,mBAAmB,aAiB1B;CA6GH;AAED,wBAAgB,4BAA4B,CAAC,EAC3C,SAAS,EACT,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC,GAAG,MAAM,CAiBT"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { assert } from "../../../../shared/src/asserts.js";
|
|
2
|
+
import { parse, valita_exports } from "../../../../shared/src/valita.js";
|
|
3
|
+
import { Database } from "../../../../zqlite/src/db.js";
|
|
4
|
+
import { RunningState } from "../running-state.js";
|
|
5
|
+
import { Subscription } from "../../types/subscription.js";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
8
|
+
//#region ../zero-cache/src/services/change-streamer/vfs-watermark-poller.ts
|
|
9
|
+
var LOCAL_CHECK_INTERVAL_MS = 1e3;
|
|
10
|
+
var queryResultRowSchema = valita_exports.object({
|
|
11
|
+
stateVersion: valita_exports.string(),
|
|
12
|
+
writeTimeMs: valita_exports.number()
|
|
13
|
+
});
|
|
14
|
+
var remoteQueryMetadataSchema = valita_exports.object({ litestream_time: valita_exports.string().map((str) => new Date(str).getTime()).optional() });
|
|
15
|
+
var remoteQueryLineSchema = valita_exports.object({
|
|
16
|
+
query_result: valita_exports.tuple([queryResultRowSchema]),
|
|
17
|
+
metadata: remoteQueryMetadataSchema
|
|
18
|
+
});
|
|
19
|
+
var INITIAL_BACKOFF_MS = 2e3;
|
|
20
|
+
var MAX_BACKOFF_MS = 3e4;
|
|
21
|
+
var VfsWatermarkPoller = class {
|
|
22
|
+
#lc;
|
|
23
|
+
#state = new RunningState("vfs-watermark-poller");
|
|
24
|
+
#pollerConfig;
|
|
25
|
+
#readLocalWatermark;
|
|
26
|
+
#stream;
|
|
27
|
+
#localWatermark;
|
|
28
|
+
#remoteWatermark;
|
|
29
|
+
#localPollTimer;
|
|
30
|
+
#shouldPollRemote = false;
|
|
31
|
+
#remotePoller;
|
|
32
|
+
#remotePollerBackoffMs = INITIAL_BACKOFF_MS;
|
|
33
|
+
#remotePollerBackoffTimer;
|
|
34
|
+
constructor(lc, replicaFile, pollerConfig) {
|
|
35
|
+
this.#lc = lc.withContext("component", "vfs-watermark-poller");
|
|
36
|
+
this.#pollerConfig = pollerConfig;
|
|
37
|
+
const db = new Database(this.#lc, replicaFile, { readonly: true });
|
|
38
|
+
this.#readLocalWatermark = db.prepare(`SELECT stateVersion, writeTimeMs FROM "_zero.replicationState"`);
|
|
39
|
+
this.#localWatermark = parse(this.#readLocalWatermark.get(), queryResultRowSchema);
|
|
40
|
+
this.#stream = Subscription.create({ cleanup: () => {
|
|
41
|
+
this.#disableRemotePoller();
|
|
42
|
+
clearInterval(this.#localPollTimer);
|
|
43
|
+
clearTimeout(this.#remotePollerBackoffTimer);
|
|
44
|
+
this.#state.stop(this.#lc);
|
|
45
|
+
db.close();
|
|
46
|
+
} });
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Starts polling for watermarks and pushes new ones to returned stream.
|
|
50
|
+
* Must only be called once.
|
|
51
|
+
*/
|
|
52
|
+
start() {
|
|
53
|
+
assert(this.#localPollTimer === void 0, `Already called start()`);
|
|
54
|
+
this.#lc.info?.(`monitoring backups at ${this.#pollerConfig.backupURL}`);
|
|
55
|
+
this.#localPollTimer = setInterval(this.checkLocalWatermark, LOCAL_CHECK_INTERVAL_MS);
|
|
56
|
+
this.checkLocalWatermark();
|
|
57
|
+
return this.#stream;
|
|
58
|
+
}
|
|
59
|
+
checkLocalWatermark = () => {
|
|
60
|
+
try {
|
|
61
|
+
this.#localWatermark = parse(this.#readLocalWatermark.get(), queryResultRowSchema);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
this.#lc.warn?.(`unable to read local watermark`, e);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (this.#localWatermark.stateVersion !== this.#remoteWatermark?.watermark) this.#enableRemotePoller();
|
|
67
|
+
else this.#disableRemotePoller();
|
|
68
|
+
};
|
|
69
|
+
#enableRemotePoller() {
|
|
70
|
+
if (this.#remotePoller || this.#remotePollerBackoffTimer) return;
|
|
71
|
+
this.#shouldPollRemote = true;
|
|
72
|
+
if (this.#remoteWatermark) this.#lc.debug?.(`local and remote watermarks differ. resuming backup polling`, {
|
|
73
|
+
local: this.#localWatermark,
|
|
74
|
+
remote: this.#remoteWatermark
|
|
75
|
+
});
|
|
76
|
+
const child = spawn(this.#pollerConfig.executable, [
|
|
77
|
+
"--replica-url",
|
|
78
|
+
buildLitestreamVfsReplicaURL(this.#pollerConfig),
|
|
79
|
+
"--query",
|
|
80
|
+
`SELECT stateVersion, writeTimeMs FROM "_zero.replicationState"`,
|
|
81
|
+
"--remote-poll-interval",
|
|
82
|
+
`${this.#pollerConfig.remotePollIntervalMs}ms`,
|
|
83
|
+
...this.#pollerConfig.logLevel ? ["--log-level", this.#pollerConfig.logLevel] : [],
|
|
84
|
+
...this.#pollerConfig.logFormat ? ["--log-format", this.#pollerConfig.logFormat] : [],
|
|
85
|
+
...this.#pollerConfig.remoteQueryTimeoutMs ? ["--query-timeout", `${this.#pollerConfig.remoteQueryTimeoutMs}ms`] : []
|
|
86
|
+
], { stdio: [
|
|
87
|
+
"pipe",
|
|
88
|
+
"pipe",
|
|
89
|
+
"inherit"
|
|
90
|
+
] }).on("error", (err) => {
|
|
91
|
+
this.#lc.error?.(`received error from vfs-query process`, err);
|
|
92
|
+
}).on("close", (code, signal) => {
|
|
93
|
+
this.#remotePoller = void 0;
|
|
94
|
+
if (this.#shouldPollRemote) {
|
|
95
|
+
if (code !== 0) this.#lc.error?.(`vfs-query exited with ${code}, ${signal}`);
|
|
96
|
+
if (!this.#remotePollerBackoffTimer) {
|
|
97
|
+
this.#remotePollerBackoffTimer = setTimeout(() => {
|
|
98
|
+
this.#remotePollerBackoffTimer = void 0;
|
|
99
|
+
if (this.#shouldPollRemote) this.#enableRemotePoller();
|
|
100
|
+
}, this.#remotePollerBackoffMs);
|
|
101
|
+
this.#remotePollerBackoffMs = Math.min(this.#remotePollerBackoffMs * 2, MAX_BACKOFF_MS);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
createInterface({ input: child.stdout }).on("line", this.#readRemotePollerLine);
|
|
106
|
+
this.#remotePoller = child;
|
|
107
|
+
}
|
|
108
|
+
#readRemotePollerLine = (line) => {
|
|
109
|
+
try {
|
|
110
|
+
const update = parse(JSON.parse(line), remoteQueryLineSchema, "passthrough");
|
|
111
|
+
const [{ stateVersion: watermark, writeTimeMs }] = update.query_result;
|
|
112
|
+
const remoteWatermark = {
|
|
113
|
+
watermark,
|
|
114
|
+
writeTimeMs,
|
|
115
|
+
backupTimeMs: update.metadata.litestream_time ?? Date.now()
|
|
116
|
+
};
|
|
117
|
+
if (remoteWatermark.watermark !== this.#remoteWatermark?.watermark) {
|
|
118
|
+
this.#lc.debug?.(`backup watermark advanced from ${this.#remoteWatermark?.watermark} to ${remoteWatermark.watermark}`, {
|
|
119
|
+
local: this.#localWatermark,
|
|
120
|
+
remote: remoteWatermark
|
|
121
|
+
});
|
|
122
|
+
this.#stream.push(remoteWatermark);
|
|
123
|
+
}
|
|
124
|
+
this.#remoteWatermark = remoteWatermark;
|
|
125
|
+
this.#remotePollerBackoffMs = INITIAL_BACKOFF_MS;
|
|
126
|
+
} catch (e) {
|
|
127
|
+
this.#lc.error?.(`unable to parse vfs-query output: ${line}`, e);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
#disableRemotePoller() {
|
|
131
|
+
if (!this.#shouldPollRemote) return;
|
|
132
|
+
this.#shouldPollRemote = false;
|
|
133
|
+
if (this.#remotePoller || this.#remotePollerBackoffTimer) this.#lc.debug?.(`local and remote watermarks match. pausing backup polling`, {
|
|
134
|
+
local: this.#localWatermark,
|
|
135
|
+
remote: this.#remoteWatermark
|
|
136
|
+
});
|
|
137
|
+
this.#remotePoller?.kill("SIGTERM");
|
|
138
|
+
this.#remotePoller = void 0;
|
|
139
|
+
clearTimeout(this.#remotePollerBackoffTimer);
|
|
140
|
+
this.#remotePollerBackoffTimer = void 0;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
function buildLitestreamVfsReplicaURL({ backupURL, endpoint, region }) {
|
|
144
|
+
if (endpoint === void 0 && region === void 0) return backupURL;
|
|
145
|
+
const url = new URL(backupURL);
|
|
146
|
+
if (url.protocol !== "s3:") return backupURL;
|
|
147
|
+
if (endpoint !== void 0 && !url.searchParams.has("endpoint")) url.searchParams.set("endpoint", endpoint);
|
|
148
|
+
if (region !== void 0 && !url.searchParams.has("region")) url.searchParams.set("region", region);
|
|
149
|
+
return url.toString();
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
export { VfsWatermarkPoller };
|
|
153
|
+
|
|
154
|
+
//# sourceMappingURL=vfs-watermark-poller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vfs-watermark-poller.js","names":["#lc","#state","#pollerConfig","#readLocalWatermark","#stream","#localWatermark","#disableRemotePoller","#localPollTimer","#remotePollerBackoffTimer","#remoteWatermark","#enableRemotePoller","#remotePoller","#shouldPollRemote","#remotePollerBackoffMs","#readRemotePollerLine"],"sources":["../../../../../../zero-cache/src/services/change-streamer/vfs-watermark-poller.ts"],"sourcesContent":["import {spawn, type ChildProcess} from 'node:child_process';\nimport {createInterface} from 'node:readline';\nimport type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {Database, type Statement} from '../../../../zqlite/src/db.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {BackedUpWatermark} from './backup-monitor.ts';\n\nconst LOCAL_CHECK_INTERVAL_MS = 1_000;\n\ntype LocalWatermark = {\n stateVersion: string;\n writeTimeMs: number;\n};\n\ntype RemoteWatermark = {\n watermark: string;\n writeTimeMs: number;\n backupTimeMs: number;\n};\n\nexport type VfsPollerConfig = {\n executable: string;\n remotePollIntervalMs: number;\n remoteQueryTimeoutMs?: number;\n backupURL: string;\n region?: string | undefined;\n endpoint?: string | undefined;\n logLevel?: string;\n logFormat?: string;\n};\n\nconst queryResultRowSchema = v.object({\n stateVersion: v.string(),\n writeTimeMs: v.number(),\n});\n\nconst remoteQueryMetadataSchema = v.object({\n litestream_time: v\n .string()\n .map(str => new Date(str).getTime())\n .optional(),\n});\n\n// Expected format of each line of stdout from the vfs-query, e.g.\n// {\"query_result\":[{\"stateVersion\":\"51y50ko\",\"writeTimeMs\":1787259951121}],\"metadata\":{\"litestream_time\":\"2026-08-20T21:05:57.36439Z\"}}\n// {\"query_result\":[{\"stateVersion\":\"51y7360\",\"writeTimeMs\":1787259966137}],\"metadata\":{\"litestream_time\":\"2026-08-20T21:06:12.384531Z\"}}\nconst remoteQueryLineSchema = v.object({\n query_result: v.tuple([queryResultRowSchema]),\n metadata: remoteQueryMetadataSchema,\n});\n\nconst INITIAL_BACKOFF_MS = 2_000;\nconst MAX_BACKOFF_MS = 30_000;\n\nexport class VfsWatermarkPoller {\n readonly #lc: LogContext;\n readonly #state = new RunningState('vfs-watermark-poller');\n readonly #pollerConfig: VfsPollerConfig;\n readonly #readLocalWatermark: Statement;\n\n readonly #stream: Subscription<BackedUpWatermark>;\n\n #localWatermark: LocalWatermark;\n #remoteWatermark: RemoteWatermark | undefined;\n #localPollTimer: NodeJS.Timeout | undefined;\n\n #shouldPollRemote = false;\n\n #remotePoller: ChildProcess | undefined;\n #remotePollerBackoffMs = INITIAL_BACKOFF_MS;\n #remotePollerBackoffTimer: NodeJS.Timeout | undefined;\n\n constructor(\n lc: LogContext,\n replicaFile: string,\n pollerConfig: VfsPollerConfig,\n ) {\n this.#lc = lc.withContext('component', 'vfs-watermark-poller');\n this.#pollerConfig = pollerConfig;\n\n const db = new Database(this.#lc, replicaFile, {readonly: true});\n this.#readLocalWatermark = db.prepare(\n `SELECT stateVersion, writeTimeMs FROM \"_zero.replicationState\"`,\n );\n // Purely to type the member variable as always defined\n this.#localWatermark = v.parse(\n this.#readLocalWatermark.get(),\n queryResultRowSchema,\n );\n this.#stream = Subscription.create<BackedUpWatermark>({\n cleanup: () => {\n this.#disableRemotePoller();\n clearInterval(this.#localPollTimer);\n clearTimeout(this.#remotePollerBackoffTimer);\n this.#state.stop(this.#lc);\n db.close();\n },\n });\n }\n\n /**\n * Starts polling for watermarks and pushes new ones to returned stream.\n * Must only be called once.\n */\n start(): Source<BackedUpWatermark> {\n assert(this.#localPollTimer === undefined, `Already called start()`);\n this.#lc.info?.(`monitoring backups at ${this.#pollerConfig.backupURL}`);\n this.#localPollTimer = setInterval(\n this.checkLocalWatermark,\n LOCAL_CHECK_INTERVAL_MS,\n );\n this.checkLocalWatermark();\n return this.#stream;\n }\n\n // Exported for testing\n readonly checkLocalWatermark = () => {\n try {\n this.#localWatermark = v.parse(\n this.#readLocalWatermark.get(),\n queryResultRowSchema,\n );\n } catch (e) {\n this.#lc.warn?.(`unable to read local watermark`, e);\n return;\n }\n if (\n this.#localWatermark.stateVersion !== this.#remoteWatermark?.watermark\n ) {\n this.#enableRemotePoller();\n } else {\n this.#disableRemotePoller();\n }\n };\n\n #enableRemotePoller() {\n if (this.#remotePoller || this.#remotePollerBackoffTimer) {\n return; // already polling or backing off\n }\n this.#shouldPollRemote = true;\n if (this.#remoteWatermark) {\n this.#lc.debug?.(\n `local and remote watermarks differ. resuming backup polling`,\n {local: this.#localWatermark, remote: this.#remoteWatermark},\n );\n }\n const child = spawn(\n this.#pollerConfig.executable,\n [\n '--replica-url',\n buildLitestreamVfsReplicaURL(this.#pollerConfig),\n '--query',\n `SELECT stateVersion, writeTimeMs FROM \"_zero.replicationState\"`,\n '--remote-poll-interval',\n `${this.#pollerConfig.remotePollIntervalMs}ms`,\n ...(this.#pollerConfig.logLevel\n ? ['--log-level', this.#pollerConfig.logLevel]\n : []),\n ...(this.#pollerConfig.logFormat\n ? ['--log-format', this.#pollerConfig.logFormat]\n : []),\n ...(this.#pollerConfig.remoteQueryTimeoutMs\n ? ['--query-timeout', `${this.#pollerConfig.remoteQueryTimeoutMs}ms`]\n : []),\n ],\n {stdio: ['pipe', 'pipe', 'inherit']},\n )\n .on('error', err => {\n this.#lc.error?.(`received error from vfs-query process`, err);\n })\n .on('close', (code, signal) => {\n this.#remotePoller = undefined;\n if (this.#shouldPollRemote) {\n if (code !== 0) {\n this.#lc.error?.(`vfs-query exited with ${code}, ${signal}`);\n }\n if (!this.#remotePollerBackoffTimer) {\n this.#remotePollerBackoffTimer = setTimeout(() => {\n this.#remotePollerBackoffTimer = undefined;\n if (this.#shouldPollRemote) {\n this.#enableRemotePoller();\n }\n }, this.#remotePollerBackoffMs);\n this.#remotePollerBackoffMs = Math.min(\n this.#remotePollerBackoffMs * 2,\n MAX_BACKOFF_MS,\n );\n }\n }\n });\n createInterface({input: child.stdout}).on(\n 'line',\n this.#readRemotePollerLine,\n );\n this.#remotePoller = child;\n }\n\n readonly #readRemotePollerLine = (line: string) => {\n try {\n const update = v.parse(\n JSON.parse(line),\n remoteQueryLineSchema,\n 'passthrough',\n );\n const [{stateVersion: watermark, writeTimeMs}] = update.query_result;\n const remoteWatermark: RemoteWatermark = {\n watermark,\n writeTimeMs,\n backupTimeMs: update.metadata.litestream_time ?? Date.now(),\n };\n if (remoteWatermark.watermark !== this.#remoteWatermark?.watermark) {\n this.#lc.debug?.(\n `backup watermark advanced from ${this.#remoteWatermark?.watermark} to ${remoteWatermark.watermark}`,\n {local: this.#localWatermark, remote: remoteWatermark},\n );\n this.#stream.push(remoteWatermark);\n }\n this.#remoteWatermark = remoteWatermark;\n\n // Reset backoff on successful output from the poller\n this.#remotePollerBackoffMs = INITIAL_BACKOFF_MS;\n } catch (e) {\n this.#lc.error?.(`unable to parse vfs-query output: ${line}`, e);\n }\n };\n\n #disableRemotePoller() {\n if (!this.#shouldPollRemote) {\n return;\n }\n this.#shouldPollRemote = false;\n if (this.#remotePoller || this.#remotePollerBackoffTimer) {\n this.#lc.debug?.(\n `local and remote watermarks match. pausing backup polling`,\n {local: this.#localWatermark, remote: this.#remoteWatermark},\n );\n }\n this.#remotePoller?.kill('SIGTERM');\n this.#remotePoller = undefined;\n clearTimeout(this.#remotePollerBackoffTimer);\n this.#remotePollerBackoffTimer = undefined;\n }\n}\n\nexport function buildLitestreamVfsReplicaURL({\n backupURL,\n endpoint,\n region,\n}: {\n readonly backupURL: string;\n readonly endpoint?: string | undefined;\n readonly region?: string | undefined;\n}): string {\n if (endpoint === undefined && region === undefined) {\n return backupURL;\n }\n\n const url = new URL(backupURL);\n if (url.protocol !== 's3:') {\n return backupURL;\n }\n\n if (endpoint !== undefined && !url.searchParams.has('endpoint')) {\n url.searchParams.set('endpoint', endpoint);\n }\n if (region !== undefined && !url.searchParams.has('region')) {\n url.searchParams.set('region', region);\n }\n return url.toString();\n}\n"],"mappings":"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"}
|
|
@@ -74,9 +74,6 @@ function childWorker(moduleUrl, env, ...args) {
|
|
|
74
74
|
}
|
|
75
75
|
return forkChildWorkerWithArgs(moduleUrl, env, args);
|
|
76
76
|
}
|
|
77
|
-
function forkChildWorker(moduleUrl, env, ...args) {
|
|
78
|
-
return forkChildWorkerWithArgs(moduleUrl, env, workerArgs(args));
|
|
79
|
-
}
|
|
80
77
|
function workerArgs(args) {
|
|
81
78
|
return [...args, ...process.argv.slice(2)];
|
|
82
79
|
}
|
|
@@ -116,6 +113,6 @@ function inProcChannel() {
|
|
|
116
113
|
}))];
|
|
117
114
|
}
|
|
118
115
|
//#endregion
|
|
119
|
-
export { childWorker,
|
|
116
|
+
export { childWorker, parentWorker, singleProcessMode };
|
|
120
117
|
|
|
121
118
|
//# sourceMappingURL=processes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"processes.js","names":[],"sources":["../../../../../zero-cache/src/types/processes.ts"],"sourcesContent":["import {\n type ChildProcess,\n fork,\n type SendHandle,\n type Serializable,\n} from 'node:child_process';\nimport EventEmitter from 'node:events';\nimport {platform} from 'node:os';\nimport {pid} from 'node:process';\n\n/**\n * Central registry of message type names, which are used to identify\n * the payload in {@link Message} objects sent between processes. The\n * payloads themselves are implementation specific and defined in each\n * component; only the type name is reserved here to avoid collisions.\n *\n * Receiving logic can call {@link getMessage()} with the name of\n * the message of interest to filter messages to those of interest.\n */\nexport const MESSAGE_TYPES = {\n handoff: 'handoff',\n status: 'status',\n subscribe: 'subscribe',\n notify: 'notify',\n ready: 'ready',\n backupWatermarkUpdate: 'backupWatermakUpdate',\n} as const;\n\nexport type Message<Payload> = [keyof typeof MESSAGE_TYPES, Payload];\n\nfunction getMessage<M extends Message<unknown>>(\n type: M[0],\n data: unknown,\n): M[1] | null {\n if (Array.isArray(data) && data.length === 2 && data[0] === type) {\n return data[1] as M[1];\n }\n return null;\n}\n\nfunction onMessageType<M extends Message<unknown>>(\n e: EventEmitter,\n type: M[0],\n handler: (msg: M[1], sendHandle?: SendHandle) => void,\n) {\n return e.on('message', (data, sendHandle) => {\n const msg = getMessage(type, data);\n if (msg) {\n handler(msg, sendHandle);\n }\n });\n}\n\nfunction onceMessageType<M extends Message<unknown>>(\n e: EventEmitter,\n type: M[0],\n handler: (msg: M[1], sendHandle?: SendHandle) => void,\n) {\n const listener = (data: unknown, sendHandle: SendHandle) => {\n const msg = getMessage(type, data);\n if (msg) {\n e.off('message', listener);\n handler(msg, sendHandle);\n }\n };\n return e.on('message', listener);\n}\n\nexport interface Sender {\n send<M extends Message<unknown>>(\n message: M,\n sendHandle?: SendHandle,\n callback?: (error: Error | null) => void,\n ): boolean;\n\n kill(signal?: NodeJS.Signals): void;\n}\n\nexport interface Subprocess extends Sender, EventEmitter {\n pid?: number | undefined;\n}\n\nexport interface Receiver extends EventEmitter {\n /**\n * The receiving side of {@link Sender.send()} that is a wrapper around\n * {@link on}('message', ...) that invokes the `handler` for messages of\n * the specified `type`.\n */\n onMessageType<M extends Message<unknown>>(\n type: M[0],\n handler: (msg: M[1], sendHandle?: SendHandle) => void,\n ): this;\n\n /**\n * The receiving side of {@link Sender.send()} that behaves like\n * {@link once}('message', ...) that invokes the `handler` for the next\n * message of the specified `type` and then unsubscribes.\n */\n onceMessageType<M extends Message<unknown>>(\n type: M[0],\n handler: (msg: M[1], sendHandle?: SendHandle) => void,\n ): this;\n}\n\nexport interface Worker extends Subprocess, Receiver {}\n\n/**\n * Adds the {@link Receiver.onMessageType()} and {@link Receiver.onceMessageType()}\n * methods to convert the given `EventEmitter` to a `Receiver`.\n */\nfunction wrap<P extends EventEmitter>(proc: P): P & Receiver {\n return new Proxy(proc, {\n get(target: P, prop: string | symbol, receiver: unknown) {\n switch (prop) {\n case 'onMessageType':\n return (\n type: keyof typeof MESSAGE_TYPES,\n handler: (msg: unknown, sendHandle?: SendHandle) => void,\n ) => {\n onMessageType(target, type, handler);\n return receiver; // this\n };\n case 'onceMessageType':\n return (\n type: keyof typeof MESSAGE_TYPES,\n handler: (msg: unknown, sendHandle?: SendHandle) => void,\n ) => {\n onceMessageType(target, type, handler);\n return receiver; // this\n };\n }\n return Reflect.get(target, prop, receiver);\n },\n }) as P & Receiver;\n}\n\ntype Proc = Pick<ChildProcess, 'send' | 'kill' | 'pid'> & EventEmitter;\n\n/**\n * The parentWorker for forked processes, or `null` if the process was not forked.\n * (Analogous to the `parentPort: MessagePort | null` of the `\"workers\"` library).\n */\nexport const parentWorker: Worker | null = process.send\n ? wrap(process as Proc)\n : null;\n\nconst SINGLE_PROCESS = 'SINGLE_PROCESS';\nlet singleProcessOverride = false;\nexport function singleProcessMode(): boolean {\n return singleProcessOverride || (process.env[SINGLE_PROCESS] ?? '0') !== '0';\n}\n\nexport function setSingleProcessMode(enabled: boolean = true): void {\n singleProcessOverride = enabled;\n}\n\n/**\n * Determines whether a module loaded as a worker process entry point should\n * start its worker.\n *\n * Most workers are launched via {@link childWorker}, which in single-process\n * mode runs them in-process (no real fork) by invoking their exported\n * `runWorker` directly; those entry points must therefore *not* self-start in\n * single-process mode, or they would run twice.\n *\n * A worker launched via {@link forkChildWorker}, however, is *always* a real\n * forked OS process — even in single-process mode — and it inherits the\n * parent's `SINGLE_PROCESS` env. So {@link singleProcessMode} alone would\n * wrongly suppress its startup, leaving the parent to spin re-forking a child\n * that immediately exits. Such a forked child has a non-null\n * {@link parentWorker} (`process.send` is defined), which is the signal to\n * start regardless. A standalone direct launch (no parent, not single-process)\n * also starts.\n *\n * @param parent The entry point's {@link parentWorker} (non-null iff forked).\n * @param isSingleProcessMode The result of {@link singleProcessMode}.\n */\nexport function shouldStartWorker(\n parent: Worker | null,\n isSingleProcessMode: boolean,\n): boolean {\n return parent !== null || !isSingleProcessMode;\n}\n\n/**\n *\n * @param modulePath Path to the module file, relative to zero-cache/src/, or an absolute file:// URL\n */\nexport function childWorker(\n moduleUrl: URL,\n env?: NodeJS.ProcessEnv,\n ...args: string[]\n): Worker {\n args = workerArgs(args);\n\n if (singleProcessMode()) {\n const [parent, child] = inProcChannel();\n import(moduleUrl.href)\n .then(async ({default: runWorker}) => {\n try {\n await runWorker(parent, env ?? process.env, ...args);\n child.emit('close', 0);\n return;\n } catch (err) {\n child.emit('error', err);\n child.emit('close', -1);\n }\n })\n .catch(err => child.emit('error', err));\n return child;\n }\n return forkChildWorkerWithArgs(moduleUrl, env, args);\n}\n\nexport function forkChildWorker(\n moduleUrl: URL,\n env?: NodeJS.ProcessEnv,\n ...args: string[]\n): Worker {\n return forkChildWorkerWithArgs(moduleUrl, env, workerArgs(args));\n}\n\nfunction workerArgs(args: string[]): string[] {\n return [...args, ...process.argv.slice(2)];\n}\n\nfunction forkChildWorkerWithArgs(\n moduleUrl: URL,\n env: NodeJS.ProcessEnv | undefined,\n args: string[],\n): Worker {\n const child = fork(moduleUrl, args, {\n // For production / non-windows, set `detached` to `true` so that SIGINT is\n // not automatically propagated and graceful shutdown happens as intended.\n // For Win32, detached: true causes all subprocesses to open in separate\n // terminals and breaks inter-process kill signals, so set it to false.\n detached: platform() !== 'win32',\n serialization: 'advanced', // use structured clone for IPC\n env,\n });\n return wrap(child);\n}\n\n/**\n * Creates two connected `Worker` instances such that messages sent to one\n * via the {@link Worker.send()} method are received by the other's\n * `on('message', ...)` handler.\n *\n * This is analogous to the two `MessagePort`s of a `MessageChannel`, and\n * is useful for executing code written for inter-process communication\n * in a single process.\n */\nexport function inProcChannel(): [Worker, Worker] {\n const worker1 = new EventEmitter();\n const worker2 = new EventEmitter();\n\n const sendTo =\n (dest: EventEmitter) =>\n (\n message: Serializable,\n sendHandle?: SendHandle,\n callback?: (error: Error | null) => void,\n ) => {\n dest.emit('message', message, sendHandle);\n if (callback) {\n callback(null);\n }\n return true;\n };\n\n const kill =\n (dest: EventEmitter) =>\n (signal: NodeJS.Signals = 'SIGTERM') =>\n dest.emit(signal, signal);\n\n return [\n wrap(\n Object.assign(worker1, {send: sendTo(worker2), kill: kill(worker2), pid}),\n ),\n wrap(\n Object.assign(worker2, {send: sendTo(worker1), kill: kill(worker1), pid}),\n ),\n ];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"processes.js","names":[],"sources":["../../../../../zero-cache/src/types/processes.ts"],"sourcesContent":["import {\n type ChildProcess,\n fork,\n type SendHandle,\n type Serializable,\n} from 'node:child_process';\nimport EventEmitter from 'node:events';\nimport {platform} from 'node:os';\nimport {pid} from 'node:process';\n\n/**\n * Central registry of message type names, which are used to identify\n * the payload in {@link Message} objects sent between processes. The\n * payloads themselves are implementation specific and defined in each\n * component; only the type name is reserved here to avoid collisions.\n *\n * Receiving logic can call {@link getMessage()} with the name of\n * the message of interest to filter messages to those of interest.\n */\nexport const MESSAGE_TYPES = {\n handoff: 'handoff',\n status: 'status',\n subscribe: 'subscribe',\n notify: 'notify',\n ready: 'ready',\n backupWatermarkUpdate: 'backupWatermakUpdate',\n} as const;\n\nexport type Message<Payload> = [keyof typeof MESSAGE_TYPES, Payload];\n\nfunction getMessage<M extends Message<unknown>>(\n type: M[0],\n data: unknown,\n): M[1] | null {\n if (Array.isArray(data) && data.length === 2 && data[0] === type) {\n return data[1] as M[1];\n }\n return null;\n}\n\nfunction onMessageType<M extends Message<unknown>>(\n e: EventEmitter,\n type: M[0],\n handler: (msg: M[1], sendHandle?: SendHandle) => void,\n) {\n return e.on('message', (data, sendHandle) => {\n const msg = getMessage(type, data);\n if (msg) {\n handler(msg, sendHandle);\n }\n });\n}\n\nfunction onceMessageType<M extends Message<unknown>>(\n e: EventEmitter,\n type: M[0],\n handler: (msg: M[1], sendHandle?: SendHandle) => void,\n) {\n const listener = (data: unknown, sendHandle: SendHandle) => {\n const msg = getMessage(type, data);\n if (msg) {\n e.off('message', listener);\n handler(msg, sendHandle);\n }\n };\n return e.on('message', listener);\n}\n\nexport interface Sender {\n send<M extends Message<unknown>>(\n message: M,\n sendHandle?: SendHandle,\n callback?: (error: Error | null) => void,\n ): boolean;\n\n kill(signal?: NodeJS.Signals): void;\n}\n\nexport interface Subprocess extends Sender, EventEmitter {\n pid?: number | undefined;\n}\n\nexport interface Receiver extends EventEmitter {\n /**\n * The receiving side of {@link Sender.send()} that is a wrapper around\n * {@link on}('message', ...) that invokes the `handler` for messages of\n * the specified `type`.\n */\n onMessageType<M extends Message<unknown>>(\n type: M[0],\n handler: (msg: M[1], sendHandle?: SendHandle) => void,\n ): this;\n\n /**\n * The receiving side of {@link Sender.send()} that behaves like\n * {@link once}('message', ...) that invokes the `handler` for the next\n * message of the specified `type` and then unsubscribes.\n */\n onceMessageType<M extends Message<unknown>>(\n type: M[0],\n handler: (msg: M[1], sendHandle?: SendHandle) => void,\n ): this;\n}\n\nexport interface Worker extends Subprocess, Receiver {}\n\n/**\n * Adds the {@link Receiver.onMessageType()} and {@link Receiver.onceMessageType()}\n * methods to convert the given `EventEmitter` to a `Receiver`.\n */\nfunction wrap<P extends EventEmitter>(proc: P): P & Receiver {\n return new Proxy(proc, {\n get(target: P, prop: string | symbol, receiver: unknown) {\n switch (prop) {\n case 'onMessageType':\n return (\n type: keyof typeof MESSAGE_TYPES,\n handler: (msg: unknown, sendHandle?: SendHandle) => void,\n ) => {\n onMessageType(target, type, handler);\n return receiver; // this\n };\n case 'onceMessageType':\n return (\n type: keyof typeof MESSAGE_TYPES,\n handler: (msg: unknown, sendHandle?: SendHandle) => void,\n ) => {\n onceMessageType(target, type, handler);\n return receiver; // this\n };\n }\n return Reflect.get(target, prop, receiver);\n },\n }) as P & Receiver;\n}\n\ntype Proc = Pick<ChildProcess, 'send' | 'kill' | 'pid'> & EventEmitter;\n\n/**\n * The parentWorker for forked processes, or `null` if the process was not forked.\n * (Analogous to the `parentPort: MessagePort | null` of the `\"workers\"` library).\n */\nexport const parentWorker: Worker | null = process.send\n ? wrap(process as Proc)\n : null;\n\nconst SINGLE_PROCESS = 'SINGLE_PROCESS';\nlet singleProcessOverride = false;\nexport function singleProcessMode(): boolean {\n return singleProcessOverride || (process.env[SINGLE_PROCESS] ?? '0') !== '0';\n}\n\nexport function setSingleProcessMode(enabled: boolean = true): void {\n singleProcessOverride = enabled;\n}\n\n/**\n * Determines whether a module loaded as a worker process entry point should\n * start its worker.\n *\n * Most workers are launched via {@link childWorker}, which in single-process\n * mode runs them in-process (no real fork) by invoking their exported\n * `runWorker` directly; those entry points must therefore *not* self-start in\n * single-process mode, or they would run twice.\n *\n * A worker launched via {@link forkChildWorker}, however, is *always* a real\n * forked OS process — even in single-process mode — and it inherits the\n * parent's `SINGLE_PROCESS` env. So {@link singleProcessMode} alone would\n * wrongly suppress its startup, leaving the parent to spin re-forking a child\n * that immediately exits. Such a forked child has a non-null\n * {@link parentWorker} (`process.send` is defined), which is the signal to\n * start regardless. A standalone direct launch (no parent, not single-process)\n * also starts.\n *\n * @param parent The entry point's {@link parentWorker} (non-null iff forked).\n * @param isSingleProcessMode The result of {@link singleProcessMode}.\n */\nexport function shouldStartWorker(\n parent: Worker | null,\n isSingleProcessMode: boolean,\n): boolean {\n return parent !== null || !isSingleProcessMode;\n}\n\n/**\n *\n * @param modulePath Path to the module file, relative to zero-cache/src/, or an absolute file:// URL\n */\nexport function childWorker(\n moduleUrl: URL,\n env?: NodeJS.ProcessEnv,\n ...args: string[]\n): Worker {\n args = workerArgs(args);\n\n if (singleProcessMode()) {\n const [parent, child] = inProcChannel();\n import(moduleUrl.href)\n .then(async ({default: runWorker}) => {\n try {\n await runWorker(parent, env ?? process.env, ...args);\n child.emit('close', 0);\n return;\n } catch (err) {\n child.emit('error', err);\n child.emit('close', -1);\n }\n })\n .catch(err => child.emit('error', err));\n return child;\n }\n return forkChildWorkerWithArgs(moduleUrl, env, args);\n}\n\nexport function forkChildWorker(\n moduleUrl: URL,\n env?: NodeJS.ProcessEnv,\n ...args: string[]\n): Worker {\n return forkChildWorkerWithArgs(moduleUrl, env, workerArgs(args));\n}\n\nfunction workerArgs(args: string[]): string[] {\n return [...args, ...process.argv.slice(2)];\n}\n\nfunction forkChildWorkerWithArgs(\n moduleUrl: URL,\n env: NodeJS.ProcessEnv | undefined,\n args: string[],\n): Worker {\n const child = fork(moduleUrl, args, {\n // For production / non-windows, set `detached` to `true` so that SIGINT is\n // not automatically propagated and graceful shutdown happens as intended.\n // For Win32, detached: true causes all subprocesses to open in separate\n // terminals and breaks inter-process kill signals, so set it to false.\n detached: platform() !== 'win32',\n serialization: 'advanced', // use structured clone for IPC\n env,\n });\n return wrap(child);\n}\n\n/**\n * Creates two connected `Worker` instances such that messages sent to one\n * via the {@link Worker.send()} method are received by the other's\n * `on('message', ...)` handler.\n *\n * This is analogous to the two `MessagePort`s of a `MessageChannel`, and\n * is useful for executing code written for inter-process communication\n * in a single process.\n */\nexport function inProcChannel(): [Worker, Worker] {\n const worker1 = new EventEmitter();\n const worker2 = new EventEmitter();\n\n const sendTo =\n (dest: EventEmitter) =>\n (\n message: Serializable,\n sendHandle?: SendHandle,\n callback?: (error: Error | null) => void,\n ) => {\n dest.emit('message', message, sendHandle);\n if (callback) {\n callback(null);\n }\n return true;\n };\n\n const kill =\n (dest: EventEmitter) =>\n (signal: NodeJS.Signals = 'SIGTERM') =>\n dest.emit(signal, signal);\n\n return [\n wrap(\n Object.assign(worker1, {send: sendTo(worker2), kill: kill(worker2), pid}),\n ),\n wrap(\n Object.assign(worker2, {send: sendTo(worker1), kill: kill(worker1), pid}),\n ),\n ];\n}\n"],"mappings":";;;;;AA8BA,SAAS,WACP,MACA,MACa;CACb,IAAI,MAAM,QAAQ,IAAI,KAAK,KAAK,WAAW,KAAK,KAAK,OAAO,MAC1D,OAAO,KAAK;CAEd,OAAO;AACT;AAEA,SAAS,cACP,GACA,MACA,SACA;CACA,OAAO,EAAE,GAAG,YAAY,MAAM,eAAe;EAC3C,MAAM,MAAM,WAAW,MAAM,IAAI;EACjC,IAAI,KACF,QAAQ,KAAK,UAAU;CAE3B,CAAC;AACH;AAEA,SAAS,gBACP,GACA,MACA,SACA;CACA,MAAM,YAAY,MAAe,eAA2B;EAC1D,MAAM,MAAM,WAAW,MAAM,IAAI;EACjC,IAAI,KAAK;GACP,EAAE,IAAI,WAAW,QAAQ;GACzB,QAAQ,KAAK,UAAU;EACzB;CACF;CACA,OAAO,EAAE,GAAG,WAAW,QAAQ;AACjC;;;;;AA4CA,SAAS,KAA6B,MAAuB;CAC3D,OAAO,IAAI,MAAM,MAAM,EACrB,IAAI,QAAW,MAAuB,UAAmB;EACvD,QAAQ,MAAR;GACE,KAAK,iBACH,QACE,MACA,YACG;IACH,cAAc,QAAQ,MAAM,OAAO;IACnC,OAAO;GACT;GACF,KAAK,mBACH,QACE,MACA,YACG;IACH,gBAAgB,QAAQ,MAAM,OAAO;IACrC,OAAO;GACT;EACJ;EACA,OAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;CAC3C,EACF,CAAC;AACH;;;;;AAQA,IAAa,eAA8B,QAAQ,OAC/C,KAAK,OAAe,IACpB;AAEJ,IAAM,iBAAiB;AACvB,IAAI,wBAAwB;AAC5B,SAAgB,oBAA6B;CAC3C,OAAO,0BAA0B,QAAQ,IAAI,mBAAmB,SAAS;AAC3E;;;;;AAsCA,SAAgB,YACd,WACA,KACA,GAAG,MACK;CACR,OAAO,WAAW,IAAI;CAEtB,IAAI,kBAAkB,GAAG;EACvB,MAAM,CAAC,QAAQ,SAAS,cAAc;EACtC,OAAO,UAAU,MACd,KAAK,OAAO,EAAC,SAAS,gBAAe;GACpC,IAAI;IACF,MAAM,UAAU,QAAQ,OAAO,QAAQ,KAAK,GAAG,IAAI;IACnD,MAAM,KAAK,SAAS,CAAC;IACrB;GACF,SAAS,KAAK;IACZ,MAAM,KAAK,SAAS,GAAG;IACvB,MAAM,KAAK,SAAS,EAAE;GACxB;EACF,CAAC,EACA,OAAM,QAAO,MAAM,KAAK,SAAS,GAAG,CAAC;EACxC,OAAO;CACT;CACA,OAAO,wBAAwB,WAAW,KAAK,IAAI;AACrD;AAUA,SAAS,WAAW,MAA0B;CAC5C,OAAO,CAAC,GAAG,MAAM,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC;AAC3C;AAEA,SAAS,wBACP,WACA,KACA,MACQ;CAUR,OAAO,KATO,KAAK,WAAW,MAAM;EAKlC,UAAU,SAAS,MAAM;EACzB,eAAe;EACf;CACF,CACY,CAAK;AACnB;;;;;;;;;;AAWA,SAAgB,gBAAkC;CAChD,MAAM,UAAU,IAAI,aAAa;CACjC,MAAM,UAAU,IAAI,aAAa;CAEjC,MAAM,UACH,UAEC,SACA,YACA,aACG;EACH,KAAK,KAAK,WAAW,SAAS,UAAU;EACxC,IAAI,UACF,SAAS,IAAI;EAEf,OAAO;CACT;CAEF,MAAM,QACH,UACA,SAAyB,cACxB,KAAK,KAAK,QAAQ,MAAM;CAE5B,OAAO,CACL,KACE,OAAO,OAAO,SAAS;EAAC,MAAM,OAAO,OAAO;EAAG,MAAM,KAAK,OAAO;EAAG;CAAG,CAAC,CAC1E,GACA,KACE,OAAO,OAAO,SAAS;EAAC,MAAM,OAAO,OAAO;EAAG,MAAM,KAAK,OAAO;EAAG;CAAG,CAAC,CAC1E,CACF;AACF"}
|
package/out/zqlite/src/db.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { manualSpan } from "../../otel/src/span.js";
|
|
2
2
|
import { version } from "../../otel/src/version.js";
|
|
3
3
|
import { trace } from "@opentelemetry/api";
|
|
4
|
-
import
|
|
4
|
+
import SQLite3Database, { SqliteError } from "@rocicorp/zero-sqlite3";
|
|
5
5
|
//#region ../zqlite/src/db.ts
|
|
6
6
|
var tracer = trace.getTracer("view-syncer", version);
|
|
7
7
|
var AUTO_VACUUM_INCREMENTAL = 2;
|
|
@@ -17,7 +17,7 @@ var Database = class {
|
|
|
17
17
|
constructor(lc, path, options, slowQueryThreshold = 100) {
|
|
18
18
|
try {
|
|
19
19
|
this.#lc = lc.withContext("class", "Database").withContext("path", path);
|
|
20
|
-
this.#db = new
|
|
20
|
+
this.#db = new SQLite3Database(path, options);
|
|
21
21
|
this.#threshold = slowQueryThreshold;
|
|
22
22
|
this.pragma("case_sensitive_like = ON");
|
|
23
23
|
const [{ page_size: pageSize }] = this.pragma("page_size");
|
|
@@ -3,7 +3,7 @@ import { must } from "../../shared/src/must.js";
|
|
|
3
3
|
import { buildSelectQuery } from "./query-builder.js";
|
|
4
4
|
import { compileInline } from "./internal/sql-inline.js";
|
|
5
5
|
import { SQLiteStatFanout } from "./sqlite-stat-fanout.js";
|
|
6
|
-
import
|
|
6
|
+
import SQLite3Database from "@rocicorp/zero-sqlite3";
|
|
7
7
|
//#region ../zqlite/src/sqlite-cost-model.ts
|
|
8
8
|
/**
|
|
9
9
|
* Creates a SQLite-based cost model for query planning.
|
|
@@ -66,13 +66,13 @@ function removeCorrelatedSubqueries(condition) {
|
|
|
66
66
|
function getScanstatusLoops(stmt) {
|
|
67
67
|
const loops = [];
|
|
68
68
|
for (let idx = 0;; idx++) {
|
|
69
|
-
const selectId = stmt.scanStatus(idx,
|
|
69
|
+
const selectId = stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_SELECTID, 1);
|
|
70
70
|
if (selectId === void 0) break;
|
|
71
71
|
loops.push({
|
|
72
72
|
selectId: must(selectId),
|
|
73
|
-
parentId: must(stmt.scanStatus(idx,
|
|
74
|
-
explain: must(stmt.scanStatus(idx,
|
|
75
|
-
est: must(stmt.scanStatus(idx,
|
|
73
|
+
parentId: must(stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_PARENTID, 1)),
|
|
74
|
+
explain: must(stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EXPLAIN, 1)),
|
|
75
|
+
est: must(stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EST, 1))
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
return loops.sort((a, b) => a.selectId - b.selectId);
|
|
@@ -8,7 +8,7 @@ import { timeSampled } from "../../otel/src/maybe-time.js";
|
|
|
8
8
|
import { compile, format, sql } from "./internal/sql.js";
|
|
9
9
|
import { StatementCache } from "./internal/statement-cache.js";
|
|
10
10
|
import { buildSelectQuery, toSQLiteType } from "./query-builder.js";
|
|
11
|
-
import
|
|
11
|
+
import SQLite3Database from "@rocicorp/zero-sqlite3";
|
|
12
12
|
//#region ../zqlite/src/table-source.ts
|
|
13
13
|
var eventCount = 0;
|
|
14
14
|
/**
|
|
@@ -154,10 +154,10 @@ var TableSource = class {
|
|
|
154
154
|
let totalNvisit = 0;
|
|
155
155
|
const planLines = [];
|
|
156
156
|
for (let i = 0;; i++) {
|
|
157
|
-
const nvisit = cachedStatement.statement.scanStatus(i,
|
|
157
|
+
const nvisit = cachedStatement.statement.scanStatus(i, SQLite3Database.SQLITE_SCANSTAT_NVISIT, 1);
|
|
158
158
|
if (nvisit === void 0) break;
|
|
159
159
|
totalNvisit += Number(nvisit);
|
|
160
|
-
const explain = cachedStatement.statement.scanStatus(i,
|
|
160
|
+
const explain = cachedStatement.statement.scanStatus(i, SQLite3Database.SQLITE_SCANSTAT_EXPLAIN, 1);
|
|
161
161
|
if (typeof explain === "string" && explain.length > 0) planLines.push(explain);
|
|
162
162
|
}
|
|
163
163
|
if (totalNvisit !== 0) debug.recordNVisit(this.#table, sqlAndBindings.text, totalNvisit);
|