@rocicorp/zero 1.12.0-canary.8 → 1.12.0-canary.9
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/btree/node.d.ts +0 -2
- package/out/replicache/src/btree/node.d.ts.map +1 -1
- package/out/replicache/src/btree/node.js.map +1 -1
- package/out/replicache/src/connection-loop.d.ts.map +1 -1
- package/out/replicache/src/connection-loop.js +1 -1
- package/out/replicache/src/connection-loop.js.map +1 -1
- package/out/replicache/src/db/commit.d.ts +0 -1
- package/out/replicache/src/db/commit.d.ts.map +1 -1
- package/out/replicache/src/db/commit.js +1 -1
- package/out/replicache/src/db/commit.js.map +1 -1
- package/out/replicache/src/db/write.d.ts +0 -2
- package/out/replicache/src/db/write.d.ts.map +1 -1
- package/out/replicache/src/db/write.js +1 -1
- package/out/replicache/src/db/write.js.map +1 -1
- package/out/replicache/src/deleted-clients.d.ts +0 -2
- package/out/replicache/src/deleted-clients.d.ts.map +1 -1
- package/out/replicache/src/deleted-clients.js +1 -1
- package/out/replicache/src/deleted-clients.js.map +1 -1
- package/out/replicache/src/frozen-json.d.ts +0 -1
- package/out/replicache/src/frozen-json.d.ts.map +1 -1
- package/out/replicache/src/frozen-json.js.map +1 -1
- package/out/replicache/src/index-defs.d.ts +0 -2
- package/out/replicache/src/index-defs.d.ts.map +1 -1
- package/out/replicache/src/index-defs.js +1 -1
- package/out/replicache/src/index-defs.js.map +1 -1
- package/out/replicache/src/kv/expo-sqlite/store.d.ts +0 -1
- package/out/replicache/src/kv/expo-sqlite/store.d.ts.map +1 -1
- package/out/replicache/src/kv/expo-sqlite/store.js +1 -1
- package/out/replicache/src/kv/expo-sqlite/store.js.map +1 -1
- package/out/replicache/src/kv/idb-store.js +1 -1
- package/out/replicache/src/kv/idb-store.js.map +1 -1
- package/out/replicache/src/kv/throw-if-closed.d.ts +0 -3
- package/out/replicache/src/kv/throw-if-closed.d.ts.map +1 -1
- package/out/replicache/src/kv/throw-if-closed.js.map +1 -1
- package/out/replicache/src/persist/client-group-gc.d.ts +0 -7
- package/out/replicache/src/persist/client-group-gc.d.ts.map +1 -1
- package/out/replicache/src/persist/client-group-gc.js +1 -1
- package/out/replicache/src/persist/client-group-gc.js.map +1 -1
- package/out/replicache/src/process-scheduler.js +1 -1
- package/out/replicache/src/process-scheduler.js.map +1 -1
- package/out/replicache/src/replicache-impl.d.ts +2 -7
- package/out/replicache/src/replicache-impl.d.ts.map +1 -1
- package/out/replicache/src/replicache-impl.js +1 -1
- package/out/replicache/src/replicache-impl.js.map +1 -1
- package/out/replicache/src/sync/patch.d.ts +0 -5
- package/out/replicache/src/sync/patch.d.ts.map +1 -1
- package/out/replicache/src/sync/patch.js +1 -1
- package/out/replicache/src/sync/patch.js.map +1 -1
- package/out/replicache/src/sync/pull.d.ts +0 -1
- package/out/replicache/src/sync/pull.d.ts.map +1 -1
- package/out/replicache/src/sync/pull.js.map +1 -1
- package/out/replicache/src/sync/push.d.ts +2 -1
- package/out/replicache/src/sync/push.d.ts.map +1 -1
- package/out/replicache/src/sync/push.js.map +1 -1
- package/out/replicache/src/types.d.ts +2 -1
- package/out/replicache/src/types.d.ts.map +1 -1
- package/out/shared/src/asserts.d.ts +0 -5
- package/out/shared/src/asserts.d.ts.map +1 -1
- package/out/shared/src/asserts.js +1 -1
- package/out/shared/src/asserts.js.map +1 -1
- package/out/shared/src/browser-env.d.ts +0 -17
- package/out/shared/src/browser-env.d.ts.map +1 -1
- package/out/shared/src/browser-env.js.map +1 -1
- package/out/shared/src/deep-clone.d.ts +0 -1
- package/out/shared/src/deep-clone.d.ts.map +1 -1
- package/out/shared/src/deep-clone.js +1 -1
- package/out/shared/src/deep-clone.js.map +1 -1
- package/out/shared/src/document-visible.js +1 -1
- package/out/shared/src/document-visible.js.map +1 -1
- package/out/shared/src/expand.d.ts +0 -3
- package/out/shared/src/expand.d.ts.map +1 -1
- package/out/shared/src/immutable.d.ts +1 -1
- package/out/shared/src/immutable.d.ts.map +1 -1
- package/out/shared/src/iterables.d.ts +0 -1
- package/out/shared/src/iterables.d.ts.map +1 -1
- package/out/shared/src/iterables.js.map +1 -1
- package/out/shared/src/logging-test-utils.js +1 -1
- package/out/shared/src/logging-test-utils.js.map +1 -1
- package/out/shared/src/options-types.d.ts +1 -1
- package/out/shared/src/options-types.d.ts.map +1 -1
- package/out/shared/src/options.d.ts +2 -3
- package/out/shared/src/options.d.ts.map +1 -1
- package/out/shared/src/options.js +1 -1
- package/out/shared/src/options.js.map +1 -1
- package/out/shared/src/promise-race.js +1 -1
- package/out/shared/src/promise-race.js.map +1 -1
- package/out/shared/src/queue.js +1 -1
- package/out/shared/src/queue.js.map +1 -1
- package/out/shared/src/resolved-promises.d.ts +0 -4
- package/out/shared/src/resolved-promises.d.ts.map +1 -1
- package/out/shared/src/resolved-promises.js +0 -1
- package/out/shared/src/resolved-promises.js.map +1 -1
- package/out/shared/src/resolver.d.ts +9 -0
- package/out/shared/src/resolver.d.ts.map +1 -0
- package/out/shared/src/resolver.js +6 -0
- package/out/shared/src/resolver.js.map +1 -0
- package/out/shared/src/sleep.js +1 -1
- package/out/shared/src/sleep.js.map +1 -1
- package/out/shared/src/tdigest.d.ts +0 -1
- package/out/shared/src/tdigest.d.ts.map +1 -1
- package/out/shared/src/tdigest.js.map +1 -1
- package/out/z2s/src/sql.d.ts +1 -5
- package/out/z2s/src/sql.d.ts.map +1 -1
- package/out/z2s/src/sql.js.map +1 -1
- package/out/zero/package.js +2 -3
- package/out/zero/package.js.map +1 -1
- package/out/zero/src/zero-cache-dev.js +1 -1
- package/out/zero/src/zero-cache-dev.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +14 -11
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +23 -0
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/db/migration-lite.js.map +1 -1
- package/out/zero-cache/src/db/migration.js +1 -1
- package/out/zero-cache/src/db/migration.js.map +1 -1
- package/out/zero-cache/src/db/pg-copy-binary.js +1 -1
- package/out/zero-cache/src/db/pg-copy-binary.js.map +1 -1
- package/out/zero-cache/src/db/pg-type-parser.js.map +1 -1
- package/out/zero-cache/src/db/specs.d.ts +3 -4
- package/out/zero-cache/src/db/specs.d.ts.map +1 -1
- package/out/zero-cache/src/db/specs.js +5 -7
- package/out/zero-cache/src/db/specs.js.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.js +2 -2
- package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
- package/out/zero-cache/src/observability/events.js +1 -1
- package/out/zero-cache/src/observability/events.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +1 -1
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/main.js +1 -1
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.js +1 -1
- package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/backfill-manager.js +1 -1
- package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js +1 -1
- package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.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/initial-sync.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/lsn.d.ts +0 -2
- package/out/zero-cache/src/services/change-source/pg/lsn.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/lsn.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +0 -12
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +5 -3
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.d.ts +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.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/validation.d.ts +0 -1
- package/out/zero-cache/src/services/change-source/pg/schema/validation.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/validation.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-monitor.js +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.js +1 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +1 -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.js +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.js +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/drain-tracker.js +1 -1
- package/out/zero-cache/src/services/change-streamer/drain-tracker.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js +2 -2
- package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/restore-admission.js +2 -2
- package/out/zero-cache/src/services/change-streamer/restore-admission.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.js +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot-message.js +2 -3
- package/out/zero-cache/src/services/change-streamer/snapshot-message.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js +2 -2
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-read-router.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscribe.js +1 -1
- package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.js +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +1 -1
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +1 -1
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/litestream/metrics.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +2 -2
- package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
- package/out/zero-cache/src/services/plannerz.d.ts +4 -12
- package/out/zero-cache/src/services/plannerz.d.ts.map +1 -1
- package/out/zero-cache/src/services/plannerz.js +1 -3
- package/out/zero-cache/src/services/plannerz.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-log-db.js +1 -1
- package/out/zero-cache/src/services/replicator/change-log-db.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-log-stream-writer.js +1 -1
- package/out/zero-cache/src/services/replicator/change-log-stream-writer.js.map +1 -1
- package/out/zero-cache/src/services/replicator/drain-handler.js +1 -1
- package/out/zero-cache/src/services/replicator/drain-handler.js.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.js +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/report-schema.d.ts +2 -11
- package/out/zero-cache/src/services/replicator/reporter/report-schema.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/report-schema.js +1 -1
- package/out/zero-cache/src/services/replicator/reporter/report-schema.js.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/change-log.d.ts +0 -10
- package/out/zero-cache/src/services/replicator/schema/change-log.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/change-log.js.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/column-metadata.d.ts +0 -7
- package/out/zero-cache/src/services/replicator/schema/column-metadata.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/column-metadata.js +1 -1
- package/out/zero-cache/src/services/replicator/schema/column-metadata.js.map +1 -1
- package/out/zero-cache/src/services/replicator/write-worker-client.js +1 -1
- package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
- package/out/zero-cache/src/services/running-state.js +1 -1
- package/out/zero-cache/src/services/running-state.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/drain-coordinator.js +1 -1
- package/out/zero-cache/src/services/view-syncer/drain-coordinator.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/query-stats.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/cvr.js +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/types.js +3 -17
- package/out/zero-cache/src/services/view-syncer/schema/types.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +10 -10
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/lite.d.ts +0 -1
- package/out/zero-cache/src/types/lite.d.ts.map +1 -1
- package/out/zero-cache/src/types/lite.js +2 -2
- package/out/zero-cache/src/types/lite.js.map +1 -1
- package/out/zero-cache/src/types/pg-types.d.ts +0 -40
- package/out/zero-cache/src/types/pg-types.d.ts.map +1 -1
- package/out/zero-cache/src/types/pg-types.js.map +1 -1
- package/out/zero-cache/src/types/poke-chunk.js +1 -1
- package/out/zero-cache/src/types/poke-chunk.js.map +1 -1
- package/out/zero-cache/src/types/row-key.d.ts +0 -5
- package/out/zero-cache/src/types/row-key.d.ts.map +1 -1
- package/out/zero-cache/src/types/row-key.js.map +1 -1
- package/out/zero-cache/src/types/streams.js +1 -1
- package/out/zero-cache/src/types/streams.js.map +1 -1
- package/out/zero-cache/src/types/subscription.js +1 -1
- package/out/zero-cache/src/types/subscription.js.map +1 -1
- package/out/zero-cache/src/types/websocket-handoff.d.ts +0 -6
- package/out/zero-cache/src/types/websocket-handoff.d.ts.map +1 -1
- package/out/zero-cache/src/types/websocket-handoff.js +8 -2
- package/out/zero-cache/src/types/websocket-handoff.js.map +1 -1
- package/out/zero-cache/src/types/ws.d.ts +2 -1
- package/out/zero-cache/src/types/ws.d.ts.map +1 -1
- package/out/zero-cache/src/types/ws.js +1 -1
- package/out/zero-cache/src/types/ws.js.map +1 -1
- package/out/zero-cache/src/workers/connection.js +4 -1
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/mutator.js +1 -1
- package/out/zero-cache/src/workers/mutator.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +36 -2
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/active-clients-manager.d.ts.map +1 -1
- package/out/zero-client/src/client/active-clients-manager.js +1 -1
- package/out/zero-client/src/client/active-clients-manager.js.map +1 -1
- package/out/zero-client/src/client/connection-manager.js +1 -1
- package/out/zero-client/src/client/connection-manager.js.map +1 -1
- package/out/zero-client/src/client/crud.d.ts +2 -8
- package/out/zero-client/src/client/crud.d.ts.map +1 -1
- package/out/zero-client/src/client/crud.js +1 -1
- package/out/zero-client/src/client/crud.js.map +1 -1
- package/out/zero-client/src/client/custom.d.ts +0 -2
- package/out/zero-client/src/client/custom.d.ts.map +1 -1
- package/out/zero-client/src/client/custom.js +1 -1
- package/out/zero-client/src/client/custom.js.map +1 -1
- package/out/zero-client/src/client/enable-analytics.d.ts +0 -1
- package/out/zero-client/src/client/enable-analytics.d.ts.map +1 -1
- package/out/zero-client/src/client/enable-analytics.js +2 -1
- package/out/zero-client/src/client/enable-analytics.js.map +1 -1
- package/out/zero-client/src/client/error.d.ts +4 -5
- package/out/zero-client/src/client/error.d.ts.map +1 -1
- package/out/zero-client/src/client/error.js.map +1 -1
- package/out/zero-client/src/client/keys.d.ts +0 -1
- package/out/zero-client/src/client/keys.d.ts.map +1 -1
- package/out/zero-client/src/client/keys.js +4 -2
- package/out/zero-client/src/client/keys.js.map +1 -1
- package/out/zero-client/src/client/mutation-tracker.d.ts.map +1 -1
- package/out/zero-client/src/client/mutation-tracker.js +1 -1
- package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
- package/out/zero-client/src/client/options.d.ts +0 -4
- package/out/zero-client/src/client/options.d.ts.map +1 -1
- package/out/zero-client/src/client/options.js.map +1 -1
- package/out/zero-client/src/client/replicache-types.d.ts +3 -2
- package/out/zero-client/src/client/replicache-types.d.ts.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero-poke-handler.d.ts +1 -2
- package/out/zero-client/src/client/zero-poke-handler.d.ts.map +1 -1
- package/out/zero-client/src/client/zero-poke-handler.js +1 -1
- package/out/zero-client/src/client/zero-poke-handler.js.map +1 -1
- package/out/zero-client/src/client/zero.d.ts +1 -5
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +1 -1
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-client/src/types/client-state.d.ts +0 -6
- package/out/zero-client/src/types/client-state.d.ts.map +1 -1
- package/out/zero-permissions/src/permissions.d.ts +1 -12
- package/out/zero-permissions/src/permissions.d.ts.map +1 -1
- package/out/zero-permissions/src/permissions.js +1 -2
- package/out/zero-permissions/src/permissions.js.map +1 -1
- package/out/zero-protocol/src/ast.d.ts +0 -4
- package/out/zero-protocol/src/ast.d.ts.map +1 -1
- package/out/zero-protocol/src/ast.js +1 -2
- package/out/zero-protocol/src/ast.js.map +1 -1
- package/out/zero-protocol/src/change-desired-queries.d.ts +0 -20
- package/out/zero-protocol/src/change-desired-queries.d.ts.map +1 -1
- package/out/zero-protocol/src/change-desired-queries.js.map +1 -1
- package/out/zero-protocol/src/client-schema.d.ts +2 -5
- package/out/zero-protocol/src/client-schema.d.ts.map +1 -1
- package/out/zero-protocol/src/client-schema.js +1 -1
- package/out/zero-protocol/src/client-schema.js.map +1 -1
- package/out/zero-protocol/src/close-connection.d.ts +0 -8
- package/out/zero-protocol/src/close-connection.d.ts.map +1 -1
- package/out/zero-protocol/src/close-connection.js +1 -1
- package/out/zero-protocol/src/close-connection.js.map +1 -1
- package/out/zero-protocol/src/connect.d.ts +0 -12
- package/out/zero-protocol/src/connect.d.ts.map +1 -1
- package/out/zero-protocol/src/connect.js +1 -1
- package/out/zero-protocol/src/connect.js.map +1 -1
- package/out/zero-protocol/src/custom-queries.d.ts +3 -17
- package/out/zero-protocol/src/custom-queries.d.ts.map +1 -1
- package/out/zero-protocol/src/custom-queries.js +1 -1
- package/out/zero-protocol/src/custom-queries.js.map +1 -1
- package/out/zero-protocol/src/data.d.ts +0 -1
- package/out/zero-protocol/src/data.d.ts.map +1 -1
- package/out/zero-protocol/src/data.js +1 -1
- package/out/zero-protocol/src/data.js.map +1 -1
- package/out/zero-protocol/src/down.d.ts +27 -27
- package/out/zero-protocol/src/inspect-down.d.ts +82 -84
- package/out/zero-protocol/src/inspect-down.d.ts.map +1 -1
- package/out/zero-protocol/src/inspect-down.js +1 -1
- package/out/zero-protocol/src/inspect-down.js.map +1 -1
- package/out/zero-protocol/src/inspect-up.d.ts +0 -28
- package/out/zero-protocol/src/inspect-up.d.ts.map +1 -1
- package/out/zero-protocol/src/inspect-up.js +1 -1
- package/out/zero-protocol/src/inspect-up.js.map +1 -1
- package/out/zero-protocol/src/mutate-server.d.ts +0 -21
- package/out/zero-protocol/src/mutate-server.d.ts.map +1 -1
- package/out/zero-protocol/src/mutate-server.js +1 -1
- package/out/zero-protocol/src/mutate-server.js.map +1 -1
- package/out/zero-protocol/src/mutation-type-enum.d.ts +0 -1
- package/out/zero-protocol/src/mutation-type-enum.d.ts.map +1 -1
- package/out/zero-protocol/src/mutation-type-enum.js.map +1 -1
- package/out/zero-protocol/src/mutation.d.ts +2 -4
- package/out/zero-protocol/src/mutation.d.ts.map +1 -1
- package/out/zero-protocol/src/mutation.js +1 -1
- package/out/zero-protocol/src/mutation.js.map +1 -1
- package/out/zero-protocol/src/mutations-patch.d.ts +0 -33
- package/out/zero-protocol/src/mutations-patch.d.ts.map +1 -1
- package/out/zero-protocol/src/mutations-patch.js +1 -1
- package/out/zero-protocol/src/mutations-patch.js.map +1 -1
- package/out/zero-protocol/src/ping.d.ts +0 -2
- package/out/zero-protocol/src/ping.d.ts.map +1 -1
- package/out/zero-protocol/src/ping.js +1 -1
- package/out/zero-protocol/src/ping.js.map +1 -1
- package/out/zero-protocol/src/poke.d.ts +3 -2
- package/out/zero-protocol/src/poke.d.ts.map +1 -1
- package/out/zero-protocol/src/poke.js +1 -1
- package/out/zero-protocol/src/poke.js.map +1 -1
- package/out/zero-protocol/src/pong.d.ts +0 -2
- package/out/zero-protocol/src/pong.d.ts.map +1 -1
- package/out/zero-protocol/src/pong.js +1 -1
- package/out/zero-protocol/src/pong.js.map +1 -1
- package/out/zero-protocol/src/pull.d.ts +2 -7
- package/out/zero-protocol/src/pull.d.ts.map +1 -1
- package/out/zero-protocol/src/pull.js +1 -1
- package/out/zero-protocol/src/pull.js.map +1 -1
- package/out/zero-protocol/src/push.d.ts +1 -3
- package/out/zero-protocol/src/push.d.ts.map +1 -1
- package/out/zero-protocol/src/push.js +1 -1
- package/out/zero-protocol/src/push.js.map +1 -1
- package/out/zero-protocol/src/queries-patch.d.ts +0 -16
- package/out/zero-protocol/src/queries-patch.d.ts.map +1 -1
- package/out/zero-protocol/src/queries-patch.js +1 -1
- package/out/zero-protocol/src/queries-patch.js.map +1 -1
- package/out/zero-protocol/src/query-server.d.ts +2 -41
- package/out/zero-protocol/src/query-server.d.ts.map +1 -1
- package/out/zero-protocol/src/query-server.js +1 -1
- package/out/zero-protocol/src/query-server.js.map +1 -1
- package/out/zero-protocol/src/up.d.ts +42 -42
- package/out/zero-protocol/src/version.d.ts +0 -1
- package/out/zero-protocol/src/version.d.ts.map +1 -1
- package/out/zero-protocol/src/version.js.map +1 -1
- package/out/zero-react/src/use-query.js +1 -1
- package/out/zero-react/src/use-query.js.map +1 -1
- package/out/zero-schema/src/builder/relationship-builder.d.ts +0 -2
- package/out/zero-schema/src/builder/relationship-builder.d.ts.map +1 -1
- package/out/zero-schema/src/builder/relationship-builder.js.map +1 -1
- package/out/zero-schema/src/builder/table-builder.d.ts +1 -1
- package/out/zero-schema/src/builder/table-builder.d.ts.map +1 -1
- package/out/zero-schema/src/builder/table-builder.js +1 -1
- package/out/zero-schema/src/builder/table-builder.js.map +1 -1
- package/out/zero-schema/src/schema-config.d.ts +0 -1
- package/out/zero-schema/src/schema-config.d.ts.map +1 -1
- package/out/zero-schema/src/schema-config.js +1 -1
- package/out/zero-schema/src/schema-config.js.map +1 -1
- package/out/zero-schema/src/table-schema.d.ts +3 -11
- package/out/zero-schema/src/table-schema.d.ts.map +1 -1
- package/out/zero-server/src/queries/process-queries.d.ts +2 -4
- package/out/zero-server/src/queries/process-queries.d.ts.map +1 -1
- package/out/zero-server/src/queries/process-queries.js.map +1 -1
- package/out/zero-solid/src/solid-view.d.ts +0 -2
- package/out/zero-solid/src/solid-view.d.ts.map +1 -1
- package/out/zero-solid/src/solid-view.js +1 -1
- package/out/zero-solid/src/solid-view.js.map +1 -1
- package/out/zero-types/src/name-mapper.d.ts +2 -1
- package/out/zero-types/src/name-mapper.d.ts.map +1 -1
- package/out/zero-types/src/name-mapper.js.map +1 -1
- package/out/zero-types/src/schema-value.d.ts +3 -2
- package/out/zero-types/src/schema-value.d.ts.map +1 -1
- package/out/zero-types/src/schema.d.ts +1 -1
- package/out/zero-types/src/schema.d.ts.map +1 -1
- package/out/zql/src/builder/builder.d.ts +1 -11
- package/out/zql/src/builder/builder.d.ts.map +1 -1
- package/out/zql/src/builder/builder.js +1 -1
- package/out/zql/src/builder/builder.js.map +1 -1
- package/out/zql/src/builder/filter.d.ts +0 -1
- package/out/zql/src/builder/filter.d.ts.map +1 -1
- package/out/zql/src/builder/filter.js.map +1 -1
- package/out/zql/src/error.d.ts +0 -5
- package/out/zql/src/error.d.ts.map +1 -1
- package/out/zql/src/error.js.map +1 -1
- package/out/zql/src/ivm/constraint.d.ts +0 -7
- package/out/zql/src/ivm/constraint.d.ts.map +1 -1
- package/out/zql/src/ivm/constraint.js +1 -1
- package/out/zql/src/ivm/constraint.js.map +1 -1
- package/out/zql/src/ivm/data.d.ts +0 -1
- package/out/zql/src/ivm/data.d.ts.map +1 -1
- package/out/zql/src/ivm/data.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts +1 -2
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +1 -1
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/stream.d.ts +0 -1
- package/out/zql/src/ivm/stream.d.ts.map +1 -1
- package/out/zql/src/ivm/stream.js.map +1 -1
- package/out/zql/src/ivm/union-fan-in.d.ts +0 -1
- package/out/zql/src/ivm/union-fan-in.d.ts.map +1 -1
- package/out/zql/src/ivm/union-fan-in.js +1 -1
- package/out/zql/src/ivm/union-fan-in.js.map +1 -1
- package/out/zql/src/ivm/view-apply-change.d.ts +5 -35
- package/out/zql/src/ivm/view-apply-change.d.ts.map +1 -1
- package/out/zql/src/ivm/view-apply-change.js +1 -1
- package/out/zql/src/ivm/view-apply-change.js.map +1 -1
- package/out/zql/src/mutate/crud.d.ts +1 -3
- package/out/zql/src/mutate/crud.d.ts.map +1 -1
- package/out/zql/src/mutate/crud.js +1 -1
- package/out/zql/src/mutate/crud.js.map +1 -1
- package/out/zql/src/mutate/mutator-registry.d.ts +1 -1
- package/out/zql/src/mutate/mutator-registry.d.ts.map +1 -1
- package/out/zql/src/mutate/mutator-registry.js.map +1 -1
- package/out/zql/src/mutate/mutator.d.ts +1 -1
- package/out/zql/src/mutate/mutator.d.ts.map +1 -1
- package/out/zql/src/mutate/mutator.js.map +1 -1
- package/out/zql/src/planner/planner-debug.d.ts +10 -9
- package/out/zql/src/planner/planner-debug.d.ts.map +1 -1
- package/out/zql/src/planner/planner-debug.js.map +1 -1
- package/out/zql/src/planner/planner-node.d.ts +0 -1
- package/out/zql/src/planner/planner-node.d.ts.map +1 -1
- package/out/zql/src/planner/planner-node.js.map +1 -1
- package/out/zql/src/planner/planner-warnings.d.ts +3 -3
- package/out/zql/src/planner/planner-warnings.d.ts.map +1 -1
- package/out/zql/src/planner/planner-warnings.js.map +1 -1
- package/out/zql/src/query/expression.d.ts +0 -22
- package/out/zql/src/query/expression.d.ts.map +1 -1
- package/out/zql/src/query/expression.js +1 -1
- package/out/zql/src/query/expression.js.map +1 -1
- package/out/zql/src/query/query-delegate-base.d.ts +0 -7
- package/out/zql/src/query/query-delegate-base.d.ts.map +1 -1
- package/out/zql/src/query/query-delegate-base.js +2 -2
- package/out/zql/src/query/query-delegate-base.js.map +1 -1
- package/out/zql/src/query/query-delegate.d.ts +1 -4
- package/out/zql/src/query/query-delegate.d.ts.map +1 -1
- package/out/zql/src/query/query-internals.d.ts +0 -3
- package/out/zql/src/query/query-internals.d.ts.map +1 -1
- package/out/zql/src/query/query-internals.js.map +1 -1
- package/out/zql/src/query/query-registry.d.ts +5 -5
- package/out/zql/src/query/query-registry.d.ts.map +1 -1
- package/out/zql/src/query/query-registry.js.map +1 -1
- package/out/zql/src/query/query.d.ts +2 -13
- package/out/zql/src/query/query.d.ts.map +1 -1
- package/out/zql/src/query/runnable-query-impl.d.ts +1 -19
- package/out/zql/src/query/runnable-query-impl.d.ts.map +1 -1
- package/out/zql/src/query/runnable-query-impl.js +1 -1
- package/out/zql/src/query/runnable-query-impl.js.map +1 -1
- package/out/zql/src/query/ttl.d.ts +2 -3
- package/out/zql/src/query/ttl.d.ts.map +1 -1
- package/out/zql/src/query/ttl.js.map +1 -1
- package/out/zqlite/src/query-builder.d.ts +0 -2
- package/out/zqlite/src/query-builder.d.ts.map +1 -1
- package/out/zqlite/src/query-builder.js +1 -1
- package/out/zqlite/src/query-builder.js.map +1 -1
- package/out/zqlite/src/table-source.d.ts +0 -1
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +1 -1
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +2 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-log-stream-writer.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/change-log-stream-writer.ts"],"sourcesContent":["import type {Statement} from '../../../../zqlite/src/db.ts';\nimport type {StatementRunner} from '../../db/statements.ts';\nimport {\n isSchemaChange,\n type DataOrSchemaChange,\n} from '../change-source/protocol/current/data.ts';\nimport {extractChangeSubstring} from '../change-streamer/change-log-codec.ts';\nimport {ChangeLogTransactionHasher} from '../change-streamer/change-log-transaction-hash.ts';\nimport {ChangeLogCookieWriter, type CookieOpTag} from './change-log-cookies.ts';\nimport {\n CHANGE_LOG_STREAM_TABLE,\n estimateChangeLogStreamRowBytes,\n} from './change-log-db.ts';\n\nexport type ChangeLogStreamTransactionStats = {\n readonly rows: number;\n readonly estimatedBytes: number;\n /** Present only for transactions selected for shadow-write validation. */\n readonly hash?: string | undefined;\n /**\n * The cookie mutations this transaction's schema changes folded in, in stream\n * order. Empty for the overwhelming majority of transactions.\n */\n readonly cookieMutations: readonly CookieOpTag[];\n};\n\nexport class ChangeLogStreamInvariantError extends Error {\n override readonly name = 'ChangeLogStreamInvariantError';\n}\n\nfunction assertInvariant(\n condition: unknown,\n message: string,\n): asserts condition {\n if (!condition) {\n throw new ChangeLogStreamInvariantError(message);\n }\n}\n\n/**\n * Appends the canonical downstream stream to the change-log database, which is\n * a separate file from the replica, and folds its schema changes into the log's\n * cookie jar (`change-log-cookies.ts`) as it goes.\n *\n * This class owns the change log's transaction, and its transaction alone. It is\n * driven from the change-streamer's stream loop, which commits it before\n * forwarding a transaction's `commit` message; see `SQLiteChangeLogWriter` in\n * `change-streamer/sqlite-change-log-writer.ts` for the ordering that makes a\n * hole unreachable, and for the fail-soft policy that wraps every call here.\n *\n * Because the cookies are written in that same transaction, they are atomic with\n * the head and {@link rollback} discards them with the rows they were folded\n * from. Neither needs any handling of its own.\n */\nexport class ChangeLogStreamWriter {\n readonly #db: StatementRunner;\n readonly #insertChange: Statement;\n readonly #insertCommit: Statement;\n readonly #cookies: ChangeLogCookieWriter;\n\n #watermark: string | undefined;\n #pos = 0;\n #estimatedBytes = 0;\n #hasher: ChangeLogTransactionHasher | undefined;\n #cookieMutations: CookieOpTag[] = [];\n\n constructor(db: StatementRunner) {\n this.#db = db;\n this.#insertChange = db.db.prepare(/*sql*/ `\n INSERT INTO \"${CHANGE_LOG_STREAM_TABLE}\"\n (\"watermark\", \"pos\", \"tag\", \"estimatedBytes\", \"change\")\n VALUES (?, ?, ?, ?, ?)\n `);\n this.#insertCommit = db.db.prepare(/*sql*/ `\n INSERT INTO \"${CHANGE_LOG_STREAM_TABLE}\"\n (\"watermark\", \"pos\", \"tag\", \"estimatedBytes\", \"change\", \"precommit\",\n \"writeTimeMs\")\n VALUES (?, ?, 'commit', ?, ?, ?, ?)\n `);\n this.#cookies = new ChangeLogCookieWriter(db.db);\n }\n\n begin(\n watermark: string,\n json: string,\n storedChange = extractChangeSubstring(json, 'begin'),\n hash = true,\n ): void {\n assertInvariant(\n this.#watermark === undefined,\n `change-log stream transaction already open at ${this.#watermark}`,\n );\n // This process is the sole writer of the change-log database, so the write\n // lock is taken up front rather than risking a failed upgrade from a\n // deferred transaction partway through the append.\n this.#db.beginImmediate();\n this.#watermark = watermark;\n this.#pos = 0;\n const change = storedChange;\n const estimatedBytes = estimateChangeLogStreamRowBytes(\n watermark,\n 'begin',\n change,\n );\n this.#insertChange.run(\n watermark,\n this.#pos,\n 'begin',\n estimatedBytes,\n change,\n );\n if (hash) {\n this.#hasher = new ChangeLogTransactionHasher();\n this.#hasher.add({\n watermark,\n pos: this.#pos,\n tag: 'begin',\n change,\n precommit: null,\n });\n }\n this.#estimatedBytes = estimatedBytes;\n }\n\n /**\n * Appends one data or schema change. The parsed message is taken rather than\n * just its tag because a schema change also mutates the cookie jar, and the\n * caller already holds it — so the fold costs no re-parse.\n *\n * The cookie statements run here, i.e. inside this transaction and in stream\n * order relative to the rows they were folded from. The Postgres change log\n * has to flush its buffered batch to get the same ordering\n * (`Storer.#processQueue()`); SQLite gets it from statement order.\n */\n append(\n json: string,\n change: DataOrSchemaChange,\n storedChange = extractChangeSubstring(json, change.tag),\n ): void {\n const watermark = this.#requireWatermark();\n const {tag} = change;\n const stored = storedChange;\n const estimatedBytes = estimateChangeLogStreamRowBytes(\n watermark,\n tag,\n stored,\n );\n this.#insertChange.run(watermark, ++this.#pos, tag, estimatedBytes, stored);\n this.#hasher?.add({\n watermark,\n pos: this.#pos,\n tag,\n change: stored,\n precommit: null,\n });\n this.#estimatedBytes += estimatedBytes;\n\n if (isSchemaChange(change)) {\n for (const op of this.#cookies.apply(change)) {\n this.#cookieMutations.push(op.op);\n }\n }\n }\n\n commit(\n watermark: string,\n json: string,\n writeTimeMs: number,\n storedChange = extractChangeSubstring(json, 'commit'),\n ): ChangeLogStreamTransactionStats {\n const precommit = this.#requireWatermark();\n assertInvariant(\n watermark === precommit,\n `change-log stream commit ${watermark} does not match begin ${precommit}`,\n );\n const change = storedChange;\n const estimatedBytes = estimateChangeLogStreamRowBytes(\n watermark,\n 'commit',\n change,\n precommit,\n true,\n );\n this.#insertCommit.run(\n watermark,\n ++this.#pos,\n estimatedBytes,\n change,\n precommit,\n writeTimeMs,\n );\n const hasher = this.#hasher;\n hasher?.add({\n watermark,\n pos: this.#pos,\n tag: 'commit',\n change,\n precommit,\n });\n const stats = {\n rows: this.#pos + 1,\n estimatedBytes: this.#estimatedBytes + estimatedBytes,\n hash: hasher?.digest(),\n cookieMutations: this.#cookieMutations,\n };\n // The log is durable at `watermark` from here on, and nothing that can\n // advance the watermark the stream would resume from has run yet. A crash in\n // that window leaves a transaction the resumed stream re-delivers, which\n // reconciliation truncates.\n this.#db.commit();\n this.#reset();\n return stats;\n }\n\n /**\n * Discards the open transaction, if any. Safe to call when none is open — the\n * caller rolls back on an interrupted change stream, which can arrive between\n * transactions as well as inside one.\n */\n rollback(): void {\n const {inTransaction} = this.#db.db;\n this.#reset();\n if (inTransaction) {\n this.#db.rollback();\n }\n }\n\n #reset(): void {\n this.#watermark = undefined;\n this.#pos = 0;\n this.#estimatedBytes = 0;\n this.#hasher = undefined;\n // A fresh array rather than a truncation: the committed stats hold a\n // reference to the previous one.\n this.#cookieMutations = [];\n }\n\n #requireWatermark(): string {\n assertInvariant(\n this.#watermark !== undefined,\n 'change-log stream message received outside of a transaction',\n );\n return this.#watermark;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"change-log-stream-writer.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/change-log-stream-writer.ts"],"sourcesContent":["import type {Statement} from '../../../../zqlite/src/db.ts';\nimport type {StatementRunner} from '../../db/statements.ts';\nimport {\n isSchemaChange,\n type DataOrSchemaChange,\n} from '../change-source/protocol/current/data.ts';\nimport {extractChangeSubstring} from '../change-streamer/change-log-codec.ts';\nimport {ChangeLogTransactionHasher} from '../change-streamer/change-log-transaction-hash.ts';\nimport {ChangeLogCookieWriter, type CookieOpTag} from './change-log-cookies.ts';\nimport {\n CHANGE_LOG_STREAM_TABLE,\n estimateChangeLogStreamRowBytes,\n} from './change-log-db.ts';\n\nexport type ChangeLogStreamTransactionStats = {\n readonly rows: number;\n readonly estimatedBytes: number;\n /** Present only for transactions selected for shadow-write validation. */\n readonly hash?: string | undefined;\n /**\n * The cookie mutations this transaction's schema changes folded in, in stream\n * order. Empty for the overwhelming majority of transactions.\n */\n readonly cookieMutations: readonly CookieOpTag[];\n};\n\nclass ChangeLogStreamInvariantError extends Error {\n override readonly name = 'ChangeLogStreamInvariantError';\n}\n\nfunction assertInvariant(\n condition: unknown,\n message: string,\n): asserts condition {\n if (!condition) {\n throw new ChangeLogStreamInvariantError(message);\n }\n}\n\n/**\n * Appends the canonical downstream stream to the change-log database, which is\n * a separate file from the replica, and folds its schema changes into the log's\n * cookie jar (`change-log-cookies.ts`) as it goes.\n *\n * This class owns the change log's transaction, and its transaction alone. It is\n * driven from the change-streamer's stream loop, which commits it before\n * forwarding a transaction's `commit` message; see `SQLiteChangeLogWriter` in\n * `change-streamer/sqlite-change-log-writer.ts` for the ordering that makes a\n * hole unreachable, and for the fail-soft policy that wraps every call here.\n *\n * Because the cookies are written in that same transaction, they are atomic with\n * the head and {@link rollback} discards them with the rows they were folded\n * from. Neither needs any handling of its own.\n */\nexport class ChangeLogStreamWriter {\n readonly #db: StatementRunner;\n readonly #insertChange: Statement;\n readonly #insertCommit: Statement;\n readonly #cookies: ChangeLogCookieWriter;\n\n #watermark: string | undefined;\n #pos = 0;\n #estimatedBytes = 0;\n #hasher: ChangeLogTransactionHasher | undefined;\n #cookieMutations: CookieOpTag[] = [];\n\n constructor(db: StatementRunner) {\n this.#db = db;\n this.#insertChange = db.db.prepare(/*sql*/ `\n INSERT INTO \"${CHANGE_LOG_STREAM_TABLE}\"\n (\"watermark\", \"pos\", \"tag\", \"estimatedBytes\", \"change\")\n VALUES (?, ?, ?, ?, ?)\n `);\n this.#insertCommit = db.db.prepare(/*sql*/ `\n INSERT INTO \"${CHANGE_LOG_STREAM_TABLE}\"\n (\"watermark\", \"pos\", \"tag\", \"estimatedBytes\", \"change\", \"precommit\",\n \"writeTimeMs\")\n VALUES (?, ?, 'commit', ?, ?, ?, ?)\n `);\n this.#cookies = new ChangeLogCookieWriter(db.db);\n }\n\n begin(\n watermark: string,\n json: string,\n storedChange = extractChangeSubstring(json, 'begin'),\n hash = true,\n ): void {\n assertInvariant(\n this.#watermark === undefined,\n `change-log stream transaction already open at ${this.#watermark}`,\n );\n // This process is the sole writer of the change-log database, so the write\n // lock is taken up front rather than risking a failed upgrade from a\n // deferred transaction partway through the append.\n this.#db.beginImmediate();\n this.#watermark = watermark;\n this.#pos = 0;\n const change = storedChange;\n const estimatedBytes = estimateChangeLogStreamRowBytes(\n watermark,\n 'begin',\n change,\n );\n this.#insertChange.run(\n watermark,\n this.#pos,\n 'begin',\n estimatedBytes,\n change,\n );\n if (hash) {\n this.#hasher = new ChangeLogTransactionHasher();\n this.#hasher.add({\n watermark,\n pos: this.#pos,\n tag: 'begin',\n change,\n precommit: null,\n });\n }\n this.#estimatedBytes = estimatedBytes;\n }\n\n /**\n * Appends one data or schema change. The parsed message is taken rather than\n * just its tag because a schema change also mutates the cookie jar, and the\n * caller already holds it — so the fold costs no re-parse.\n *\n * The cookie statements run here, i.e. inside this transaction and in stream\n * order relative to the rows they were folded from. The Postgres change log\n * has to flush its buffered batch to get the same ordering\n * (`Storer.#processQueue()`); SQLite gets it from statement order.\n */\n append(\n json: string,\n change: DataOrSchemaChange,\n storedChange = extractChangeSubstring(json, change.tag),\n ): void {\n const watermark = this.#requireWatermark();\n const {tag} = change;\n const stored = storedChange;\n const estimatedBytes = estimateChangeLogStreamRowBytes(\n watermark,\n tag,\n stored,\n );\n this.#insertChange.run(watermark, ++this.#pos, tag, estimatedBytes, stored);\n this.#hasher?.add({\n watermark,\n pos: this.#pos,\n tag,\n change: stored,\n precommit: null,\n });\n this.#estimatedBytes += estimatedBytes;\n\n if (isSchemaChange(change)) {\n for (const op of this.#cookies.apply(change)) {\n this.#cookieMutations.push(op.op);\n }\n }\n }\n\n commit(\n watermark: string,\n json: string,\n writeTimeMs: number,\n storedChange = extractChangeSubstring(json, 'commit'),\n ): ChangeLogStreamTransactionStats {\n const precommit = this.#requireWatermark();\n assertInvariant(\n watermark === precommit,\n `change-log stream commit ${watermark} does not match begin ${precommit}`,\n );\n const change = storedChange;\n const estimatedBytes = estimateChangeLogStreamRowBytes(\n watermark,\n 'commit',\n change,\n precommit,\n true,\n );\n this.#insertCommit.run(\n watermark,\n ++this.#pos,\n estimatedBytes,\n change,\n precommit,\n writeTimeMs,\n );\n const hasher = this.#hasher;\n hasher?.add({\n watermark,\n pos: this.#pos,\n tag: 'commit',\n change,\n precommit,\n });\n const stats = {\n rows: this.#pos + 1,\n estimatedBytes: this.#estimatedBytes + estimatedBytes,\n hash: hasher?.digest(),\n cookieMutations: this.#cookieMutations,\n };\n // The log is durable at `watermark` from here on, and nothing that can\n // advance the watermark the stream would resume from has run yet. A crash in\n // that window leaves a transaction the resumed stream re-delivers, which\n // reconciliation truncates.\n this.#db.commit();\n this.#reset();\n return stats;\n }\n\n /**\n * Discards the open transaction, if any. Safe to call when none is open — the\n * caller rolls back on an interrupted change stream, which can arrive between\n * transactions as well as inside one.\n */\n rollback(): void {\n const {inTransaction} = this.#db.db;\n this.#reset();\n if (inTransaction) {\n this.#db.rollback();\n }\n }\n\n #reset(): void {\n this.#watermark = undefined;\n this.#pos = 0;\n this.#estimatedBytes = 0;\n this.#hasher = undefined;\n // A fresh array rather than a truncation: the committed stats hold a\n // reference to the previous one.\n this.#cookieMutations = [];\n }\n\n #requireWatermark(): string {\n assertInvariant(\n this.#watermark !== undefined,\n 'change-log stream message received outside of a transaction',\n );\n return this.#watermark;\n }\n}\n"],"mappings":";;;;;;AA0BA,IAAM,gCAAN,cAA4C,MAAM;CAChD,OAAyB;AAC3B;AAEA,SAAS,gBACP,WACA,SACmB;CACnB,IAAI,CAAC,WACH,MAAM,IAAI,8BAA8B,OAAO;AAEnD;;;;;;;;;;;;;;;;AAiBA,IAAa,wBAAb,MAAmC;CACjC;CACA;CACA;CACA;CAEA;CACA,OAAO;CACP,kBAAkB;CAClB;CACA,mBAAkC,CAAC;CAEnC,YAAY,IAAqB;EAC/B,KAAK,MAAM;EACX,KAAK,gBAAgB,GAAG,GAAG,QAAgB;qBAC1B,wBAAwB;;;KAGxC;EACD,KAAK,gBAAgB,GAAG,GAAG,QAAgB;qBAC1B,wBAAwB;;;;KAIxC;EACD,KAAK,WAAW,IAAI,sBAAsB,GAAG,EAAE;CACjD;CAEA,MACE,WACA,MACA,eAAe,uBAAuB,MAAM,OAAO,GACnD,OAAO,MACD;EACN,gBACE,KAAK,eAAe,KAAA,GACpB,iDAAiD,KAAK,YACxD;EAIA,KAAK,IAAI,eAAe;EACxB,KAAK,aAAa;EAClB,KAAK,OAAO;EACZ,MAAM,SAAS;EACf,MAAM,iBAAiB,gCACrB,WACA,SACA,MACF;EACA,KAAK,cAAc,IACjB,WACA,KAAK,MACL,SACA,gBACA,MACF;EACA,IAAI,MAAM;GACR,KAAK,UAAU,IAAI,2BAA2B;GAC9C,KAAK,QAAQ,IAAI;IACf;IACA,KAAK,KAAK;IACV,KAAK;IACL;IACA,WAAW;GACb,CAAC;EACH;EACA,KAAK,kBAAkB;CACzB;;;;;;;;;;;CAYA,OACE,MACA,QACA,eAAe,uBAAuB,MAAM,OAAO,GAAG,GAChD;EACN,MAAM,YAAY,KAAK,kBAAkB;EACzC,MAAM,EAAC,QAAO;EACd,MAAM,SAAS;EACf,MAAM,iBAAiB,gCACrB,WACA,KACA,MACF;EACA,KAAK,cAAc,IAAI,WAAW,EAAE,KAAK,MAAM,KAAK,gBAAgB,MAAM;EAC1E,KAAK,SAAS,IAAI;GAChB;GACA,KAAK,KAAK;GACV;GACA,QAAQ;GACR,WAAW;EACb,CAAC;EACD,KAAK,mBAAmB;EAExB,IAAI,eAAe,MAAM,GACvB,KAAK,MAAM,MAAM,KAAK,SAAS,MAAM,MAAM,GACzC,KAAK,iBAAiB,KAAK,GAAG,EAAE;CAGtC;CAEA,OACE,WACA,MACA,aACA,eAAe,uBAAuB,MAAM,QAAQ,GACnB;EACjC,MAAM,YAAY,KAAK,kBAAkB;EACzC,gBACE,cAAc,WACd,4BAA4B,UAAU,wBAAwB,WAChE;EACA,MAAM,SAAS;EACf,MAAM,iBAAiB,gCACrB,WACA,UACA,QACA,WACA,IACF;EACA,KAAK,cAAc,IACjB,WACA,EAAE,KAAK,MACP,gBACA,QACA,WACA,WACF;EACA,MAAM,SAAS,KAAK;EACpB,QAAQ,IAAI;GACV;GACA,KAAK,KAAK;GACV,KAAK;GACL;GACA;EACF,CAAC;EACD,MAAM,QAAQ;GACZ,MAAM,KAAK,OAAO;GAClB,gBAAgB,KAAK,kBAAkB;GACvC,MAAM,QAAQ,OAAO;GACrB,iBAAiB,KAAK;EACxB;EAKA,KAAK,IAAI,OAAO;EAChB,KAAK,OAAO;EACZ,OAAO;CACT;;;;;;CAOA,WAAiB;EACf,MAAM,EAAC,kBAAiB,KAAK,IAAI;EACjC,KAAK,OAAO;EACZ,IAAI,eACF,KAAK,IAAI,SAAS;CAEtB;CAEA,SAAe;EACb,KAAK,aAAa,KAAA;EAClB,KAAK,OAAO;EACZ,KAAK,kBAAkB;EACvB,KAAK,UAAU,KAAA;EAGf,KAAK,mBAAmB,CAAC;CAC3B;CAEA,oBAA4B;EAC1B,gBACE,KAAK,eAAe,KAAA,GACpB,6DACF;EACA,OAAO,KAAK;CACd;AACF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { resolver } from "../../../../shared/src/resolver.js";
|
|
1
2
|
import { readHead } from "../../types/streams.js";
|
|
2
|
-
import { resolver } from "@rocicorp/resolver";
|
|
3
3
|
//#region ../zero-cache/src/services/replicator/drain-handler.ts
|
|
4
4
|
var DEFAULT_SWITCHOVER_TIMEOUT_MS = 1e4;
|
|
5
5
|
var MIN_SWITCHOVER_RETRY_DELAY_MS = 1e3;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drain-handler.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/drain-handler.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {readHead, type Sized, type Source} from '../../types/streams.ts';\nimport type {\n ChangeStreamer,\n Status,\n SubscriberContext,\n} from '../change-streamer/change-streamer.ts';\nimport type {SubscribeDownstream} from '../change-streamer/subscribe.ts';\nimport type {RunningState} from '../running-state.ts';\n\n// Bounds the time to wait for a switchover subscription to be established\n// and to receive its first message.\nconst DEFAULT_SWITCHOVER_TIMEOUT_MS = 10_000;\nconst MIN_SWITCHOVER_RETRY_DELAY_MS = 1_000;\nconst MAX_SWITCHOVER_RETRY_DELAY_MS = 30_000;\n\n/**\n * Bounds the time to search for another replication-manager to switch over\n * to, after which the current subscription is closed so that the draining\n * replication-manager can shut down (e.g. when it is the only one). The time\n * during which a subscription is held by another replication-manager (i.e.\n * waiting for permission to restore) does not count towards it.\n *\n * This is well below the default termination grace period of container\n * runners (30 seconds in Kubernetes and ECS), so that the draining\n * replication-manager can shut down cleanly when there is no other.\n */\nconst DEFAULT_SWITCHOVER_SEARCH_TIMEOUT_MS = 20_000;\n\nexport type SwitchoverOptions = {\n /**\n * Called when a replication-manager with a newer replica version grants\n * permission to restore its replica. This should initiate a graceful\n * restart of the server (during which the current subscription continues\n * to be served).\n */\n onRestoreGranted: () => void;\n\n /**\n * The replica file, from which the state of the last committed\n * transaction is read for switchover subscriptions.\n */\n replicaFile: string;\n\n /** For testing. */\n timeoutMs?: number | undefined;\n\n /** For testing. */\n searchTimeoutMs?: number | undefined;\n};\n\nexport type Downstream = Source<Sized<SubscribeDownstream>>;\n\ntype AttemptOutcome =\n | 'switched'\n | 'restoring'\n // The subscription was held for restore, but closed before a grant.\n | 'released'\n | 'retry';\n\n/**\n * Handles the switchover of a serving replica's subscription from a draining\n * replication-manager to another one, without interrupting the current\n * subscription.\n *\n * Upon {@link onChangeStreamerDrain}, the handler repeatedly (with backoff)\n * opens a new `switchover` subscription, at the watermark of the replica's\n * last committed transaction, to whichever replication-manager the\n * connection is routed to. The first message on the new subscription\n * determines the outcome:\n * * `status`: The subscription was accepted (i.e. the replica is compatible).\n * The current subscription is cancelled (aborting any transaction in\n * progress), and the new one is made available via\n * {@link getNextConnection}. Note that the new subscription may start with\n * transactions that were committed from the current subscription in the\n * meantime, which the consumer must skip.\n * * `status` with `restore: queued`: The replication-manager has a newer\n * replica version, and is holding the subscription until it grants\n * permission to restore. The held subscription is monitored for the grant.\n * * `status` with `restore: granted`: Permission to restore is granted, and\n * {@link SwitchoverOptions.onRestoreGranted} is called. The granted\n * subscription is kept open until the process exits, so that the\n * replication-manager can track the grant until the restore begins.\n * * Anything else (e.g. an error, or a `draining` replication-manager):\n * The switchover is retried later.\n *\n * If no other replication-manager accepts or holds a switchover subscription\n * within {@link DEFAULT_SWITCHOVER_SEARCH_TIMEOUT_MS} (e.g. because the\n * draining one is the only one), the current subscription is closed so that\n * the draining replication-manager can shut down, and the consumer reconnects\n * on its own.\n *\n * The current subscription continues to be consumed throughout. A\n * switchover is scoped to the subscription from which it switches: If that\n * subscription ends first (e.g. the replication-manager shuts down), the\n * switchover is abandoned, including any held (but not granted)\n * subscription, as the consumer reconnects on its own.\n */\nexport class DrainHandler {\n readonly #lc: LogContext;\n readonly #changeStreamer: ChangeStreamer;\n readonly #state: RunningState;\n readonly #options: SwitchoverOptions;\n readonly #committedContext: () => SubscriberContext;\n\n // The subscription currently being switched from.\n #draining: Downstream | undefined;\n #restoreGranted = false;\n #next: Downstream | undefined;\n\n /**\n * @param committedContext Returns the subscriber context as of the\n * replica's last committed transaction.\n */\n constructor(\n lc: LogContext,\n changeStreamer: ChangeStreamer,\n state: RunningState,\n options: SwitchoverOptions,\n committedContext: () => SubscriberContext,\n ) {\n this.#lc = lc.withContext('component', 'drain-handler');\n this.#changeStreamer = changeStreamer;\n this.#state = state;\n this.#options = options;\n this.#committedContext = committedContext;\n }\n\n /**\n * Called when the change-streamer of the `current` subscription is\n * draining.\n */\n onChangeStreamerDrain(current: Downstream) {\n if (\n this.#restoreGranted ||\n this.#draining === current ||\n !this.#isActive(current)\n ) {\n return;\n }\n this.#draining = current;\n this.#lc.info?.(\n `replication-manager is draining. switching over to another`,\n );\n void this.#switchover(current);\n }\n\n get hasNextConnection(): boolean {\n return this.#next !== undefined;\n }\n\n /**\n * Returns (and clears) the subscription that replaces the (now cancelled)\n * current one, if a switchover succeeded.\n */\n getNextConnection(): Downstream | undefined {\n const next = this.#next;\n this.#next = undefined;\n return next;\n }\n\n #isActive(current: Downstream) {\n return !current.signal.aborted && this.#state.shouldRun();\n }\n\n async #switchover(current: Downstream) {\n const searchTimeoutMs =\n this.#options.searchTimeoutMs ?? DEFAULT_SWITCHOVER_SEARCH_TIMEOUT_MS;\n let failures = 0;\n let searchStart = Date.now();\n while (this.#isActive(current)) {\n const outcome = await this.#attempt(current);\n if (outcome === 'switched' || outcome === 'restoring') {\n return;\n }\n if (outcome === 'released') {\n // Time spent held by another replication-manager does not count\n // towards the search.\n failures = 0;\n searchStart = Date.now();\n }\n const remaining = searchStart + searchTimeoutMs - Date.now();\n if (remaining > 0) {\n const delay = Math.min(\n MIN_SWITCHOVER_RETRY_DELAY_MS * 2 ** failures++,\n MAX_SWITCHOVER_RETRY_DELAY_MS,\n );\n await this.#state.sleep(Math.min(delay, remaining));\n continue;\n }\n // The search has timed out.\n if (this.#isActive(current)) {\n this.#lc.warn?.(\n `no other replication-manager found within ${searchTimeoutMs} ms. ` +\n `disconnecting from the draining replication-manager`,\n );\n current.cancel();\n }\n return;\n }\n }\n\n async #attempt(current: Downstream): Promise<AttemptOutcome> {\n const lc = this.#lc;\n const timeoutMs = this.#options.timeoutMs ?? DEFAULT_SWITCHOVER_TIMEOUT_MS;\n let candidate: Downstream | undefined;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n try {\n const ctx = this.#committedContext();\n lc.info?.(`attempting switchover at ${ctx.watermark}`);\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n timedOut = true;\n reject(new Error(`timed out after ${timeoutMs} ms`));\n }, timeoutMs);\n });\n const subscribing = this.#changeStreamer.subscribe({\n ...ctx,\n switchover: true,\n });\n // Clean up a connection that is established after a timeout.\n void subscribing.then(\n sub => (timedOut ? sub.cancel() : undefined),\n () => {},\n );\n candidate = await Promise.race([subscribing, timeout]);\n const {head, rest} = readHead(candidate);\n const first = await Promise.race([head.next(), timeout]);\n const msg = first.done ? undefined : first.value.data;\n\n if (!this.#isActive(current)) {\n // e.g. The current subscription failed in the meantime.\n } else if (msg?.[0] === 'status') {\n const {restore, draining} = msg[1];\n if (restore) {\n const granted = await this.#holdForRestore(current, rest, restore);\n if (granted) {\n return 'restoring';\n }\n lc.info?.(`held switchover subscription closed. will retry`);\n return 'released';\n }\n if (!draining) {\n lc.info?.(`switched over to a new subscription at ${ctx.watermark}`);\n this.#next = rest;\n current.cancel();\n return 'switched';\n }\n }\n lc.warn?.(`switchover rejected. continuing current subscription`, msg);\n } catch (e) {\n lc.warn?.(`switchover failed. continuing current subscription`, e);\n } finally {\n clearTimeout(timer);\n }\n candidate?.cancel();\n return 'retry';\n }\n\n /**\n * Keeps the held (or granted) subscription open, and initiates a graceful\n * restart when permission to restore is granted. A held subscription that\n * has yet to be granted is cancelled if the `current` subscription ends.\n *\n * @returns `true` once permission is granted, or `false` if the held\n * subscription closes before then.\n */\n #holdForRestore(\n current: Downstream,\n held: Downstream,\n restore: NonNullable<Status['restore']>,\n ): Promise<boolean> {\n const lc = this.#lc;\n const result = resolver<boolean>();\n const unregister = this.#state.cancelOnStop(held);\n const onCurrentEnded = () => {\n if (!this.#restoreGranted) {\n lc.info?.(`abandoning held switchover subscription`);\n held.cancel();\n }\n };\n current.signal.addEventListener('abort', onCurrentEnded, {once: true});\n\n const onRestore = (r: NonNullable<Status['restore']>) => {\n if (r.state === 'queued') {\n lc.info?.(\n `waiting for permission to restore a new replica (position ${r.position})`,\n );\n } else if (!this.#restoreGranted) {\n lc.info?.(\n `granted permission to restore a new replica. restarting gracefully`,\n );\n this.#restoreGranted = true;\n this.#options.onRestoreGranted();\n result.resolve(true);\n }\n };\n onRestore(restore);\n void (async () => {\n try {\n // Note: The loop must not be exited early, as that would close the\n // connection.\n for await (const {data: msg} of held) {\n if (msg[0] === 'status' && msg[1].restore) {\n onRestore(msg[1].restore);\n }\n }\n } catch (e) {\n lc.warn?.(`held switchover subscription failed`, e);\n } finally {\n unregister();\n current.signal.removeEventListener('abort', onCurrentEnded);\n result.resolve(false);\n }\n })();\n return result.promise;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"drain-handler.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/drain-handler.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '../../../../shared/src/resolver.ts';\nimport {readHead, type Sized, type Source} from '../../types/streams.ts';\nimport type {\n ChangeStreamer,\n Status,\n SubscriberContext,\n} from '../change-streamer/change-streamer.ts';\nimport type {SubscribeDownstream} from '../change-streamer/subscribe.ts';\nimport type {RunningState} from '../running-state.ts';\n\n// Bounds the time to wait for a switchover subscription to be established\n// and to receive its first message.\nconst DEFAULT_SWITCHOVER_TIMEOUT_MS = 10_000;\nconst MIN_SWITCHOVER_RETRY_DELAY_MS = 1_000;\nconst MAX_SWITCHOVER_RETRY_DELAY_MS = 30_000;\n\n/**\n * Bounds the time to search for another replication-manager to switch over\n * to, after which the current subscription is closed so that the draining\n * replication-manager can shut down (e.g. when it is the only one). The time\n * during which a subscription is held by another replication-manager (i.e.\n * waiting for permission to restore) does not count towards it.\n *\n * This is well below the default termination grace period of container\n * runners (30 seconds in Kubernetes and ECS), so that the draining\n * replication-manager can shut down cleanly when there is no other.\n */\nconst DEFAULT_SWITCHOVER_SEARCH_TIMEOUT_MS = 20_000;\n\nexport type SwitchoverOptions = {\n /**\n * Called when a replication-manager with a newer replica version grants\n * permission to restore its replica. This should initiate a graceful\n * restart of the server (during which the current subscription continues\n * to be served).\n */\n onRestoreGranted: () => void;\n\n /**\n * The replica file, from which the state of the last committed\n * transaction is read for switchover subscriptions.\n */\n replicaFile: string;\n\n /** For testing. */\n timeoutMs?: number | undefined;\n\n /** For testing. */\n searchTimeoutMs?: number | undefined;\n};\n\nexport type Downstream = Source<Sized<SubscribeDownstream>>;\n\ntype AttemptOutcome =\n | 'switched'\n | 'restoring'\n // The subscription was held for restore, but closed before a grant.\n | 'released'\n | 'retry';\n\n/**\n * Handles the switchover of a serving replica's subscription from a draining\n * replication-manager to another one, without interrupting the current\n * subscription.\n *\n * Upon {@link onChangeStreamerDrain}, the handler repeatedly (with backoff)\n * opens a new `switchover` subscription, at the watermark of the replica's\n * last committed transaction, to whichever replication-manager the\n * connection is routed to. The first message on the new subscription\n * determines the outcome:\n * * `status`: The subscription was accepted (i.e. the replica is compatible).\n * The current subscription is cancelled (aborting any transaction in\n * progress), and the new one is made available via\n * {@link getNextConnection}. Note that the new subscription may start with\n * transactions that were committed from the current subscription in the\n * meantime, which the consumer must skip.\n * * `status` with `restore: queued`: The replication-manager has a newer\n * replica version, and is holding the subscription until it grants\n * permission to restore. The held subscription is monitored for the grant.\n * * `status` with `restore: granted`: Permission to restore is granted, and\n * {@link SwitchoverOptions.onRestoreGranted} is called. The granted\n * subscription is kept open until the process exits, so that the\n * replication-manager can track the grant until the restore begins.\n * * Anything else (e.g. an error, or a `draining` replication-manager):\n * The switchover is retried later.\n *\n * If no other replication-manager accepts or holds a switchover subscription\n * within {@link DEFAULT_SWITCHOVER_SEARCH_TIMEOUT_MS} (e.g. because the\n * draining one is the only one), the current subscription is closed so that\n * the draining replication-manager can shut down, and the consumer reconnects\n * on its own.\n *\n * The current subscription continues to be consumed throughout. A\n * switchover is scoped to the subscription from which it switches: If that\n * subscription ends first (e.g. the replication-manager shuts down), the\n * switchover is abandoned, including any held (but not granted)\n * subscription, as the consumer reconnects on its own.\n */\nexport class DrainHandler {\n readonly #lc: LogContext;\n readonly #changeStreamer: ChangeStreamer;\n readonly #state: RunningState;\n readonly #options: SwitchoverOptions;\n readonly #committedContext: () => SubscriberContext;\n\n // The subscription currently being switched from.\n #draining: Downstream | undefined;\n #restoreGranted = false;\n #next: Downstream | undefined;\n\n /**\n * @param committedContext Returns the subscriber context as of the\n * replica's last committed transaction.\n */\n constructor(\n lc: LogContext,\n changeStreamer: ChangeStreamer,\n state: RunningState,\n options: SwitchoverOptions,\n committedContext: () => SubscriberContext,\n ) {\n this.#lc = lc.withContext('component', 'drain-handler');\n this.#changeStreamer = changeStreamer;\n this.#state = state;\n this.#options = options;\n this.#committedContext = committedContext;\n }\n\n /**\n * Called when the change-streamer of the `current` subscription is\n * draining.\n */\n onChangeStreamerDrain(current: Downstream) {\n if (\n this.#restoreGranted ||\n this.#draining === current ||\n !this.#isActive(current)\n ) {\n return;\n }\n this.#draining = current;\n this.#lc.info?.(\n `replication-manager is draining. switching over to another`,\n );\n void this.#switchover(current);\n }\n\n get hasNextConnection(): boolean {\n return this.#next !== undefined;\n }\n\n /**\n * Returns (and clears) the subscription that replaces the (now cancelled)\n * current one, if a switchover succeeded.\n */\n getNextConnection(): Downstream | undefined {\n const next = this.#next;\n this.#next = undefined;\n return next;\n }\n\n #isActive(current: Downstream) {\n return !current.signal.aborted && this.#state.shouldRun();\n }\n\n async #switchover(current: Downstream) {\n const searchTimeoutMs =\n this.#options.searchTimeoutMs ?? DEFAULT_SWITCHOVER_SEARCH_TIMEOUT_MS;\n let failures = 0;\n let searchStart = Date.now();\n while (this.#isActive(current)) {\n const outcome = await this.#attempt(current);\n if (outcome === 'switched' || outcome === 'restoring') {\n return;\n }\n if (outcome === 'released') {\n // Time spent held by another replication-manager does not count\n // towards the search.\n failures = 0;\n searchStart = Date.now();\n }\n const remaining = searchStart + searchTimeoutMs - Date.now();\n if (remaining > 0) {\n const delay = Math.min(\n MIN_SWITCHOVER_RETRY_DELAY_MS * 2 ** failures++,\n MAX_SWITCHOVER_RETRY_DELAY_MS,\n );\n await this.#state.sleep(Math.min(delay, remaining));\n continue;\n }\n // The search has timed out.\n if (this.#isActive(current)) {\n this.#lc.warn?.(\n `no other replication-manager found within ${searchTimeoutMs} ms. ` +\n `disconnecting from the draining replication-manager`,\n );\n current.cancel();\n }\n return;\n }\n }\n\n async #attempt(current: Downstream): Promise<AttemptOutcome> {\n const lc = this.#lc;\n const timeoutMs = this.#options.timeoutMs ?? DEFAULT_SWITCHOVER_TIMEOUT_MS;\n let candidate: Downstream | undefined;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n try {\n const ctx = this.#committedContext();\n lc.info?.(`attempting switchover at ${ctx.watermark}`);\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n timedOut = true;\n reject(new Error(`timed out after ${timeoutMs} ms`));\n }, timeoutMs);\n });\n const subscribing = this.#changeStreamer.subscribe({\n ...ctx,\n switchover: true,\n });\n // Clean up a connection that is established after a timeout.\n void subscribing.then(\n sub => (timedOut ? sub.cancel() : undefined),\n () => {},\n );\n candidate = await Promise.race([subscribing, timeout]);\n const {head, rest} = readHead(candidate);\n const first = await Promise.race([head.next(), timeout]);\n const msg = first.done ? undefined : first.value.data;\n\n if (!this.#isActive(current)) {\n // e.g. The current subscription failed in the meantime.\n } else if (msg?.[0] === 'status') {\n const {restore, draining} = msg[1];\n if (restore) {\n const granted = await this.#holdForRestore(current, rest, restore);\n if (granted) {\n return 'restoring';\n }\n lc.info?.(`held switchover subscription closed. will retry`);\n return 'released';\n }\n if (!draining) {\n lc.info?.(`switched over to a new subscription at ${ctx.watermark}`);\n this.#next = rest;\n current.cancel();\n return 'switched';\n }\n }\n lc.warn?.(`switchover rejected. continuing current subscription`, msg);\n } catch (e) {\n lc.warn?.(`switchover failed. continuing current subscription`, e);\n } finally {\n clearTimeout(timer);\n }\n candidate?.cancel();\n return 'retry';\n }\n\n /**\n * Keeps the held (or granted) subscription open, and initiates a graceful\n * restart when permission to restore is granted. A held subscription that\n * has yet to be granted is cancelled if the `current` subscription ends.\n *\n * @returns `true` once permission is granted, or `false` if the held\n * subscription closes before then.\n */\n #holdForRestore(\n current: Downstream,\n held: Downstream,\n restore: NonNullable<Status['restore']>,\n ): Promise<boolean> {\n const lc = this.#lc;\n const result = resolver<boolean>();\n const unregister = this.#state.cancelOnStop(held);\n const onCurrentEnded = () => {\n if (!this.#restoreGranted) {\n lc.info?.(`abandoning held switchover subscription`);\n held.cancel();\n }\n };\n current.signal.addEventListener('abort', onCurrentEnded, {once: true});\n\n const onRestore = (r: NonNullable<Status['restore']>) => {\n if (r.state === 'queued') {\n lc.info?.(\n `waiting for permission to restore a new replica (position ${r.position})`,\n );\n } else if (!this.#restoreGranted) {\n lc.info?.(\n `granted permission to restore a new replica. restarting gracefully`,\n );\n this.#restoreGranted = true;\n this.#options.onRestoreGranted();\n result.resolve(true);\n }\n };\n onRestore(restore);\n void (async () => {\n try {\n // Note: The loop must not be exited early, as that would close the\n // connection.\n for await (const {data: msg} of held) {\n if (msg[0] === 'status' && msg[1].restore) {\n onRestore(msg[1].restore);\n }\n }\n } catch (e) {\n lc.warn?.(`held switchover subscription failed`, e);\n } finally {\n unregister();\n current.signal.removeEventListener('abort', onCurrentEnded);\n result.resolve(false);\n }\n })();\n return result.promise;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { resolver } from "../../../../../shared/src/resolver.js";
|
|
1
2
|
import { getOrCreateGauge } from "../../../observability/metrics.js";
|
|
2
|
-
import { resolver } from "@rocicorp/resolver";
|
|
3
3
|
//#region ../zero-cache/src/services/replicator/reporter/recorder.ts
|
|
4
4
|
var LOG_ALL_REPLICATION_REPORTS_AT_DEBUG = process.env.ZERO_LOG_ALL_REPLICATION_REPORTS_AT_DEBUG === "1";
|
|
5
5
|
var ReplicationReportRecorder = class {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recorder.js","names":[],"sources":["../../../../../../../zero-cache/src/services/replicator/reporter/recorder.ts"],"sourcesContent":["import type {ObservableResult} from '@opentelemetry/api';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {getOrCreateGauge} from '../../../observability/metrics.ts';\nimport type {ReplicationReport} from './report-schema.ts';\n\n// Hook for sanity checking lag reports in development.\nconst LOG_ALL_REPLICATION_REPORTS_AT_DEBUG =\n process.env.ZERO_LOG_ALL_REPLICATION_REPORTS_AT_DEBUG === '1';\n\nexport type ReplicationLagReadinessOptions = {\n readinessThresholdMs?: number | undefined;\n maxReadinessDelayMs?: number | undefined;\n setTimeoutFn?: typeof setTimeout | undefined;\n clearTimeoutFn?: typeof clearTimeout | undefined;\n};\n\nexport class ReplicationReportRecorder {\n readonly #lc: LogContext;\n readonly #readinessThresholdMs: number;\n readonly #readyGate = resolver();\n readonly #clearTimeout: typeof clearTimeout;\n readonly #readinessTimer: ReturnType<typeof setTimeout>;\n #ready = false;\n #last: ReplicationReport | null = null;\n\n constructor(\n lc: LogContext,\n {\n readinessThresholdMs = 1000,\n maxReadinessDelayMs = 600_000,\n setTimeoutFn = setTimeout,\n clearTimeoutFn = clearTimeout,\n }: ReplicationLagReadinessOptions = {},\n ) {\n this.#lc = lc;\n this.#readinessThresholdMs = readinessThresholdMs;\n this.#clearTimeout = clearTimeoutFn;\n\n this.#readinessTimer = setTimeoutFn(\n () =>\n this.#setReady(\n 'warn',\n `timed out waiting for replication lag readiness threshold of ${this.#readinessThresholdMs} ms`,\n ),\n maxReadinessDelayMs,\n );\n // The timer should not keep the process alive.\n this.#readinessTimer?.unref?.();\n }\n\n ready(): Promise<void> {\n return this.#readyGate.promise;\n }\n\n record(report: ReplicationReport) {\n if (isLagReportingDisabledSignal(report)) {\n this.#setReady(\n 'info',\n `replica is caught up (replication lag reporting is disabled upstream)`,\n );\n return;\n }\n this.#last = report;\n\n const {lastTimings} = report;\n if (lastTimings) {\n const total = lastTimings.replicateTimeMs - lastTimings.sendTimeMs;\n if (total > 10_000) {\n const log = this.#ready ? 'warn' : 'info';\n this.#lc[log]?.(`high replication lag: ${total} ms`, {\n ready: this.#ready,\n report,\n });\n } else if (total > 1_000) {\n this.#lc.info?.(`replication lag: ${total} ms`, {\n ready: this.#ready,\n report,\n });\n }\n if (LOG_ALL_REPLICATION_REPORTS_AT_DEBUG) {\n this.#lc.debug?.(`replication lag ${total} ms`, {\n ready: this.#ready,\n report,\n });\n }\n if (!this.#ready && total <= this.#readinessThresholdMs) {\n this.#setReady(\n 'info',\n `replicator is now ready with replication lag (${total} ms) less than ${this.#readinessThresholdMs} ms readiness threshold`,\n );\n }\n }\n }\n\n #setReady(log: LogLevel, message: string) {\n if (this.#ready) {\n return;\n }\n this.#lc[log]?.(message, {lastReport: this.#last});\n this.#ready = true;\n this.#clearTimeout(this.#readinessTimer);\n this.#readyGate.resolve();\n\n getOrCreateGauge('replication', 'upstream_lag', {\n description:\n 'Latency from sending an upstream replication report ' +\n 'to receiving it in the replication stream',\n unit: 'millisecond',\n }).addCallback(this.reportUpstreamLag);\n\n getOrCreateGauge('replication', 'replica_lag', {\n description:\n 'Latency from receiving an upstream replication report ' +\n 'to its reaching the replica',\n unit: 'millisecond',\n }).addCallback(this.reportReplicaLag);\n\n getOrCreateGauge('replication', 'total_lag', {\n description:\n 'Latency from sending an upstream replication report to its ' +\n 'reaching the replica. This reflects the actual measured ' +\n 'round-trip of the most recently received report and does not ' +\n 'grow when reports stop arriving.',\n unit: 'millisecond',\n }).addCallback(this.reportTotalLag);\n\n getOrCreateGauge('replication', 'last_total_lag', {\n description:\n 'Latency from sending the most recently received upstream ' +\n 'replication report to its reaching the replica. This is an alias ' +\n 'of replication.total_lag retained for dashboards that explicitly ' +\n 'want the non-extrapolated value.',\n unit: 'millisecond',\n }).addCallback(this.reportLastTotalLag);\n\n getOrCreateGauge('replication', 'upstream_clock_skew', {\n description:\n 'Estimated offset of the upstream database clock relative to ' +\n \"zero-cache's. Positive means upstream is running ahead. \" +\n 'Derived from the lag report round-trip, whose send and receive ' +\n 'times are stamped on our clock either side of the upstream ' +\n 'commit timestamp. This matters because ' +\n 'sync.e2e_serving_lag subtracts an upstream commit timestamp from ' +\n 'a local clock, so a skewed upstream clock biases it directly -- ' +\n 'and an upstream clock running ahead biases it *low*, which looks ' +\n 'like healthy serving rather than a broken measurement.',\n unit: 'millisecond',\n }).addCallback(this.reportUpstreamClockSkew);\n }\n\n readonly reportUpstreamLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.receiveTimeMs - last.sendTimeMs);\n }\n };\n\n readonly reportReplicaLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.replicateTimeMs - last.receiveTimeMs);\n }\n };\n\n readonly reportTotalLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.replicateTimeMs - last.sendTimeMs);\n }\n };\n\n readonly reportLastTotalLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.replicateTimeMs - last.sendTimeMs);\n }\n };\n\n readonly reportUpstreamClockSkew = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(estimateUpstreamClockSkewMs(last));\n }\n };\n}\n\n/**\n * Estimates how far the upstream database's clock is from ours, in\n * milliseconds, positive meaning upstream is ahead.\n *\n * `sendTimeMs` and `receiveTimeMs` are both stamped in the\n * replication-manager, either side of `commitTimeMs`, which upstream stamps.\n * Assuming the upstream commit fell midway through that round trip, the\n * difference between where it actually landed and that midpoint is the clock\n * offset -- the same round-trip estimate NTP uses.\n *\n * The estimate is biased by any asymmetry between the two legs: the outbound\n * leg is a SQL round trip, while the return leg additionally waits on a WAL\n * flush and the walsender (which the pre-PG-17 path notes can add 50~100ms on\n * an idle database). So treat this as a detector of gross skew -- the kind\n * that invalidates sync.e2e_serving_lag -- and not as a sub-100ms correction.\n *\n * The rigorous bound, if a tighter reading is ever needed: the commit\n * genuinely happened between send and receive, so a `commitTimeMs` outside\n * that window proves skew of at least the amount by which it falls outside,\n * independent of any latency assumption.\n */\nexport function estimateUpstreamClockSkewMs({\n sendTimeMs,\n commitTimeMs,\n receiveTimeMs,\n}: {\n sendTimeMs: number;\n commitTimeMs: number;\n receiveTimeMs: number;\n}): number {\n return commitTimeMs - (sendTimeMs + receiveTimeMs) / 2;\n}\n\n/**\n * Returns whether the `report` is the signal sent by the change-streamer,\n * once the subscriber is caught up, to indicate that lag reporting is\n * disabled upstream (and thus readiness cannot be gated on lag reports).\n *\n * This is a lag report with a `nextSendTimeMs` of 0 and no `lastTimings`.\n */\nexport function isLagReportingDisabledSignal(\n report: ReplicationReport,\n): boolean {\n return report.nextSendTimeMs === 0 && report.lastTimings === undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"recorder.js","names":[],"sources":["../../../../../../../zero-cache/src/services/replicator/reporter/recorder.ts"],"sourcesContent":["import type {ObservableResult} from '@opentelemetry/api';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport {resolver} from '../../../../../shared/src/resolver.ts';\nimport {getOrCreateGauge} from '../../../observability/metrics.ts';\nimport type {ReplicationReport} from './report-schema.ts';\n\n// Hook for sanity checking lag reports in development.\nconst LOG_ALL_REPLICATION_REPORTS_AT_DEBUG =\n process.env.ZERO_LOG_ALL_REPLICATION_REPORTS_AT_DEBUG === '1';\n\nexport type ReplicationLagReadinessOptions = {\n readinessThresholdMs?: number | undefined;\n maxReadinessDelayMs?: number | undefined;\n setTimeoutFn?: typeof setTimeout | undefined;\n clearTimeoutFn?: typeof clearTimeout | undefined;\n};\n\nexport class ReplicationReportRecorder {\n readonly #lc: LogContext;\n readonly #readinessThresholdMs: number;\n readonly #readyGate = resolver();\n readonly #clearTimeout: typeof clearTimeout;\n readonly #readinessTimer: ReturnType<typeof setTimeout>;\n #ready = false;\n #last: ReplicationReport | null = null;\n\n constructor(\n lc: LogContext,\n {\n readinessThresholdMs = 1000,\n maxReadinessDelayMs = 600_000,\n setTimeoutFn = setTimeout,\n clearTimeoutFn = clearTimeout,\n }: ReplicationLagReadinessOptions = {},\n ) {\n this.#lc = lc;\n this.#readinessThresholdMs = readinessThresholdMs;\n this.#clearTimeout = clearTimeoutFn;\n\n this.#readinessTimer = setTimeoutFn(\n () =>\n this.#setReady(\n 'warn',\n `timed out waiting for replication lag readiness threshold of ${this.#readinessThresholdMs} ms`,\n ),\n maxReadinessDelayMs,\n );\n // The timer should not keep the process alive.\n this.#readinessTimer?.unref?.();\n }\n\n ready(): Promise<void> {\n return this.#readyGate.promise;\n }\n\n record(report: ReplicationReport) {\n if (isLagReportingDisabledSignal(report)) {\n this.#setReady(\n 'info',\n `replica is caught up (replication lag reporting is disabled upstream)`,\n );\n return;\n }\n this.#last = report;\n\n const {lastTimings} = report;\n if (lastTimings) {\n const total = lastTimings.replicateTimeMs - lastTimings.sendTimeMs;\n if (total > 10_000) {\n const log = this.#ready ? 'warn' : 'info';\n this.#lc[log]?.(`high replication lag: ${total} ms`, {\n ready: this.#ready,\n report,\n });\n } else if (total > 1_000) {\n this.#lc.info?.(`replication lag: ${total} ms`, {\n ready: this.#ready,\n report,\n });\n }\n if (LOG_ALL_REPLICATION_REPORTS_AT_DEBUG) {\n this.#lc.debug?.(`replication lag ${total} ms`, {\n ready: this.#ready,\n report,\n });\n }\n if (!this.#ready && total <= this.#readinessThresholdMs) {\n this.#setReady(\n 'info',\n `replicator is now ready with replication lag (${total} ms) less than ${this.#readinessThresholdMs} ms readiness threshold`,\n );\n }\n }\n }\n\n #setReady(log: LogLevel, message: string) {\n if (this.#ready) {\n return;\n }\n this.#lc[log]?.(message, {lastReport: this.#last});\n this.#ready = true;\n this.#clearTimeout(this.#readinessTimer);\n this.#readyGate.resolve();\n\n getOrCreateGauge('replication', 'upstream_lag', {\n description:\n 'Latency from sending an upstream replication report ' +\n 'to receiving it in the replication stream',\n unit: 'millisecond',\n }).addCallback(this.reportUpstreamLag);\n\n getOrCreateGauge('replication', 'replica_lag', {\n description:\n 'Latency from receiving an upstream replication report ' +\n 'to its reaching the replica',\n unit: 'millisecond',\n }).addCallback(this.reportReplicaLag);\n\n getOrCreateGauge('replication', 'total_lag', {\n description:\n 'Latency from sending an upstream replication report to its ' +\n 'reaching the replica. This reflects the actual measured ' +\n 'round-trip of the most recently received report and does not ' +\n 'grow when reports stop arriving.',\n unit: 'millisecond',\n }).addCallback(this.reportTotalLag);\n\n getOrCreateGauge('replication', 'last_total_lag', {\n description:\n 'Latency from sending the most recently received upstream ' +\n 'replication report to its reaching the replica. This is an alias ' +\n 'of replication.total_lag retained for dashboards that explicitly ' +\n 'want the non-extrapolated value.',\n unit: 'millisecond',\n }).addCallback(this.reportLastTotalLag);\n\n getOrCreateGauge('replication', 'upstream_clock_skew', {\n description:\n 'Estimated offset of the upstream database clock relative to ' +\n \"zero-cache's. Positive means upstream is running ahead. \" +\n 'Derived from the lag report round-trip, whose send and receive ' +\n 'times are stamped on our clock either side of the upstream ' +\n 'commit timestamp. This matters because ' +\n 'sync.e2e_serving_lag subtracts an upstream commit timestamp from ' +\n 'a local clock, so a skewed upstream clock biases it directly -- ' +\n 'and an upstream clock running ahead biases it *low*, which looks ' +\n 'like healthy serving rather than a broken measurement.',\n unit: 'millisecond',\n }).addCallback(this.reportUpstreamClockSkew);\n }\n\n readonly reportUpstreamLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.receiveTimeMs - last.sendTimeMs);\n }\n };\n\n readonly reportReplicaLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.replicateTimeMs - last.receiveTimeMs);\n }\n };\n\n readonly reportTotalLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.replicateTimeMs - last.sendTimeMs);\n }\n };\n\n readonly reportLastTotalLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.replicateTimeMs - last.sendTimeMs);\n }\n };\n\n readonly reportUpstreamClockSkew = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(estimateUpstreamClockSkewMs(last));\n }\n };\n}\n\n/**\n * Estimates how far the upstream database's clock is from ours, in\n * milliseconds, positive meaning upstream is ahead.\n *\n * `sendTimeMs` and `receiveTimeMs` are both stamped in the\n * replication-manager, either side of `commitTimeMs`, which upstream stamps.\n * Assuming the upstream commit fell midway through that round trip, the\n * difference between where it actually landed and that midpoint is the clock\n * offset -- the same round-trip estimate NTP uses.\n *\n * The estimate is biased by any asymmetry between the two legs: the outbound\n * leg is a SQL round trip, while the return leg additionally waits on a WAL\n * flush and the walsender (which the pre-PG-17 path notes can add 50~100ms on\n * an idle database). So treat this as a detector of gross skew -- the kind\n * that invalidates sync.e2e_serving_lag -- and not as a sub-100ms correction.\n *\n * The rigorous bound, if a tighter reading is ever needed: the commit\n * genuinely happened between send and receive, so a `commitTimeMs` outside\n * that window proves skew of at least the amount by which it falls outside,\n * independent of any latency assumption.\n */\nexport function estimateUpstreamClockSkewMs({\n sendTimeMs,\n commitTimeMs,\n receiveTimeMs,\n}: {\n sendTimeMs: number;\n commitTimeMs: number;\n receiveTimeMs: number;\n}): number {\n return commitTimeMs - (sendTimeMs + receiveTimeMs) / 2;\n}\n\n/**\n * Returns whether the `report` is the signal sent by the change-streamer,\n * once the subscriber is caught up, to indicate that lag reporting is\n * disabled upstream (and thus readiness cannot be gated on lag reports).\n *\n * This is a lag report with a `nextSendTimeMs` of 0 and no `lastTimings`.\n */\nexport function isLagReportingDisabledSignal(\n report: ReplicationReport,\n): boolean {\n return report.nextSendTimeMs === 0 && report.lastTimings === undefined;\n}\n"],"mappings":"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"}
|
|
@@ -12,14 +12,7 @@ export declare const changeSourceReportSchema: v.ObjectType<{
|
|
|
12
12
|
}, undefined>;
|
|
13
13
|
nextSendTimeMs: v.Type<number>;
|
|
14
14
|
}, undefined>;
|
|
15
|
-
|
|
16
|
-
sendTimeMs: v.Type<number>;
|
|
17
|
-
commitTimeMs: v.Type<number>;
|
|
18
|
-
receiveTimeMs: v.Type<number>;
|
|
19
|
-
}, "replicateTimeMs"> & {
|
|
20
|
-
replicateTimeMs: v.Type<number>;
|
|
21
|
-
}, undefined>;
|
|
22
|
-
export declare const replicationReportSchema: v.ObjectType<{
|
|
15
|
+
declare const replicationReportSchema: v.ObjectType<{
|
|
23
16
|
lastTimings: v.Optional<{
|
|
24
17
|
sendTimeMs: number;
|
|
25
18
|
commitTimeMs: number;
|
|
@@ -28,8 +21,6 @@ export declare const replicationReportSchema: v.ObjectType<{
|
|
|
28
21
|
}>;
|
|
29
22
|
nextSendTimeMs: v.Type<number>;
|
|
30
23
|
}, undefined>;
|
|
31
|
-
export type ChangeSourceTimings = v.Infer<typeof changeSourceTimingsSchema>;
|
|
32
|
-
export type ChangeSourceReport = v.Infer<typeof changeSourceReportSchema>;
|
|
33
|
-
export type ReplicationTimings = v.Infer<typeof replicationTimingsSchema>;
|
|
34
24
|
export type ReplicationReport = v.Infer<typeof replicationReportSchema>;
|
|
25
|
+
export {};
|
|
35
26
|
//# sourceMappingURL=report-schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report-schema.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/replicator/reporter/report-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,qCAAqC,CAAC;AAEzD,eAAO,MAAM,yBAAyB;;;;aAIpC,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;aAGnC,CAAC;
|
|
1
|
+
{"version":3,"file":"report-schema.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/replicator/reporter/report-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,qCAAqC,CAAC;AAEzD,eAAO,MAAM,yBAAyB;;;;aAIpC,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;aAGnC,CAAC;AAMH,QAAA,MAAM,uBAAuB;;;;;;;;aAG3B,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
|
|
@@ -15,6 +15,6 @@ valita_exports.object({
|
|
|
15
15
|
nextSendTimeMs: valita_exports.number()
|
|
16
16
|
});
|
|
17
17
|
//#endregion
|
|
18
|
-
export { changeSourceReportSchema, changeSourceTimingsSchema
|
|
18
|
+
export { changeSourceReportSchema, changeSourceTimingsSchema };
|
|
19
19
|
|
|
20
20
|
//# sourceMappingURL=report-schema.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report-schema.js","names":["v"],"sources":["../../../../../../../zero-cache/src/services/replicator/reporter/report-schema.ts"],"sourcesContent":["import * as v from '../../../../../shared/src/valita.ts';\n\nexport const changeSourceTimingsSchema = v.object({\n sendTimeMs: v.number(),\n commitTimeMs: v.number(),\n receiveTimeMs: v.number(),\n});\n\nexport const changeSourceReportSchema = v.object({\n lastTimings: changeSourceTimingsSchema,\n nextSendTimeMs: v.number(),\n});\n\
|
|
1
|
+
{"version":3,"file":"report-schema.js","names":["v"],"sources":["../../../../../../../zero-cache/src/services/replicator/reporter/report-schema.ts"],"sourcesContent":["import * as v from '../../../../../shared/src/valita.ts';\n\nexport const changeSourceTimingsSchema = v.object({\n sendTimeMs: v.number(),\n commitTimeMs: v.number(),\n receiveTimeMs: v.number(),\n});\n\nexport const changeSourceReportSchema = v.object({\n lastTimings: changeSourceTimingsSchema,\n nextSendTimeMs: v.number(),\n});\n\nconst replicationTimingsSchema = changeSourceTimingsSchema.extend({\n replicateTimeMs: v.number(),\n});\n\nconst replicationReportSchema = v.object({\n lastTimings: replicationTimingsSchema.optional(),\n nextSendTimeMs: v.number(),\n});\n\nexport type ReplicationReport = v.Infer<typeof replicationReportSchema>;\n"],"mappings":";;AAEA,IAAa,4BAA4BA,eAAE,OAAO;CAChD,YAAYA,eAAE,OAAO;CACrB,cAAcA,eAAE,OAAO;CACvB,eAAeA,eAAE,OAAO;AAC1B,CAAC;AAED,IAAa,2BAA2BA,eAAE,OAAO;CAC/C,aAAa;CACb,gBAAgBA,eAAE,OAAO;AAC3B,CAAC;AAED,IAAM,2BAA2B,0BAA0B,OAAO,EAChE,iBAAiBA,eAAE,OAAO,EAC5B,CAAC;AAE+BA,eAAE,OAAO;CACvC,aAAa,yBAAyB,SAAS;CAC/C,gBAAgBA,eAAE,OAAO;AAC3B,CAAC"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type JSONValue } from '../../../../../shared/src/bigint-json.ts';
|
|
2
|
-
import * as v from '../../../../../shared/src/valita.ts';
|
|
3
2
|
import type { Database } from '../../../../../zqlite/src/db.ts';
|
|
4
3
|
import type { LexiVersion } from '../../../types/lexi-version.ts';
|
|
5
4
|
import type { LiteRowKey } from '../../../types/lite.ts';
|
|
@@ -62,14 +61,6 @@ export type ChangeLogEntry = {
|
|
|
62
61
|
* {@link ChangeLog}).
|
|
63
62
|
*/
|
|
64
63
|
export declare function parseChangeLogEntry(stateVersion: string, table: string, rowKey: string, op: string): ChangeLogEntry;
|
|
65
|
-
declare const rawChangeLogEntrySchema: v.ObjectType<{
|
|
66
|
-
stateVersion: v.Type<string>;
|
|
67
|
-
table: v.Type<string>;
|
|
68
|
-
rowKey: v.Type<string>;
|
|
69
|
-
op: v.Type<"d" | "r" | "s" | "t">;
|
|
70
|
-
backfillingColumnVersions: v.Type<Record<string, string>>;
|
|
71
|
-
}, undefined>;
|
|
72
|
-
export type RawChangeLogEntry = v.Infer<typeof rawChangeLogEntrySchema>;
|
|
73
64
|
export declare class ChangeLog {
|
|
74
65
|
#private;
|
|
75
66
|
constructor(db: Database);
|
|
@@ -96,5 +87,4 @@ export declare class ChangeLog {
|
|
|
96
87
|
logTruncateOp(version: LexiVersion, table: string): void;
|
|
97
88
|
logResetOp(version: LexiVersion, table: string): void;
|
|
98
89
|
}
|
|
99
|
-
export {};
|
|
100
90
|
//# sourceMappingURL=change-log.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-log.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/replicator/schema/change-log.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,0CAA0C,CAAC;
|
|
1
|
+
{"version":3,"file":"change-log.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/replicator/schema/change-log.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,0CAA0C,CAAC;AAElD,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gCAAgC,CAAC;AAChE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,wBAAwB,CAAC;AAGvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,eAAO,MAAM,MAAM,MAAM,CAAC;AAC1B,eAAO,MAAM,MAAM,MAAM,CAAC;AAC1B,eAAO,MAAM,WAAW,MAAM,CAAC;AAC/B,eAAO,MAAM,QAAQ,MAAM,CAAC;AAG5B,eAAO,MAAM,uBAAuB,sbAmCjC,CAAC;AAEJ;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GACtB;IACE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IACrD,QAAQ,CAAC,EAAE,EAAE,OAAO,MAAM,GAAG,OAAO,MAAM,CAAC;CAC5C,GACD;IACE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,OAAO,WAAW,GAAG,OAAO,QAAQ,CAAC;CACnD,CAAC;AAEN;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,GACT,cAAc,CAWhB;AAqBD,qBAAa,SAAS;;IAMpB,YAAY,EAAE,EAAE,QAAQ,EAqCvB;IAED;;;;;;;;;;OAUG;IACH,QAAQ,CACN,OAAO,EAAE,WAAW,EACpB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,UAAU,EACf,UAAU,EAAE,MAAM,EAAE,GAAG,SAAS,GAC/B,MAAM,CAER;IAED,WAAW,CACT,OAAO,EAAE,WAAW,EACpB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,UAAU,GACd,MAAM,CAKR;IAED,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU;;;;;;kBAM5C;IA8BD,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,QAEhD;IAED,UAAU,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,QAE7C;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-log.js","names":["parse","v","v.literalUnion","v.parse"],"sources":["../../../../../../../zero-cache/src/services/replicator/schema/change-log.ts"],"sourcesContent":["import {assert} from '../../../../../shared/src/asserts.ts';\nimport {\n parse,\n stringify,\n type JSONValue,\n} from '../../../../../shared/src/bigint-json.ts';\nimport * as v from '../../../../../shared/src/valita.ts';\nimport type {Database, Statement} from '../../../../../zqlite/src/db.ts';\nimport type {LexiVersion} from '../../../types/lexi-version.ts';\nimport type {LiteRowKey} from '../../../types/lite.ts';\nimport {normalizedKeyOrder} from '../../../types/row-key.ts';\n\n/**\n * The Change Log tracks the last operation (set or delete) for each row in the\n * data base, ordered by state version; in other words, a cross-table\n * index of row changes ordered by version. This facilitates a minimal \"diff\"\n * of row changes needed to advance a pipeline from one state version to another.\n *\n * The Change Log stores identifiers only, i.e. it does not store contents.\n * A database snapshot at the previous version can be used to query a row's\n * old contents, if any, and the current snapshot can be used to query a row's\n * new contents. (In the common case, the new contents will have just been applied\n * and thus has a high likelihood of being in the SQLite cache.)\n *\n * There are two table-wide operations:\n * - `t` corresponds to the postgres `TRUNCATE` operation\n * - `r` represents any schema (i.e. column) change\n *\n * For both operations, the corresponding row changes are not explicitly included\n * in the change log. The consumer has the option of simulating them be reading\n * from pre- and post- snapshots, or resetting their state entirely with the current\n * snapshot.\n *\n * To achieve the desired ordering semantics when processing tables that have been\n * truncated, reset, and modified, the \"rowKey\" is set to `null` for resets and\n * the empty string `\"\"` for truncates. This means that resets will be encountered\n * before truncates, which will be processed before any subsequent row changes.\n *\n * This ordering is chosen because resets are currently the more \"destructive\" op\n * and result in aborting the processing (and starting from scratch); doing this\n * earlier reduces wasted work.\n */\n\nexport const SET_OP = 's';\nexport const DEL_OP = 'd';\nexport const TRUNCATE_OP = 't';\nexport const RESET_OP = 'r';\n\n// Exported for testing (and migrations)\nexport const CREATE_CHANGELOG_SCHEMA =\n // stateVersion : a.k.a. row version\n // pos : order in which to process the change (within the version)\n // table : The table associated with the change\n // rowKey : JSON row key for a row change. For table-wide changes RESET\n // and TRUNCATE, there is no associated row; instead, `pos` is\n // set to -1 and the rowKey is set to the stateVersion,\n // guaranteeing when attempting to process the transaction,\n // the pipeline is reset (and the change log traversal\n // aborted).\n // op : 's' for set (insert/update)\n // : 'd' for delete\n // : 'r' for table reset (schema change)\n // : 't' for table truncation (which also resets the pipeline)\n // backfillingColumnVersions\n // : A JSON mapping from column name to stateVersion tracked\n // for replicated writes of columns that are being backfilled.\n // This is used to prevent backfill data, which is at a\n // fixed snapshot/version outside of the replication stream,\n // from overwriting newer column values.\n //\n // Naming note: To maintain compatibility between a new replication-manager\n // and old view-syncers, the previous _zero.changeLog table is preserved\n // and its replacement given a new name \"changeLog2\".\n `\n CREATE TABLE \"_zero.changeLog2\" (\n \"stateVersion\" TEXT NOT NULL,\n \"pos\" INT NOT NULL,\n \"table\" TEXT NOT NULL,\n \"rowKey\" TEXT NOT NULL,\n \"op\" TEXT NOT NULL,\n \"backfillingColumnVersions\" TEXT DEFAULT '{}',\n PRIMARY KEY(\"stateVersion\", \"pos\"),\n UNIQUE(\"table\", \"rowKey\")\n );\n `;\n\n/**\n * Contains the changeLog fields relevant for computing the diff between\n * two snapshots of a replica. The `pos` and `backfillingColumnVersions`\n * fields are excluded, though the query should be ordered by\n * `<stateVersion, pos>`.\n */\nexport type ChangeLogEntry =\n | {\n readonly stateVersion: string;\n readonly table: string;\n readonly rowKey: Readonly<Record<string, JSONValue>>;\n readonly op: typeof SET_OP | typeof DEL_OP;\n }\n | {\n readonly stateVersion: string;\n readonly table: string;\n // Table-wide ops have no row.\n readonly rowKey: null;\n readonly op: typeof TRUNCATE_OP | typeof RESET_OP;\n };\n\n/**\n * The {@link ChangeLogEntry} for the columns of a change log row. This does\n * not validate what zero-cache itself wrote: the columns are\n * `TEXT NOT NULL`, and `rowKey` is the JSON of an object (see\n * {@link ChangeLog}).\n */\nexport function parseChangeLogEntry(\n stateVersion: string,\n table: string,\n rowKey: string,\n op: string,\n): ChangeLogEntry {\n switch (op) {\n case SET_OP:\n case DEL_OP:\n return {stateVersion, table, rowKey: parseRowKey(rowKey), op};\n case TRUNCATE_OP:\n case RESET_OP:\n return {stateVersion, table, rowKey: null, op};\n default:\n throw new Error(`Invalid change log op \"${op}\" for table \"${table}\"`);\n }\n}\n\nfunction parseRowKey(rowKey: string): Readonly<Record<string, JSONValue>> {\n const key = parse(rowKey);\n assert(\n typeof key === 'object' && key !== null && !Array.isArray(key),\n () => `Invalid change log rowKey ${rowKey}`,\n );\n return key as Readonly<Record<string, JSONValue>>;\n}\n\nconst rawChangeLogEntrySchema = v.object({\n stateVersion: v.string(),\n table: v.string(),\n rowKey: v.string(),\n op: v.literalUnion(SET_OP, DEL_OP, TRUNCATE_OP, RESET_OP),\n backfillingColumnVersions: v\n .string()\n .map(val => v.record(v.string()).parse(JSON.parse(val))),\n});\n\nexport type RawChangeLogEntry = v.Infer<typeof rawChangeLogEntrySchema>;\n\nexport class ChangeLog {\n readonly #logRowOpStmt: Statement;\n readonly #logRowOpWithBackfillStmt: Statement;\n readonly #logTableWideOpStmt;\n readonly #getRowOpStmt: Statement;\n\n constructor(db: Database) {\n this.#logRowOpStmt = db.prepare(/*sql*/ `\n INSERT OR REPLACE INTO \"_zero.changeLog2\" \n (stateVersion, pos, \"table\", rowKey, op)\n VALUES (@version, @pos, @table, JSON(@rowKey), @op)\n `);\n\n this.#logRowOpWithBackfillStmt = db.prepare(/*sql*/ `\n INSERT INTO \"_zero.changeLog2\" \n (stateVersion, pos, \"table\", rowKey, op, backfillingColumnVersions)\n VALUES (@version, @pos, @table, JSON(@rowKey), @op, \n JSON(@backfillingColumnVersions))\n ON CONFLICT (\"table\", rowKey) DO UPDATE \n SET stateVersion = excluded.stateVersion,\n pos = excluded.pos,\n op = excluded.op,\n backfillingColumnVersions = json_patch(\n backfillingColumnVersions, excluded.backfillingColumnVersions)\n `);\n\n // Because table-wide ops result in aborting an incremental update\n // and rehydrating all queries at \"head\", they are assigned pos = -1\n // as an optimization to abort as early as possible to skip unnecessary\n // updates.\n //\n // However, changeLog entries that are destined to be \"skipped\" are\n // nonetheless kept for the purpose of tracking backfillingColumnVersions.\n this.#logTableWideOpStmt = db.prepare(/*sql*/ `\n INSERT OR REPLACE INTO \"_zero.changeLog2\" \n (stateVersion, pos, \"table\", rowKey, op) \n VALUES (@version, -1, @table, @version, @op)\n `);\n\n // oxlint-disable-next-line zero/no-select-star -- Local SQLite replica query; not run through pg prepared statements.\n this.#getRowOpStmt = db.prepare(/*sql*/ `\n SELECT * FROM \"_zero.changeLog2\" WHERE \"table\" = ? AND \"rowKey\" = JSON(?)\n `);\n }\n\n /**\n *\n * @param backfilled The backfilling columns for which values were set. Note\n * that an empty list and the `undefined` value mean different things;\n * * An empty list indicates that a backfill is in progress but no\n * backfilling values were set. In this case, existing\n * backfillingColumnVersions are preserved.\n * * `undefined` indicates that there are no columns being backfilled.\n * In this case, any vestigial `backfillingColumnVersions` value\n * is cleared.\n */\n logSetOp(\n version: LexiVersion,\n pos: number,\n table: string,\n row: LiteRowKey,\n backfilled: string[] | undefined,\n ): string {\n return this.#logRowOp(version, pos, table, row, SET_OP, backfilled);\n }\n\n logDeleteOp(\n version: LexiVersion,\n pos: number,\n table: string,\n row: LiteRowKey,\n ): string {\n // Note: For delete ops, it is always safe to clear the\n // backfillingColumnVersions because the backfill algorithm\n // understands that deletes apply to the whole row.\n return this.#logRowOp(version, pos, table, row, DEL_OP, undefined);\n }\n\n getLatestRowOp(table: string, row: LiteRowKey) {\n const rowKey = stringify(normalizedKeyOrder(row));\n const result = this.#getRowOpStmt.get(table, rowKey);\n return result === undefined\n ? undefined\n : v.parse(result, rawChangeLogEntrySchema, 'passthrough');\n }\n\n #logRowOp(\n version: LexiVersion,\n pos: number,\n table: string,\n row: LiteRowKey,\n op: string,\n backfilled: string[] | undefined,\n ): string {\n const rowKey = stringify(normalizedKeyOrder(row));\n if (backfilled === undefined) {\n this.#logRowOpStmt.run({version, pos, table, rowKey, op});\n } else {\n const versions: Record<string, string> = {};\n for (const col of backfilled) {\n versions[col] = version;\n }\n this.#logRowOpWithBackfillStmt.run({\n version,\n pos,\n table,\n rowKey,\n op,\n backfillingColumnVersions: JSON.stringify(versions),\n });\n }\n return rowKey;\n }\n\n logTruncateOp(version: LexiVersion, table: string) {\n this.#logTableWideOpStmt.run({version, table, op: TRUNCATE_OP});\n }\n\n logResetOp(version: LexiVersion, table: string) {\n this.#logTableWideOpStmt.run({version, table, op: RESET_OP});\n }\n}\n"],"mappings":";;;;AAiDA,IAAa,0BAwBX;;;;;;;;;;;;;;;;;;AAwCF,SAAgB,oBACd,cACA,OACA,QACA,IACgB;CAChB,QAAQ,IAAR;EACE,KAAA;EACA,KAAA,KACE,OAAO;GAAC;GAAc;GAAO,QAAQ,YAAY,MAAM;GAAG;EAAE;EAC9D,KAAA;EACA,KAAA,KACE,OAAO;GAAC;GAAc;GAAO,QAAQ;GAAM;EAAE;EAC/C,SACE,MAAM,IAAI,MAAM,0BAA0B,GAAG,eAAe,MAAM,EAAE;CACxE;AACF;AAEA,SAAS,YAAY,QAAqD;CACxE,MAAM,MAAMA,QAAM,MAAM;CACxB,OACE,OAAO,QAAQ,YAAY,QAAQ,QAAQ,CAAC,MAAM,QAAQ,GAAG,SACvD,6BAA6B,QACrC;CACA,OAAO;AACT;AAEA,IAAM,0BAA0BC,eAAE,OAAO;CACvC,cAAcA,eAAE,OAAO;CACvB,OAAOA,eAAE,OAAO;CAChB,QAAQA,eAAE,OAAO;CACjB,IAAIC,aAAAA,KAAAA,KAAAA,KAAAA,GAAoD;CACxD,2BAA2BD,eACxB,OAAO,CAAC,CACR,KAAI,QAAOA,eAAE,OAAOA,eAAE,OAAO,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,GAAG,CAAC,CAAC;AAC3D,CAAC;AAID,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CACA;CAEA,YAAY,IAAc;EACxB,KAAK,gBAAgB,GAAG,QAAgB;;;;KAIvC;EAED,KAAK,4BAA4B,GAAG,QAAgB;;;;;;;;;;;KAWnD;EASD,KAAK,sBAAsB,GAAG,QAAgB;;;;KAI7C;EAGD,KAAK,gBAAgB,GAAG,QAAgB;;KAEvC;CACH;;;;;;;;;;;;CAaA,SACE,SACA,KACA,OACA,KACA,YACQ;EACR,OAAO,KAAK,UAAU,SAAS,KAAK,OAAO,KAAA,KAAa,UAAU;CACpE;CAEA,YACE,SACA,KACA,OACA,KACQ;EAIR,OAAO,KAAK,UAAU,SAAS,KAAK,OAAO,KAAA,KAAa,KAAA,CAAS;CACnE;CAEA,eAAe,OAAe,KAAiB;EAC7C,MAAM,SAAS,UAAU,mBAAmB,GAAG,CAAC;EAChD,MAAM,SAAS,KAAK,cAAc,IAAI,OAAO,MAAM;EACnD,OAAO,WAAW,KAAA,IACd,KAAA,IACAE,MAAQ,QAAQ,yBAAyB,aAAa;CAC5D;CAEA,UACE,SACA,KACA,OACA,KACA,IACA,YACQ;EACR,MAAM,SAAS,UAAU,mBAAmB,GAAG,CAAC;EAChD,IAAI,eAAe,KAAA,GACjB,KAAK,cAAc,IAAI;GAAC;GAAS;GAAK;GAAO;GAAQ;EAAE,CAAC;OACnD;GACL,MAAM,WAAmC,CAAC;GAC1C,KAAK,MAAM,OAAO,YAChB,SAAS,OAAO;GAElB,KAAK,0BAA0B,IAAI;IACjC;IACA;IACA;IACA;IACA;IACA,2BAA2B,KAAK,UAAU,QAAQ;GACpD,CAAC;EACH;EACA,OAAO;CACT;CAEA,cAAc,SAAsB,OAAe;EACjD,KAAK,oBAAoB,IAAI;GAAC;GAAS;GAAO,IAAA;EAAe,CAAC;CAChE;CAEA,WAAW,SAAsB,OAAe;EAC9C,KAAK,oBAAoB,IAAI;GAAC;GAAS;GAAO,IAAA;EAAY,CAAC;CAC7D;AACF"}
|
|
1
|
+
{"version":3,"file":"change-log.js","names":["parse","v","v.literalUnion","v.parse"],"sources":["../../../../../../../zero-cache/src/services/replicator/schema/change-log.ts"],"sourcesContent":["import {assert} from '../../../../../shared/src/asserts.ts';\nimport {\n parse,\n stringify,\n type JSONValue,\n} from '../../../../../shared/src/bigint-json.ts';\nimport * as v from '../../../../../shared/src/valita.ts';\nimport type {Database, Statement} from '../../../../../zqlite/src/db.ts';\nimport type {LexiVersion} from '../../../types/lexi-version.ts';\nimport type {LiteRowKey} from '../../../types/lite.ts';\nimport {normalizedKeyOrder} from '../../../types/row-key.ts';\n\n/**\n * The Change Log tracks the last operation (set or delete) for each row in the\n * data base, ordered by state version; in other words, a cross-table\n * index of row changes ordered by version. This facilitates a minimal \"diff\"\n * of row changes needed to advance a pipeline from one state version to another.\n *\n * The Change Log stores identifiers only, i.e. it does not store contents.\n * A database snapshot at the previous version can be used to query a row's\n * old contents, if any, and the current snapshot can be used to query a row's\n * new contents. (In the common case, the new contents will have just been applied\n * and thus has a high likelihood of being in the SQLite cache.)\n *\n * There are two table-wide operations:\n * - `t` corresponds to the postgres `TRUNCATE` operation\n * - `r` represents any schema (i.e. column) change\n *\n * For both operations, the corresponding row changes are not explicitly included\n * in the change log. The consumer has the option of simulating them be reading\n * from pre- and post- snapshots, or resetting their state entirely with the current\n * snapshot.\n *\n * To achieve the desired ordering semantics when processing tables that have been\n * truncated, reset, and modified, the \"rowKey\" is set to `null` for resets and\n * the empty string `\"\"` for truncates. This means that resets will be encountered\n * before truncates, which will be processed before any subsequent row changes.\n *\n * This ordering is chosen because resets are currently the more \"destructive\" op\n * and result in aborting the processing (and starting from scratch); doing this\n * earlier reduces wasted work.\n */\n\nexport const SET_OP = 's';\nexport const DEL_OP = 'd';\nexport const TRUNCATE_OP = 't';\nexport const RESET_OP = 'r';\n\n// Exported for testing (and migrations)\nexport const CREATE_CHANGELOG_SCHEMA =\n // stateVersion : a.k.a. row version\n // pos : order in which to process the change (within the version)\n // table : The table associated with the change\n // rowKey : JSON row key for a row change. For table-wide changes RESET\n // and TRUNCATE, there is no associated row; instead, `pos` is\n // set to -1 and the rowKey is set to the stateVersion,\n // guaranteeing when attempting to process the transaction,\n // the pipeline is reset (and the change log traversal\n // aborted).\n // op : 's' for set (insert/update)\n // : 'd' for delete\n // : 'r' for table reset (schema change)\n // : 't' for table truncation (which also resets the pipeline)\n // backfillingColumnVersions\n // : A JSON mapping from column name to stateVersion tracked\n // for replicated writes of columns that are being backfilled.\n // This is used to prevent backfill data, which is at a\n // fixed snapshot/version outside of the replication stream,\n // from overwriting newer column values.\n //\n // Naming note: To maintain compatibility between a new replication-manager\n // and old view-syncers, the previous _zero.changeLog table is preserved\n // and its replacement given a new name \"changeLog2\".\n `\n CREATE TABLE \"_zero.changeLog2\" (\n \"stateVersion\" TEXT NOT NULL,\n \"pos\" INT NOT NULL,\n \"table\" TEXT NOT NULL,\n \"rowKey\" TEXT NOT NULL,\n \"op\" TEXT NOT NULL,\n \"backfillingColumnVersions\" TEXT DEFAULT '{}',\n PRIMARY KEY(\"stateVersion\", \"pos\"),\n UNIQUE(\"table\", \"rowKey\")\n );\n `;\n\n/**\n * Contains the changeLog fields relevant for computing the diff between\n * two snapshots of a replica. The `pos` and `backfillingColumnVersions`\n * fields are excluded, though the query should be ordered by\n * `<stateVersion, pos>`.\n */\nexport type ChangeLogEntry =\n | {\n readonly stateVersion: string;\n readonly table: string;\n readonly rowKey: Readonly<Record<string, JSONValue>>;\n readonly op: typeof SET_OP | typeof DEL_OP;\n }\n | {\n readonly stateVersion: string;\n readonly table: string;\n // Table-wide ops have no row.\n readonly rowKey: null;\n readonly op: typeof TRUNCATE_OP | typeof RESET_OP;\n };\n\n/**\n * The {@link ChangeLogEntry} for the columns of a change log row. This does\n * not validate what zero-cache itself wrote: the columns are\n * `TEXT NOT NULL`, and `rowKey` is the JSON of an object (see\n * {@link ChangeLog}).\n */\nexport function parseChangeLogEntry(\n stateVersion: string,\n table: string,\n rowKey: string,\n op: string,\n): ChangeLogEntry {\n switch (op) {\n case SET_OP:\n case DEL_OP:\n return {stateVersion, table, rowKey: parseRowKey(rowKey), op};\n case TRUNCATE_OP:\n case RESET_OP:\n return {stateVersion, table, rowKey: null, op};\n default:\n throw new Error(`Invalid change log op \"${op}\" for table \"${table}\"`);\n }\n}\n\nfunction parseRowKey(rowKey: string): Readonly<Record<string, JSONValue>> {\n const key = parse(rowKey);\n assert(\n typeof key === 'object' && key !== null && !Array.isArray(key),\n () => `Invalid change log rowKey ${rowKey}`,\n );\n return key as Readonly<Record<string, JSONValue>>;\n}\n\nconst rawChangeLogEntrySchema = v.object({\n stateVersion: v.string(),\n table: v.string(),\n rowKey: v.string(),\n op: v.literalUnion(SET_OP, DEL_OP, TRUNCATE_OP, RESET_OP),\n backfillingColumnVersions: v\n .string()\n .map(val => v.record(v.string()).parse(JSON.parse(val))),\n});\n\nexport class ChangeLog {\n readonly #logRowOpStmt: Statement;\n readonly #logRowOpWithBackfillStmt: Statement;\n readonly #logTableWideOpStmt;\n readonly #getRowOpStmt: Statement;\n\n constructor(db: Database) {\n this.#logRowOpStmt = db.prepare(/*sql*/ `\n INSERT OR REPLACE INTO \"_zero.changeLog2\" \n (stateVersion, pos, \"table\", rowKey, op)\n VALUES (@version, @pos, @table, JSON(@rowKey), @op)\n `);\n\n this.#logRowOpWithBackfillStmt = db.prepare(/*sql*/ `\n INSERT INTO \"_zero.changeLog2\" \n (stateVersion, pos, \"table\", rowKey, op, backfillingColumnVersions)\n VALUES (@version, @pos, @table, JSON(@rowKey), @op, \n JSON(@backfillingColumnVersions))\n ON CONFLICT (\"table\", rowKey) DO UPDATE \n SET stateVersion = excluded.stateVersion,\n pos = excluded.pos,\n op = excluded.op,\n backfillingColumnVersions = json_patch(\n backfillingColumnVersions, excluded.backfillingColumnVersions)\n `);\n\n // Because table-wide ops result in aborting an incremental update\n // and rehydrating all queries at \"head\", they are assigned pos = -1\n // as an optimization to abort as early as possible to skip unnecessary\n // updates.\n //\n // However, changeLog entries that are destined to be \"skipped\" are\n // nonetheless kept for the purpose of tracking backfillingColumnVersions.\n this.#logTableWideOpStmt = db.prepare(/*sql*/ `\n INSERT OR REPLACE INTO \"_zero.changeLog2\" \n (stateVersion, pos, \"table\", rowKey, op) \n VALUES (@version, -1, @table, @version, @op)\n `);\n\n // oxlint-disable-next-line zero/no-select-star -- Local SQLite replica query; not run through pg prepared statements.\n this.#getRowOpStmt = db.prepare(/*sql*/ `\n SELECT * FROM \"_zero.changeLog2\" WHERE \"table\" = ? AND \"rowKey\" = JSON(?)\n `);\n }\n\n /**\n *\n * @param backfilled The backfilling columns for which values were set. Note\n * that an empty list and the `undefined` value mean different things;\n * * An empty list indicates that a backfill is in progress but no\n * backfilling values were set. In this case, existing\n * backfillingColumnVersions are preserved.\n * * `undefined` indicates that there are no columns being backfilled.\n * In this case, any vestigial `backfillingColumnVersions` value\n * is cleared.\n */\n logSetOp(\n version: LexiVersion,\n pos: number,\n table: string,\n row: LiteRowKey,\n backfilled: string[] | undefined,\n ): string {\n return this.#logRowOp(version, pos, table, row, SET_OP, backfilled);\n }\n\n logDeleteOp(\n version: LexiVersion,\n pos: number,\n table: string,\n row: LiteRowKey,\n ): string {\n // Note: For delete ops, it is always safe to clear the\n // backfillingColumnVersions because the backfill algorithm\n // understands that deletes apply to the whole row.\n return this.#logRowOp(version, pos, table, row, DEL_OP, undefined);\n }\n\n getLatestRowOp(table: string, row: LiteRowKey) {\n const rowKey = stringify(normalizedKeyOrder(row));\n const result = this.#getRowOpStmt.get(table, rowKey);\n return result === undefined\n ? undefined\n : v.parse(result, rawChangeLogEntrySchema, 'passthrough');\n }\n\n #logRowOp(\n version: LexiVersion,\n pos: number,\n table: string,\n row: LiteRowKey,\n op: string,\n backfilled: string[] | undefined,\n ): string {\n const rowKey = stringify(normalizedKeyOrder(row));\n if (backfilled === undefined) {\n this.#logRowOpStmt.run({version, pos, table, rowKey, op});\n } else {\n const versions: Record<string, string> = {};\n for (const col of backfilled) {\n versions[col] = version;\n }\n this.#logRowOpWithBackfillStmt.run({\n version,\n pos,\n table,\n rowKey,\n op,\n backfillingColumnVersions: JSON.stringify(versions),\n });\n }\n return rowKey;\n }\n\n logTruncateOp(version: LexiVersion, table: string) {\n this.#logTableWideOpStmt.run({version, table, op: TRUNCATE_OP});\n }\n\n logResetOp(version: LexiVersion, table: string) {\n this.#logTableWideOpStmt.run({version, table, op: RESET_OP});\n }\n}\n"],"mappings":";;;;AAiDA,IAAa,0BAwBX;;;;;;;;;;;;;;;;;;AAwCF,SAAgB,oBACd,cACA,OACA,QACA,IACgB;CAChB,QAAQ,IAAR;EACE,KAAA;EACA,KAAA,KACE,OAAO;GAAC;GAAc;GAAO,QAAQ,YAAY,MAAM;GAAG;EAAE;EAC9D,KAAA;EACA,KAAA,KACE,OAAO;GAAC;GAAc;GAAO,QAAQ;GAAM;EAAE;EAC/C,SACE,MAAM,IAAI,MAAM,0BAA0B,GAAG,eAAe,MAAM,EAAE;CACxE;AACF;AAEA,SAAS,YAAY,QAAqD;CACxE,MAAM,MAAMA,QAAM,MAAM;CACxB,OACE,OAAO,QAAQ,YAAY,QAAQ,QAAQ,CAAC,MAAM,QAAQ,GAAG,SACvD,6BAA6B,QACrC;CACA,OAAO;AACT;AAEA,IAAM,0BAA0BC,eAAE,OAAO;CACvC,cAAcA,eAAE,OAAO;CACvB,OAAOA,eAAE,OAAO;CAChB,QAAQA,eAAE,OAAO;CACjB,IAAIC,aAAAA,KAAAA,KAAAA,KAAAA,GAAoD;CACxD,2BAA2BD,eACxB,OAAO,CAAC,CACR,KAAI,QAAOA,eAAE,OAAOA,eAAE,OAAO,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,GAAG,CAAC,CAAC;AAC3D,CAAC;AAED,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CACA;CAEA,YAAY,IAAc;EACxB,KAAK,gBAAgB,GAAG,QAAgB;;;;KAIvC;EAED,KAAK,4BAA4B,GAAG,QAAgB;;;;;;;;;;;KAWnD;EASD,KAAK,sBAAsB,GAAG,QAAgB;;;;KAI7C;EAGD,KAAK,gBAAgB,GAAG,QAAgB;;KAEvC;CACH;;;;;;;;;;;;CAaA,SACE,SACA,KACA,OACA,KACA,YACQ;EACR,OAAO,KAAK,UAAU,SAAS,KAAK,OAAO,KAAA,KAAa,UAAU;CACpE;CAEA,YACE,SACA,KACA,OACA,KACQ;EAIR,OAAO,KAAK,UAAU,SAAS,KAAK,OAAO,KAAA,KAAa,KAAA,CAAS;CACnE;CAEA,eAAe,OAAe,KAAiB;EAC7C,MAAM,SAAS,UAAU,mBAAmB,GAAG,CAAC;EAChD,MAAM,SAAS,KAAK,cAAc,IAAI,OAAO,MAAM;EACnD,OAAO,WAAW,KAAA,IACd,KAAA,IACAE,MAAQ,QAAQ,yBAAyB,aAAa;CAC5D;CAEA,UACE,SACA,KACA,OACA,KACA,IACA,YACQ;EACR,MAAM,SAAS,UAAU,mBAAmB,GAAG,CAAC;EAChD,IAAI,eAAe,KAAA,GACjB,KAAK,cAAc,IAAI;GAAC;GAAS;GAAK;GAAO;GAAQ;EAAE,CAAC;OACnD;GACL,MAAM,WAAmC,CAAC;GAC1C,KAAK,MAAM,OAAO,YAChB,SAAS,OAAO;GAElB,KAAK,0BAA0B,IAAI;IACjC;IACA;IACA;IACA;IACA;IACA,2BAA2B,KAAK,UAAU,QAAQ;GACpD,CAAC;EACH;EACA,OAAO;CACT;CAEA,cAAc,SAAsB,OAAe;EACjD,KAAK,oBAAoB,IAAI;GAAC;GAAS;GAAO,IAAA;EAAe,CAAC;CAChE;CAEA,WAAW,SAAsB,OAAe;EAC9C,KAAK,oBAAoB,IAAI;GAAC;GAAS;GAAO,IAAA;EAAY,CAAC;CAC7D;AACF"}
|
|
@@ -65,11 +65,4 @@ export declare function liteTypeStringToMetadata(liteTypeString: string, charact
|
|
|
65
65
|
* This is a compatibility helper for the migration period.
|
|
66
66
|
*/
|
|
67
67
|
export declare function metadataToLiteTypeString(metadata: ColumnMetadata): string;
|
|
68
|
-
/**
|
|
69
|
-
* Converts PostgreSQL ColumnSpec to structured ColumnMetadata.
|
|
70
|
-
* Used during replication to populate the metadata table from upstream schema.
|
|
71
|
-
*
|
|
72
|
-
* Uses the same logic as liteTypeString() and mapPostgresToLiteColumn() via shared helpers.
|
|
73
|
-
*/
|
|
74
|
-
export declare function pgColumnSpecToMetadata(spec: ColumnSpec): ColumnMetadata;
|
|
75
68
|
//# sourceMappingURL=column-metadata.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"column-metadata.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/replicator/schema/column-metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,iCAAiC,CAAC;AAEzE,OAAO,KAAK,EAAC,UAAU,EAAE,aAAa,EAAC,MAAM,sBAAsB,CAAC;AAQpE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,yCAAyC,CAAC;AAExE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,sDAAsD;IACtD,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,aAAa,EAAE,OAAO,CAAC;CACxB;AAWD,eAAO,MAAM,4BAA4B,8VAYxC,CAAC;AAEF;;;;;;GAMG;AACH,qBAAa,mBAAmB;;IAa9B,OAAO,eAyDN;IAED;;;OAGG;IACH,MAAM,CAAC,WAAW,CAAC,EAAE,EAAE,QAAQ,GAAG,mBAAmB,GAAG,SAAS,CAkBhE;IAED,MAAM,CACJ,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,UAAU,EAChB,QAAQ,CAAC,EAAE,UAAU,GACpB,IAAI,CAGN;IAoBD,MAAM,CACJ,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,UAAU,GACf,IAAI,CAYN;IAED,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAE5D;IAED,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAExD;IAED,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEnC;IAED,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAE5D;IAED,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAiB3E;IAED,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAkBvD;IAED,QAAQ,IAAI,OAAO,CAGlB;CACF;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,aAAa,EAAE,GACtB,IAAI,CA0BN;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,cAAc,EAAE,MAAM,EACtB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,GACjC,cAAc,CAkBhB;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAOzE
|
|
1
|
+
{"version":3,"file":"column-metadata.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/replicator/schema/column-metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,iCAAiC,CAAC;AAEzE,OAAO,KAAK,EAAC,UAAU,EAAE,aAAa,EAAC,MAAM,sBAAsB,CAAC;AAQpE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,yCAAyC,CAAC;AAExE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,sDAAsD;IACtD,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,aAAa,EAAE,OAAO,CAAC;CACxB;AAWD,eAAO,MAAM,4BAA4B,8VAYxC,CAAC;AAEF;;;;;;GAMG;AACH,qBAAa,mBAAmB;;IAa9B,OAAO,eAyDN;IAED;;;OAGG;IACH,MAAM,CAAC,WAAW,CAAC,EAAE,EAAE,QAAQ,GAAG,mBAAmB,GAAG,SAAS,CAkBhE;IAED,MAAM,CACJ,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,UAAU,EAChB,QAAQ,CAAC,EAAE,UAAU,GACpB,IAAI,CAGN;IAoBD,MAAM,CACJ,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,UAAU,GACf,IAAI,CAYN;IAED,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAE5D;IAED,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAExD;IAED,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEnC;IAED,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAE5D;IAED,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAiB3E;IAED,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAkBvD;IAED,QAAQ,IAAI,OAAO,CAGlB;CACF;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,aAAa,EAAE,GACtB,IAAI,CA0BN;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,cAAc,EAAE,MAAM,EACtB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,GACjC,cAAc,CAkBhB;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAOzE"}
|
|
@@ -202,6 +202,6 @@ function pgColumnSpecToMetadata(spec) {
|
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
204
|
//#endregion
|
|
205
|
-
export { CREATE_COLUMN_METADATA_TABLE, ColumnMetadataStore, liteTypeStringToMetadata, metadataToLiteTypeString,
|
|
205
|
+
export { CREATE_COLUMN_METADATA_TABLE, ColumnMetadataStore, liteTypeStringToMetadata, metadataToLiteTypeString, populateFromExistingTables };
|
|
206
206
|
|
|
207
207
|
//# sourceMappingURL=column-metadata.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"column-metadata.js","names":["checkIsArray","checkIsEnum"],"sources":["../../../../../../../zero-cache/src/services/replicator/schema/column-metadata.ts"],"sourcesContent":["/**\n * Column metadata table for storing upstream PostgreSQL schema information.\n *\n * Previously, upstream type metadata was embedded in SQLite column type strings\n * using pipe-delimited notation (e.g., \"int8|NOT_NULL|TEXT_ENUM\"). This caused\n * issues with SQLite type affinity and made schema inspection difficult.\n *\n * This table stores that metadata separately, allowing SQLite columns to use\n * plain type names while preserving all necessary upstream type information.\n */\n\nimport type {Database, Statement} from '../../../../../zqlite/src/db.ts';\nimport {isArrayColumn, isEnumColumn} from '../../../db/pg-to-lite.ts';\nimport type {ColumnSpec, LiteTableSpec} from '../../../db/specs.ts';\nimport {\n isArray as checkIsArray,\n isEnum as checkIsEnum,\n liteTypeString,\n nullableUpstream,\n upstreamDataType,\n} from '../../../types/lite.ts';\nimport type {BackfillID} from '../../change-source/protocol/current.ts';\n\n/**\n * Structured column metadata, replacing the old pipe-delimited string format.\n */\nexport interface ColumnMetadata {\n /** PostgreSQL type name, e.g., 'int8', 'varchar', 'text[]', 'user_role' */\n upstreamType: string;\n isNotNull: boolean;\n isEnum: boolean;\n isArray: boolean;\n /** Maximum character length for varchar/char types */\n characterMaxLength?: number | null;\n isBackfilling: boolean;\n}\n\ntype ColumnMetadataRow = {\n upstream_type: string;\n is_not_null: number;\n is_enum: number;\n is_array: number;\n character_max_length: number | null;\n backfill: string | null;\n};\n\nexport const CREATE_COLUMN_METADATA_TABLE = `\n CREATE TABLE \"_zero.column_metadata\" (\n table_name TEXT NOT NULL,\n column_name TEXT NOT NULL,\n upstream_type TEXT NOT NULL,\n is_not_null INTEGER NOT NULL,\n is_enum INTEGER NOT NULL,\n is_array INTEGER NOT NULL,\n character_max_length INTEGER,\n backfill TEXT,\n PRIMARY KEY (table_name, column_name)\n );\n`;\n\n/**\n * Efficient column metadata store that prepares all statements upfront.\n * Use this class to avoid re-preparing statements on every operation.\n *\n * Access via `ColumnMetadataStore.getInstance(db)`, which returns `undefined`\n * if the metadata table doesn't exist yet.\n */\nexport class ColumnMetadataStore {\n static #instances = new WeakMap<Database, ColumnMetadataStore>();\n\n readonly #insertStmt: Statement;\n readonly #updateStmt: Statement;\n readonly #clearBackfillStmt: Statement;\n readonly #deleteColumnStmt: Statement;\n readonly #deleteTableStmt: Statement;\n readonly #renameTableStmt: Statement;\n readonly #getColumnStmt: Statement;\n readonly #getTableStmt: Statement;\n readonly #hasTableStmt: Statement;\n\n private constructor(db: Database) {\n this.#insertStmt = db.prepare(`\n INSERT INTO \"_zero.column_metadata\"\n (table_name, column_name, upstream_type, is_not_null, is_enum, is_array, character_max_length, backfill)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n\n this.#updateStmt = db.prepare(`\n UPDATE \"_zero.column_metadata\"\n SET column_name = ?,\n upstream_type = ?,\n is_not_null = ?,\n is_enum = ?,\n is_array = ?,\n character_max_length = ?\n WHERE table_name = ? AND column_name = ?\n `);\n\n this.#clearBackfillStmt = db.prepare(/*sql*/ `\n UPDATE \"_zero.column_metadata\"\n SET backfill = NULL\n WHERE table_name = ? AND column_name = ?\n `);\n\n this.#deleteColumnStmt = db.prepare(`\n DELETE FROM \"_zero.column_metadata\"\n WHERE table_name = ? AND column_name = ?\n `);\n\n this.#deleteTableStmt = db.prepare(`\n DELETE FROM \"_zero.column_metadata\"\n WHERE table_name = ?\n `);\n\n this.#renameTableStmt = db.prepare(`\n UPDATE \"_zero.column_metadata\"\n SET table_name = ?\n WHERE table_name = ?\n `);\n\n this.#getColumnStmt = db.prepare(`\n SELECT upstream_type, is_not_null, is_enum, is_array, character_max_length, backfill\n FROM \"_zero.column_metadata\"\n WHERE table_name = ? AND column_name = ?\n `);\n\n this.#getTableStmt = db.prepare(`\n SELECT column_name, upstream_type, is_not_null, is_enum, is_array, character_max_length, backfill\n FROM \"_zero.column_metadata\"\n WHERE table_name = ?\n ORDER BY column_name\n `);\n\n this.#hasTableStmt = db.prepare(`\n SELECT 1 FROM sqlite_master\n WHERE type = 'table' AND name = '_zero.column_metadata'\n `);\n }\n\n /**\n * Gets the singleton instance of ColumnMetadataStore for the given database.\n * Returns `undefined` if the metadata table doesn't exist yet.\n */\n static getInstance(db: Database): ColumnMetadataStore | undefined {\n let instance = ColumnMetadataStore.#instances.get(db);\n if (!instance) {\n // Check if table exists\n const tableExists = db\n .prepare(\n `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_zero.column_metadata'`,\n )\n .get();\n\n if (!tableExists) {\n return undefined;\n }\n\n instance = new ColumnMetadataStore(db);\n ColumnMetadataStore.#instances.set(db, instance);\n }\n return instance;\n }\n\n insert(\n tableName: string,\n columnName: string,\n spec: ColumnSpec,\n backfill?: BackfillID,\n ): void {\n const metadata = pgColumnSpecToMetadata(spec);\n this.#insertMetadata(tableName, columnName, metadata, backfill);\n }\n\n #insertMetadata(\n tableName: string,\n columnName: string,\n metadata: Omit<ColumnMetadata, 'isBackfilling'>,\n backfill?: BackfillID,\n ): void {\n this.#insertStmt.run(\n tableName,\n columnName,\n metadata.upstreamType,\n metadata.isNotNull ? 1 : 0,\n metadata.isEnum ? 1 : 0,\n metadata.isArray ? 1 : 0,\n metadata.characterMaxLength ?? null,\n backfill ? JSON.stringify(backfill) : null,\n );\n }\n\n update(\n tableName: string,\n oldColumnName: string,\n newColumnName: string,\n spec: ColumnSpec,\n ): void {\n const metadata = pgColumnSpecToMetadata(spec);\n this.#updateStmt.run(\n newColumnName,\n metadata.upstreamType,\n metadata.isNotNull ? 1 : 0,\n metadata.isEnum ? 1 : 0,\n metadata.isArray ? 1 : 0,\n metadata.characterMaxLength ?? null,\n tableName,\n oldColumnName,\n );\n }\n\n clearBackfilling(tableName: string, columnName: string): void {\n this.#clearBackfillStmt.run(tableName, columnName);\n }\n\n deleteColumn(tableName: string, columnName: string): void {\n this.#deleteColumnStmt.run(tableName, columnName);\n }\n\n deleteTable(tableName: string): void {\n this.#deleteTableStmt.run(tableName);\n }\n\n renameTable(oldTableName: string, newTableName: string): void {\n this.#renameTableStmt.run(newTableName, oldTableName);\n }\n\n getColumn(tableName: string, columnName: string): ColumnMetadata | undefined {\n const row = this.#getColumnStmt.get(tableName, columnName) as\n | ColumnMetadataRow\n | undefined;\n\n if (!row) {\n return undefined;\n }\n\n return {\n upstreamType: row.upstream_type,\n isNotNull: row.is_not_null !== 0,\n isEnum: row.is_enum !== 0,\n isArray: row.is_array !== 0,\n characterMaxLength: row.character_max_length,\n isBackfilling: row.backfill !== null,\n };\n }\n\n getTable(tableName: string): Map<string, ColumnMetadata> {\n const rows = this.#getTableStmt.all(tableName) as Array<\n ColumnMetadataRow & {column_name: string}\n >;\n\n const metadata = new Map<string, ColumnMetadata>();\n for (const row of rows) {\n metadata.set(row.column_name, {\n upstreamType: row.upstream_type,\n isNotNull: row.is_not_null !== 0,\n isEnum: row.is_enum !== 0,\n isArray: row.is_array !== 0,\n characterMaxLength: row.character_max_length,\n isBackfilling: row.backfill !== null,\n });\n }\n\n return metadata;\n }\n\n hasTable(): boolean {\n const result = this.#hasTableStmt.get();\n return result !== undefined;\n }\n}\n\n/**\n * Populates metadata table from existing tables that use pipe notation.\n * This is used during migration v8 to backfill the metadata table.\n */\nexport function populateFromExistingTables(\n db: Database,\n tables: LiteTableSpec[],\n): void {\n // The backfill column is not relevant here, and does not exist on\n // older versions of the replica.\n const legacyInsertStmt = db.prepare(`\n INSERT INTO \"_zero.column_metadata\"\n (table_name, column_name, upstream_type, is_not_null, is_enum, is_array, character_max_length)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n\n for (const table of tables) {\n for (const [columnName, columnSpec] of Object.entries(table.columns)) {\n const metadata = liteTypeStringToMetadata(\n columnSpec.dataType,\n columnSpec.characterMaximumLength,\n );\n legacyInsertStmt.run(\n table.name,\n columnName,\n metadata.upstreamType,\n metadata.isNotNull ? 1 : 0,\n metadata.isEnum ? 1 : 0,\n metadata.isArray ? 1 : 0,\n metadata.characterMaxLength ?? null,\n );\n }\n }\n}\n\n/**\n * Converts pipe-delimited LiteTypeString to structured ColumnMetadata.\n * This is a compatibility helper for the migration period.\n */\nexport function liteTypeStringToMetadata(\n liteTypeString: string,\n characterMaxLength?: number | null,\n): ColumnMetadata {\n const baseType = upstreamDataType(liteTypeString);\n const isArrayType = checkIsArray(liteTypeString);\n\n // Reconstruct the full upstream type including array notation\n // For new-style arrays like 'text[]', upstreamDataType returns 'text[]'\n // For old-style arrays like 'int4|NOT_NULL[]', upstreamDataType returns 'int4', so we append '[]'\n const fullUpstreamType =\n isArrayType && !baseType.includes('[]') ? `${baseType}[]` : baseType;\n\n return {\n upstreamType: fullUpstreamType,\n isNotNull: !nullableUpstream(liteTypeString),\n isEnum: checkIsEnum(liteTypeString),\n isArray: isArrayType,\n characterMaxLength: characterMaxLength ?? null,\n isBackfilling: false,\n };\n}\n\n/**\n * Converts structured ColumnMetadata back to pipe-delimited LiteTypeString.\n * This is a compatibility helper for the migration period.\n */\nexport function metadataToLiteTypeString(metadata: ColumnMetadata): string {\n return liteTypeString(\n metadata.upstreamType,\n metadata.isNotNull,\n metadata.isEnum,\n metadata.isArray,\n );\n}\n\n/**\n * Converts PostgreSQL ColumnSpec to structured ColumnMetadata.\n * Used during replication to populate the metadata table from upstream schema.\n *\n * Uses the same logic as liteTypeString() and mapPostgresToLiteColumn() via shared helpers.\n */\nexport function pgColumnSpecToMetadata(spec: ColumnSpec): ColumnMetadata {\n return {\n upstreamType: spec.dataType,\n isNotNull: spec.notNull ?? false,\n isEnum: isEnumColumn(spec),\n isArray: isArrayColumn(spec),\n characterMaxLength: spec.characterMaximumLength ?? null,\n isBackfilling: false,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"column-metadata.js","names":["checkIsArray","checkIsEnum"],"sources":["../../../../../../../zero-cache/src/services/replicator/schema/column-metadata.ts"],"sourcesContent":["/**\n * Column metadata table for storing upstream PostgreSQL schema information.\n *\n * Previously, upstream type metadata was embedded in SQLite column type strings\n * using pipe-delimited notation (e.g., \"int8|NOT_NULL|TEXT_ENUM\"). This caused\n * issues with SQLite type affinity and made schema inspection difficult.\n *\n * This table stores that metadata separately, allowing SQLite columns to use\n * plain type names while preserving all necessary upstream type information.\n */\n\nimport type {Database, Statement} from '../../../../../zqlite/src/db.ts';\nimport {isArrayColumn, isEnumColumn} from '../../../db/pg-to-lite.ts';\nimport type {ColumnSpec, LiteTableSpec} from '../../../db/specs.ts';\nimport {\n isArray as checkIsArray,\n isEnum as checkIsEnum,\n liteTypeString,\n nullableUpstream,\n upstreamDataType,\n} from '../../../types/lite.ts';\nimport type {BackfillID} from '../../change-source/protocol/current.ts';\n\n/**\n * Structured column metadata, replacing the old pipe-delimited string format.\n */\nexport interface ColumnMetadata {\n /** PostgreSQL type name, e.g., 'int8', 'varchar', 'text[]', 'user_role' */\n upstreamType: string;\n isNotNull: boolean;\n isEnum: boolean;\n isArray: boolean;\n /** Maximum character length for varchar/char types */\n characterMaxLength?: number | null;\n isBackfilling: boolean;\n}\n\ntype ColumnMetadataRow = {\n upstream_type: string;\n is_not_null: number;\n is_enum: number;\n is_array: number;\n character_max_length: number | null;\n backfill: string | null;\n};\n\nexport const CREATE_COLUMN_METADATA_TABLE = `\n CREATE TABLE \"_zero.column_metadata\" (\n table_name TEXT NOT NULL,\n column_name TEXT NOT NULL,\n upstream_type TEXT NOT NULL,\n is_not_null INTEGER NOT NULL,\n is_enum INTEGER NOT NULL,\n is_array INTEGER NOT NULL,\n character_max_length INTEGER,\n backfill TEXT,\n PRIMARY KEY (table_name, column_name)\n );\n`;\n\n/**\n * Efficient column metadata store that prepares all statements upfront.\n * Use this class to avoid re-preparing statements on every operation.\n *\n * Access via `ColumnMetadataStore.getInstance(db)`, which returns `undefined`\n * if the metadata table doesn't exist yet.\n */\nexport class ColumnMetadataStore {\n static #instances = new WeakMap<Database, ColumnMetadataStore>();\n\n readonly #insertStmt: Statement;\n readonly #updateStmt: Statement;\n readonly #clearBackfillStmt: Statement;\n readonly #deleteColumnStmt: Statement;\n readonly #deleteTableStmt: Statement;\n readonly #renameTableStmt: Statement;\n readonly #getColumnStmt: Statement;\n readonly #getTableStmt: Statement;\n readonly #hasTableStmt: Statement;\n\n private constructor(db: Database) {\n this.#insertStmt = db.prepare(`\n INSERT INTO \"_zero.column_metadata\"\n (table_name, column_name, upstream_type, is_not_null, is_enum, is_array, character_max_length, backfill)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n\n this.#updateStmt = db.prepare(`\n UPDATE \"_zero.column_metadata\"\n SET column_name = ?,\n upstream_type = ?,\n is_not_null = ?,\n is_enum = ?,\n is_array = ?,\n character_max_length = ?\n WHERE table_name = ? AND column_name = ?\n `);\n\n this.#clearBackfillStmt = db.prepare(/*sql*/ `\n UPDATE \"_zero.column_metadata\"\n SET backfill = NULL\n WHERE table_name = ? AND column_name = ?\n `);\n\n this.#deleteColumnStmt = db.prepare(`\n DELETE FROM \"_zero.column_metadata\"\n WHERE table_name = ? AND column_name = ?\n `);\n\n this.#deleteTableStmt = db.prepare(`\n DELETE FROM \"_zero.column_metadata\"\n WHERE table_name = ?\n `);\n\n this.#renameTableStmt = db.prepare(`\n UPDATE \"_zero.column_metadata\"\n SET table_name = ?\n WHERE table_name = ?\n `);\n\n this.#getColumnStmt = db.prepare(`\n SELECT upstream_type, is_not_null, is_enum, is_array, character_max_length, backfill\n FROM \"_zero.column_metadata\"\n WHERE table_name = ? AND column_name = ?\n `);\n\n this.#getTableStmt = db.prepare(`\n SELECT column_name, upstream_type, is_not_null, is_enum, is_array, character_max_length, backfill\n FROM \"_zero.column_metadata\"\n WHERE table_name = ?\n ORDER BY column_name\n `);\n\n this.#hasTableStmt = db.prepare(`\n SELECT 1 FROM sqlite_master\n WHERE type = 'table' AND name = '_zero.column_metadata'\n `);\n }\n\n /**\n * Gets the singleton instance of ColumnMetadataStore for the given database.\n * Returns `undefined` if the metadata table doesn't exist yet.\n */\n static getInstance(db: Database): ColumnMetadataStore | undefined {\n let instance = ColumnMetadataStore.#instances.get(db);\n if (!instance) {\n // Check if table exists\n const tableExists = db\n .prepare(\n `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_zero.column_metadata'`,\n )\n .get();\n\n if (!tableExists) {\n return undefined;\n }\n\n instance = new ColumnMetadataStore(db);\n ColumnMetadataStore.#instances.set(db, instance);\n }\n return instance;\n }\n\n insert(\n tableName: string,\n columnName: string,\n spec: ColumnSpec,\n backfill?: BackfillID,\n ): void {\n const metadata = pgColumnSpecToMetadata(spec);\n this.#insertMetadata(tableName, columnName, metadata, backfill);\n }\n\n #insertMetadata(\n tableName: string,\n columnName: string,\n metadata: Omit<ColumnMetadata, 'isBackfilling'>,\n backfill?: BackfillID,\n ): void {\n this.#insertStmt.run(\n tableName,\n columnName,\n metadata.upstreamType,\n metadata.isNotNull ? 1 : 0,\n metadata.isEnum ? 1 : 0,\n metadata.isArray ? 1 : 0,\n metadata.characterMaxLength ?? null,\n backfill ? JSON.stringify(backfill) : null,\n );\n }\n\n update(\n tableName: string,\n oldColumnName: string,\n newColumnName: string,\n spec: ColumnSpec,\n ): void {\n const metadata = pgColumnSpecToMetadata(spec);\n this.#updateStmt.run(\n newColumnName,\n metadata.upstreamType,\n metadata.isNotNull ? 1 : 0,\n metadata.isEnum ? 1 : 0,\n metadata.isArray ? 1 : 0,\n metadata.characterMaxLength ?? null,\n tableName,\n oldColumnName,\n );\n }\n\n clearBackfilling(tableName: string, columnName: string): void {\n this.#clearBackfillStmt.run(tableName, columnName);\n }\n\n deleteColumn(tableName: string, columnName: string): void {\n this.#deleteColumnStmt.run(tableName, columnName);\n }\n\n deleteTable(tableName: string): void {\n this.#deleteTableStmt.run(tableName);\n }\n\n renameTable(oldTableName: string, newTableName: string): void {\n this.#renameTableStmt.run(newTableName, oldTableName);\n }\n\n getColumn(tableName: string, columnName: string): ColumnMetadata | undefined {\n const row = this.#getColumnStmt.get(tableName, columnName) as\n | ColumnMetadataRow\n | undefined;\n\n if (!row) {\n return undefined;\n }\n\n return {\n upstreamType: row.upstream_type,\n isNotNull: row.is_not_null !== 0,\n isEnum: row.is_enum !== 0,\n isArray: row.is_array !== 0,\n characterMaxLength: row.character_max_length,\n isBackfilling: row.backfill !== null,\n };\n }\n\n getTable(tableName: string): Map<string, ColumnMetadata> {\n const rows = this.#getTableStmt.all(tableName) as Array<\n ColumnMetadataRow & {column_name: string}\n >;\n\n const metadata = new Map<string, ColumnMetadata>();\n for (const row of rows) {\n metadata.set(row.column_name, {\n upstreamType: row.upstream_type,\n isNotNull: row.is_not_null !== 0,\n isEnum: row.is_enum !== 0,\n isArray: row.is_array !== 0,\n characterMaxLength: row.character_max_length,\n isBackfilling: row.backfill !== null,\n });\n }\n\n return metadata;\n }\n\n hasTable(): boolean {\n const result = this.#hasTableStmt.get();\n return result !== undefined;\n }\n}\n\n/**\n * Populates metadata table from existing tables that use pipe notation.\n * This is used during migration v8 to backfill the metadata table.\n */\nexport function populateFromExistingTables(\n db: Database,\n tables: LiteTableSpec[],\n): void {\n // The backfill column is not relevant here, and does not exist on\n // older versions of the replica.\n const legacyInsertStmt = db.prepare(`\n INSERT INTO \"_zero.column_metadata\"\n (table_name, column_name, upstream_type, is_not_null, is_enum, is_array, character_max_length)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n\n for (const table of tables) {\n for (const [columnName, columnSpec] of Object.entries(table.columns)) {\n const metadata = liteTypeStringToMetadata(\n columnSpec.dataType,\n columnSpec.characterMaximumLength,\n );\n legacyInsertStmt.run(\n table.name,\n columnName,\n metadata.upstreamType,\n metadata.isNotNull ? 1 : 0,\n metadata.isEnum ? 1 : 0,\n metadata.isArray ? 1 : 0,\n metadata.characterMaxLength ?? null,\n );\n }\n }\n}\n\n/**\n * Converts pipe-delimited LiteTypeString to structured ColumnMetadata.\n * This is a compatibility helper for the migration period.\n */\nexport function liteTypeStringToMetadata(\n liteTypeString: string,\n characterMaxLength?: number | null,\n): ColumnMetadata {\n const baseType = upstreamDataType(liteTypeString);\n const isArrayType = checkIsArray(liteTypeString);\n\n // Reconstruct the full upstream type including array notation\n // For new-style arrays like 'text[]', upstreamDataType returns 'text[]'\n // For old-style arrays like 'int4|NOT_NULL[]', upstreamDataType returns 'int4', so we append '[]'\n const fullUpstreamType =\n isArrayType && !baseType.includes('[]') ? `${baseType}[]` : baseType;\n\n return {\n upstreamType: fullUpstreamType,\n isNotNull: !nullableUpstream(liteTypeString),\n isEnum: checkIsEnum(liteTypeString),\n isArray: isArrayType,\n characterMaxLength: characterMaxLength ?? null,\n isBackfilling: false,\n };\n}\n\n/**\n * Converts structured ColumnMetadata back to pipe-delimited LiteTypeString.\n * This is a compatibility helper for the migration period.\n */\nexport function metadataToLiteTypeString(metadata: ColumnMetadata): string {\n return liteTypeString(\n metadata.upstreamType,\n metadata.isNotNull,\n metadata.isEnum,\n metadata.isArray,\n );\n}\n\n/**\n * Converts PostgreSQL ColumnSpec to structured ColumnMetadata.\n * Used during replication to populate the metadata table from upstream schema.\n *\n * Uses the same logic as liteTypeString() and mapPostgresToLiteColumn() via shared helpers.\n */\nfunction pgColumnSpecToMetadata(spec: ColumnSpec): ColumnMetadata {\n return {\n upstreamType: spec.dataType,\n isNotNull: spec.notNull ?? false,\n isEnum: isEnumColumn(spec),\n isArray: isArrayColumn(spec),\n characterMaxLength: spec.characterMaximumLength ?? null,\n isBackfilling: false,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { assert } from "../../../../shared/src/asserts.js";
|
|
2
|
+
import { resolver } from "../../../../shared/src/resolver.js";
|
|
2
3
|
import { WRITE_WORKER_URL } from "../../server/worker-urls.js";
|
|
3
|
-
import { resolver } from "@rocicorp/resolver";
|
|
4
4
|
import { Worker } from "node:worker_threads";
|
|
5
5
|
//#region ../zero-cache/src/services/replicator/write-worker-client.ts
|
|
6
6
|
function serializeError(err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-worker-client.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/write-worker-client.ts"],"sourcesContent":["import {Worker} from 'node:worker_threads';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport type {LogConfig} from '../../../../shared/src/logging.ts';\nimport type {Database} from '../../../../zqlite/src/db.ts';\nimport {WRITE_WORKER_URL} from '../../server/worker-urls.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport type {BackfillRequest} from '../change-source/protocol/current/upstream.ts';\nimport type {ChangeProcessorMode, CommitResult} from './change-processor.ts';\nimport type {SubscriptionState} from './schema/replication-state.ts';\n\nexport type PragmaConfig = {\n busyTimeout: number;\n analysisLimit: number;\n walAutocheckpoint?: number | undefined;\n};\n\n/**\n * Enables write-path checkpoint backpressure in the write worker. At each\n * commit boundary, if the WAL has grown to at least `checkpointThresholdPages`,\n * the worker forces an immediate litestream sync (sealing the WAL to LTX and\n * running litestream's checkpoint).\n *\n * The optional `walMaxPages` is an emergency break that pauses until the WAL drains,\n * rather than letting it exceed available disk space. This replaces litestream's\n * analogous `truncate-page-n` emergency break, which is more disruptive because of\n * https://github.com/benbjohnson/litestream/issues/1332. Note, however, this should\n * never been needed if non-initial snapshots are disabled (which is the default).\n *\n * Set only for the backup replicator when replicating with litestream v5; `null`\n * disables the feature entirely (no per-commit overhead).\n */\nexport type ForceCheckpointConfig = {\n checkpointThresholdPages: number;\n maxWalPages?: number | undefined;\n};\n\ntype ErrorHandler = (err: Error) => void;\n\n/**\n * Interface for a write worker that processes replication messages.\n */\n/**\n * The replica's {@link SubscriptionState}, along with its pending backfills,\n * read in the same snapshot.\n */\nexport type SubscriptionStateWithBackfills = SubscriptionState & {\n backfills: BackfillRequest[];\n};\n\nexport interface WriteWorkerClient {\n getSubscriptionState(): Promise<SubscriptionStateWithBackfills>;\n processMessages(\n downstream: readonly ChangeStreamData[],\n ): Promise<CommitResult | null>;\n abort(): void;\n stop(): Promise<void>;\n onError(handler: ErrorHandler): void;\n}\n\nexport type SerializedError = {\n name: string;\n message: string;\n stack?: string | undefined;\n cause?: SerializedError | string | undefined;\n details?: Record<string, unknown> | undefined;\n};\n\nexport function serializeError(err: unknown): SerializedError {\n if (!(err instanceof Error)) {\n return {\n name: 'Error',\n message: String(err),\n details: err && typeof err === 'object' ? {...err} : undefined,\n };\n }\n\n // Error fields such as message, stack, and some native error details are\n // non-enumerable, so JSON.stringify(err) would usually return \"{}\".\n const details = Object.fromEntries(\n Object.getOwnPropertyNames(err)\n .filter(key => !['name', 'message', 'stack', 'cause'].includes(key))\n .map(key => [key, (err as unknown as Record<string, unknown>)[key]]),\n );\n const cause =\n err.cause instanceof Error\n ? serializeError(err.cause)\n : err.cause === undefined\n ? undefined\n : String(err.cause);\n\n return {\n name: err.name,\n message: err.message,\n stack: err.stack,\n cause,\n details: Object.keys(details).length ? details : undefined,\n };\n}\n\nexport function deserializeError(serialized: SerializedError): Error {\n const err = new Error(serialized.message);\n err.name = serialized.name;\n if (serialized.stack !== undefined) {\n err.stack = serialized.stack;\n }\n if (serialized.cause !== undefined) {\n err.cause =\n typeof serialized.cause === 'string'\n ? serialized.cause\n : deserializeError(serialized.cause);\n }\n if (serialized.details) {\n Object.assign(err, serialized.details);\n }\n return err;\n}\n\n// Wire protocol types.\nexport type ArgsMap = {\n init: [\n string,\n ChangeProcessorMode,\n PragmaConfig,\n LogConfig,\n ForceCheckpointConfig | null,\n ];\n getSubscriptionState: [];\n processMessages: [readonly ChangeStreamData[]];\n abort: [];\n stop: [];\n};\n\nexport type Method = keyof ArgsMap;\n\nexport type Request<M extends Method = Method> = {method: M; args: ArgsMap[M]};\n\nexport type ResultMap = {\n init: void;\n getSubscriptionState: SubscriptionStateWithBackfills;\n processMessages: CommitResult | null;\n abort: void;\n stop: void;\n};\n\nexport type Response<M extends Method = Method> =\n | {method: M; result: ResultMap[M]; error?: undefined}\n | {method: M; error: SerializedError; result?: undefined};\n\nexport type WriteError = {writeError: SerializedError};\n\nexport function applyPragmas(db: Database, pragmas: PragmaConfig) {\n db.pragma(`busy_timeout = ${pragmas.busyTimeout}`);\n db.pragma(`analysis_limit = ${pragmas.analysisLimit}`);\n if (pragmas.walAutocheckpoint !== undefined) {\n db.pragma(`wal_autocheckpoint = ${pragmas.walAutocheckpoint}`);\n }\n}\n\n/**\n * Delegates SQLite writes to a worker_thread,\n * keeping the main event loop free for WebSocket heartbeats and IPC.\n */\nexport class ThreadWriteWorkerClient implements WriteWorkerClient {\n readonly #worker: Worker;\n #pending: Resolver<unknown, Error> | null = null;\n #errorHandler: ErrorHandler = () => {};\n #terminated = false;\n\n constructor() {\n this.#worker = new Worker(WRITE_WORKER_URL);\n\n this.#worker.on('message', (msg: Response | WriteError) => {\n if ('writeError' in msg) {\n const error = deserializeError(msg.writeError);\n this.#rejectAll(error);\n this.#errorHandler(error);\n return;\n }\n const r = this.#pending;\n if (!r) return; // stale abort response\n this.#pending = null;\n if (msg.error !== undefined) {\n r.reject(deserializeError(msg.error));\n } else {\n r.resolve(msg.result);\n }\n });\n\n this.#worker.on('error', (err: Error) => {\n this.#rejectAll(err);\n this.#errorHandler(err);\n });\n\n this.#worker.on('exit', (code: number) => {\n this.#terminated = true;\n if (code !== 0) {\n const err = new Error(`Worker exited with code ${code}`);\n this.#rejectAll(err);\n this.#errorHandler(err);\n }\n });\n }\n\n #rejectAll(err: Error) {\n const r = this.#pending;\n if (r) {\n this.#pending = null;\n r.reject(err);\n }\n }\n\n #call<M extends Method>(method: M, args: ArgsMap[M]): Promise<ResultMap[M]> {\n assert(this.#pending === null, `concurrent call: ${method}`);\n const r = resolver<ResultMap[M]>();\n this.#pending = r as Resolver<unknown, Error>;\n this.#worker.postMessage({method, args} satisfies Request);\n return r.promise;\n }\n\n init(\n dbPath: string,\n mode: ChangeProcessorMode,\n pragmas: PragmaConfig,\n logConfig: LogConfig,\n checkpoint: ForceCheckpointConfig | null = null,\n ): Promise<void> {\n return this.#call('init', [dbPath, mode, pragmas, logConfig, checkpoint]);\n }\n\n getSubscriptionState(): Promise<SubscriptionStateWithBackfills> {\n return this.#call('getSubscriptionState', []);\n }\n\n processMessages(\n downstream: readonly ChangeStreamData[],\n ): Promise<CommitResult | null> {\n return this.#call('processMessages', [downstream]);\n }\n\n abort(): void {\n if (!this.#terminated) {\n this.#worker.postMessage({method: 'abort', args: []} satisfies Request);\n }\n }\n\n async stop(): Promise<void> {\n await this.#call('stop', []);\n if (!this.#terminated) {\n await this.#worker.terminate();\n }\n }\n\n onError(handler: ErrorHandler): void {\n this.#errorHandler = handler;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"write-worker-client.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/write-worker-client.ts"],"sourcesContent":["import {Worker} from 'node:worker_threads';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport type {LogConfig} from '../../../../shared/src/logging.ts';\nimport {resolver, type Resolver} from '../../../../shared/src/resolver.ts';\nimport type {Database} from '../../../../zqlite/src/db.ts';\nimport {WRITE_WORKER_URL} from '../../server/worker-urls.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport type {BackfillRequest} from '../change-source/protocol/current/upstream.ts';\nimport type {ChangeProcessorMode, CommitResult} from './change-processor.ts';\nimport type {SubscriptionState} from './schema/replication-state.ts';\n\nexport type PragmaConfig = {\n busyTimeout: number;\n analysisLimit: number;\n walAutocheckpoint?: number | undefined;\n};\n\n/**\n * Enables write-path checkpoint backpressure in the write worker. At each\n * commit boundary, if the WAL has grown to at least `checkpointThresholdPages`,\n * the worker forces an immediate litestream sync (sealing the WAL to LTX and\n * running litestream's checkpoint).\n *\n * The optional `walMaxPages` is an emergency break that pauses until the WAL drains,\n * rather than letting it exceed available disk space. This replaces litestream's\n * analogous `truncate-page-n` emergency break, which is more disruptive because of\n * https://github.com/benbjohnson/litestream/issues/1332. Note, however, this should\n * never been needed if non-initial snapshots are disabled (which is the default).\n *\n * Set only for the backup replicator when replicating with litestream v5; `null`\n * disables the feature entirely (no per-commit overhead).\n */\nexport type ForceCheckpointConfig = {\n checkpointThresholdPages: number;\n maxWalPages?: number | undefined;\n};\n\ntype ErrorHandler = (err: Error) => void;\n\n/**\n * Interface for a write worker that processes replication messages.\n */\n/**\n * The replica's {@link SubscriptionState}, along with its pending backfills,\n * read in the same snapshot.\n */\nexport type SubscriptionStateWithBackfills = SubscriptionState & {\n backfills: BackfillRequest[];\n};\n\nexport interface WriteWorkerClient {\n getSubscriptionState(): Promise<SubscriptionStateWithBackfills>;\n processMessages(\n downstream: readonly ChangeStreamData[],\n ): Promise<CommitResult | null>;\n abort(): void;\n stop(): Promise<void>;\n onError(handler: ErrorHandler): void;\n}\n\nexport type SerializedError = {\n name: string;\n message: string;\n stack?: string | undefined;\n cause?: SerializedError | string | undefined;\n details?: Record<string, unknown> | undefined;\n};\n\nexport function serializeError(err: unknown): SerializedError {\n if (!(err instanceof Error)) {\n return {\n name: 'Error',\n message: String(err),\n details: err && typeof err === 'object' ? {...err} : undefined,\n };\n }\n\n // Error fields such as message, stack, and some native error details are\n // non-enumerable, so JSON.stringify(err) would usually return \"{}\".\n const details = Object.fromEntries(\n Object.getOwnPropertyNames(err)\n .filter(key => !['name', 'message', 'stack', 'cause'].includes(key))\n .map(key => [key, (err as unknown as Record<string, unknown>)[key]]),\n );\n const cause =\n err.cause instanceof Error\n ? serializeError(err.cause)\n : err.cause === undefined\n ? undefined\n : String(err.cause);\n\n return {\n name: err.name,\n message: err.message,\n stack: err.stack,\n cause,\n details: Object.keys(details).length ? details : undefined,\n };\n}\n\nexport function deserializeError(serialized: SerializedError): Error {\n const err = new Error(serialized.message);\n err.name = serialized.name;\n if (serialized.stack !== undefined) {\n err.stack = serialized.stack;\n }\n if (serialized.cause !== undefined) {\n err.cause =\n typeof serialized.cause === 'string'\n ? serialized.cause\n : deserializeError(serialized.cause);\n }\n if (serialized.details) {\n Object.assign(err, serialized.details);\n }\n return err;\n}\n\n// Wire protocol types.\nexport type ArgsMap = {\n init: [\n string,\n ChangeProcessorMode,\n PragmaConfig,\n LogConfig,\n ForceCheckpointConfig | null,\n ];\n getSubscriptionState: [];\n processMessages: [readonly ChangeStreamData[]];\n abort: [];\n stop: [];\n};\n\nexport type Method = keyof ArgsMap;\n\nexport type Request<M extends Method = Method> = {method: M; args: ArgsMap[M]};\n\nexport type ResultMap = {\n init: void;\n getSubscriptionState: SubscriptionStateWithBackfills;\n processMessages: CommitResult | null;\n abort: void;\n stop: void;\n};\n\nexport type Response<M extends Method = Method> =\n | {method: M; result: ResultMap[M]; error?: undefined}\n | {method: M; error: SerializedError; result?: undefined};\n\nexport type WriteError = {writeError: SerializedError};\n\nexport function applyPragmas(db: Database, pragmas: PragmaConfig) {\n db.pragma(`busy_timeout = ${pragmas.busyTimeout}`);\n db.pragma(`analysis_limit = ${pragmas.analysisLimit}`);\n if (pragmas.walAutocheckpoint !== undefined) {\n db.pragma(`wal_autocheckpoint = ${pragmas.walAutocheckpoint}`);\n }\n}\n\n/**\n * Delegates SQLite writes to a worker_thread,\n * keeping the main event loop free for WebSocket heartbeats and IPC.\n */\nexport class ThreadWriteWorkerClient implements WriteWorkerClient {\n readonly #worker: Worker;\n #pending: Resolver<unknown, Error> | null = null;\n #errorHandler: ErrorHandler = () => {};\n #terminated = false;\n\n constructor() {\n this.#worker = new Worker(WRITE_WORKER_URL);\n\n this.#worker.on('message', (msg: Response | WriteError) => {\n if ('writeError' in msg) {\n const error = deserializeError(msg.writeError);\n this.#rejectAll(error);\n this.#errorHandler(error);\n return;\n }\n const r = this.#pending;\n if (!r) return; // stale abort response\n this.#pending = null;\n if (msg.error !== undefined) {\n r.reject(deserializeError(msg.error));\n } else {\n r.resolve(msg.result);\n }\n });\n\n this.#worker.on('error', (err: Error) => {\n this.#rejectAll(err);\n this.#errorHandler(err);\n });\n\n this.#worker.on('exit', (code: number) => {\n this.#terminated = true;\n if (code !== 0) {\n const err = new Error(`Worker exited with code ${code}`);\n this.#rejectAll(err);\n this.#errorHandler(err);\n }\n });\n }\n\n #rejectAll(err: Error) {\n const r = this.#pending;\n if (r) {\n this.#pending = null;\n r.reject(err);\n }\n }\n\n #call<M extends Method>(method: M, args: ArgsMap[M]): Promise<ResultMap[M]> {\n assert(this.#pending === null, `concurrent call: ${method}`);\n const r = resolver<ResultMap[M]>();\n this.#pending = r as Resolver<unknown, Error>;\n this.#worker.postMessage({method, args} satisfies Request);\n return r.promise;\n }\n\n init(\n dbPath: string,\n mode: ChangeProcessorMode,\n pragmas: PragmaConfig,\n logConfig: LogConfig,\n checkpoint: ForceCheckpointConfig | null = null,\n ): Promise<void> {\n return this.#call('init', [dbPath, mode, pragmas, logConfig, checkpoint]);\n }\n\n getSubscriptionState(): Promise<SubscriptionStateWithBackfills> {\n return this.#call('getSubscriptionState', []);\n }\n\n processMessages(\n downstream: readonly ChangeStreamData[],\n ): Promise<CommitResult | null> {\n return this.#call('processMessages', [downstream]);\n }\n\n abort(): void {\n if (!this.#terminated) {\n this.#worker.postMessage({method: 'abort', args: []} satisfies Request);\n }\n }\n\n async stop(): Promise<void> {\n await this.#call('stop', []);\n if (!this.#terminated) {\n await this.#worker.terminate();\n }\n }\n\n onError(handler: ErrorHandler): void {\n this.#errorHandler = handler;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { resolver } from "../../../shared/src/resolver.js";
|
|
1
2
|
import { AbortError } from "../../../shared/src/abort-error.js";
|
|
2
3
|
import { sleepWithAbort } from "../../../shared/src/sleep.js";
|
|
3
4
|
import { isPostgresError } from "../types/pg.js";
|
|
4
|
-
import { resolver } from "@rocicorp/resolver";
|
|
5
5
|
import { PG_UNDEFINED_TABLE } from "@drdgvhbh/postgres-error-codes";
|
|
6
6
|
//#region ../zero-cache/src/services/running-state.ts
|
|
7
7
|
var DEFAULT_INITIAL_RETRY_DELAY_MS = 25;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"running-state.js","names":[],"sources":["../../../../../zero-cache/src/services/running-state.ts"],"sourcesContent":["import {PG_UNDEFINED_TABLE} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {
|
|
1
|
+
{"version":3,"file":"running-state.js","names":[],"sources":["../../../../../zero-cache/src/services/running-state.ts"],"sourcesContent":["import {PG_UNDEFINED_TABLE} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {AbortError} from '../../../shared/src/abort-error.ts';\nimport {resolver} from '../../../shared/src/resolver.ts';\nimport {sleepWithAbort} from '../../../shared/src/sleep.ts';\nimport {isPostgresError} from '../types/pg.ts';\n\nconst DEFAULT_INITIAL_RETRY_DELAY_MS = 25;\nexport const DEFAULT_MAX_RETRY_DELAY_MS = 10000;\n\nexport type RetryConfig = {\n initialRetryDelay?: number;\n maxRetryDelay?: number;\n};\n\nexport interface Cancelable {\n cancel(): void;\n}\n\nexport type UnregisterFn = () => void;\n\n/**\n * Facilitates lifecycle control with exponential backoff.\n */\nexport class RunningState {\n readonly #serviceName: string;\n readonly #controller: AbortController;\n readonly #sleep: typeof sleepWithAbort;\n readonly #setTimeout: typeof setTimeout;\n readonly #stopped: Promise<void>;\n\n readonly #initialRetryDelay: number;\n readonly #maxRetryDelay: number;\n readonly #pendingTimeouts = new Set<NodeJS.Timeout>();\n #retryDelay: number;\n\n constructor(\n serviceName: string,\n retryConfig?: RetryConfig,\n setTimeoutFn = setTimeout,\n sleeper = sleepWithAbort,\n ) {\n const {\n initialRetryDelay = DEFAULT_INITIAL_RETRY_DELAY_MS,\n maxRetryDelay = DEFAULT_MAX_RETRY_DELAY_MS,\n } = retryConfig ?? {};\n\n this.#serviceName = serviceName;\n this.#initialRetryDelay = initialRetryDelay;\n this.#maxRetryDelay = maxRetryDelay;\n this.#retryDelay = initialRetryDelay;\n\n this.#controller = new AbortController();\n this.#sleep = sleeper;\n this.#setTimeout = setTimeoutFn;\n\n const {promise, resolve} = resolver();\n this.#stopped = promise;\n this.#controller.signal.addEventListener(\n 'abort',\n () => {\n resolve();\n for (const timeout of this.#pendingTimeouts) {\n clearTimeout(timeout);\n }\n this.#pendingTimeouts.clear();\n },\n {once: true},\n );\n }\n\n get signal(): AbortSignal {\n return this.#controller.signal;\n }\n\n get retryDelay() {\n return this.#retryDelay;\n }\n\n /**\n * Returns `true` until {@link stop()} has been called.\n *\n * This is usually called as part of the service's main loop\n * conditional to determine if the next iteration should execute.\n */\n shouldRun(): boolean {\n return !this.#controller.signal.aborted;\n }\n\n /**\n * Registers a Cancelable object to be invoked when {@link stop()} is called.\n * Returns a method to unregister the object.\n */\n cancelOnStop(c: Cancelable): UnregisterFn {\n const onStop = () => c.cancel();\n this.#controller.signal.addEventListener('abort', onStop, {once: true});\n return () => this.#controller.signal.removeEventListener('abort', onStop);\n }\n\n /**\n * Sets a Timeout that is automatically cancelled if the service is cancelled.\n */\n setTimeout<TArgs extends unknown[]>(\n fn: (...args: TArgs) => void,\n timeoutMs: number,\n ...args: TArgs\n ) {\n const timeout = this.#setTimeout(() => {\n clearTimeout(timeout);\n this.#pendingTimeouts.delete(timeout);\n return fn(...args);\n }, timeoutMs);\n\n this.#pendingTimeouts.add(timeout);\n return timeout;\n }\n\n /**\n * Returns a promise that resolves after `ms` milliseconds or when\n * the service is stopped.\n */\n async sleep(ms: number): Promise<void> {\n await Promise.race(this.#sleep(ms, this.#controller.signal));\n }\n\n /**\n * Called to stop the service. After this is called, {@link shouldRun()}\n * will return `false` and the {@link stopped()} Promise will be resolved.\n */\n stop(lc: LogContext, err?: unknown): void {\n if (this.shouldRun()) {\n const log = !err || err instanceof AbortError ? 'info' : 'error';\n lc[log]?.(`stopping ${this.#serviceName}`, err ?? '');\n this.#controller.abort();\n }\n }\n\n /**\n * Returns a Promise that resolves when {@link stop()} is called.\n */\n stopped(): Promise<void> {\n return this.#stopped;\n }\n\n /**\n * Call in response to an error or unexpected termination in the main\n * loop of the service. The returned Promise will resolve after an\n * exponential delay, or once {@link stop()} is called.\n *\n * If the supplied `err` is an `AbortError`, the service will shut down.\n */\n async backoff(lc: LogContext, err: unknown): Promise<void> {\n const delay = this.#retryDelay;\n this.#retryDelay = Math.min(delay * 2, this.#maxRetryDelay);\n\n // Common failure mode when the user deletes a pg schema out from under\n // us after init. Turn into an AbortError to force restart, which will\n // restart and re-init the schema.\n if (isPostgresError(err, PG_UNDEFINED_TABLE)) {\n err = new AbortError('undefined table', {cause: err});\n }\n\n if (err instanceof AbortError || err instanceof UnrecoverableError) {\n this.resetBackoff();\n this.stop(lc, err);\n } else if (this.shouldRun()) {\n // Use delay-based log level: higher delay means more retries\n const log: 'info' | 'warn' | 'error' =\n delay < 1000 ? 'info' : delay < 6500 ? 'warn' : 'error';\n\n lc[log]?.(`retrying ${this.#serviceName} in ${delay} ms`, err);\n await this.sleep(delay);\n }\n }\n\n /**\n * When using {@link backoff()}, this method should be called when the\n * implementation receives a healthy signal (e.g. a successful\n * response). This resets the delay used in {@link backoff()}.\n *\n * @returns The previous backoff delay\n */\n resetBackoff() {\n const prevDelay = this.#retryDelay;\n this.#retryDelay = this.#initialRetryDelay;\n return prevDelay;\n }\n}\n\n/**\n * Superclass for Errors that should bypass exponential backoff\n * and immediately shut down the server.\n */\nexport class UnrecoverableError extends Error {\n readonly name = 'UnrecoverableError';\n}\n"],"mappings":";;;;;;AAOA,IAAM,iCAAiC;AACvC,IAAa,6BAA6B;;;;AAgB1C,IAAa,eAAb,MAA0B;CACxB;CACA;CACA;CACA;CACA;CAEA;CACA;CACA,mCAA4B,IAAI,IAAoB;CACpD;CAEA,YACE,aACA,aACA,eAAe,YACf,UAAU,gBACV;EACA,MAAM,EACJ,oBAAoB,gCACpB,gBAAgB,+BACd,eAAe,CAAC;EAEpB,KAAK,eAAe;EACpB,KAAK,qBAAqB;EAC1B,KAAK,iBAAiB;EACtB,KAAK,cAAc;EAEnB,KAAK,cAAc,IAAI,gBAAgB;EACvC,KAAK,SAAS;EACd,KAAK,cAAc;EAEnB,MAAM,EAAC,SAAS,YAAW,SAAS;EACpC,KAAK,WAAW;EAChB,KAAK,YAAY,OAAO,iBACtB,eACM;GACJ,QAAQ;GACR,KAAK,MAAM,WAAW,KAAK,kBACzB,aAAa,OAAO;GAEtB,KAAK,iBAAiB,MAAM;EAC9B,GACA,EAAC,MAAM,KAAI,CACb;CACF;CAEA,IAAI,SAAsB;EACxB,OAAO,KAAK,YAAY;CAC1B;CAEA,IAAI,aAAa;EACf,OAAO,KAAK;CACd;;;;;;;CAQA,YAAqB;EACnB,OAAO,CAAC,KAAK,YAAY,OAAO;CAClC;;;;;CAMA,aAAa,GAA6B;EACxC,MAAM,eAAe,EAAE,OAAO;EAC9B,KAAK,YAAY,OAAO,iBAAiB,SAAS,QAAQ,EAAC,MAAM,KAAI,CAAC;EACtE,aAAa,KAAK,YAAY,OAAO,oBAAoB,SAAS,MAAM;CAC1E;;;;CAKA,WACE,IACA,WACA,GAAG,MACH;EACA,MAAM,UAAU,KAAK,kBAAkB;GACrC,aAAa,OAAO;GACpB,KAAK,iBAAiB,OAAO,OAAO;GACpC,OAAO,GAAG,GAAG,IAAI;EACnB,GAAG,SAAS;EAEZ,KAAK,iBAAiB,IAAI,OAAO;EACjC,OAAO;CACT;;;;;CAMA,MAAM,MAAM,IAA2B;EACrC,MAAM,QAAQ,KAAK,KAAK,OAAO,IAAI,KAAK,YAAY,MAAM,CAAC;CAC7D;;;;;CAMA,KAAK,IAAgB,KAAqB;EACxC,IAAI,KAAK,UAAU,GAAG;GAEpB,GADY,CAAC,OAAO,eAAe,aAAa,SAAS,QAClD,GAAG,YAAY,KAAK,gBAAgB,OAAO,EAAE;GACpD,KAAK,YAAY,MAAM;EACzB;CACF;;;;CAKA,UAAyB;EACvB,OAAO,KAAK;CACd;;;;;;;;CASA,MAAM,QAAQ,IAAgB,KAA6B;EACzD,MAAM,QAAQ,KAAK;EACnB,KAAK,cAAc,KAAK,IAAI,QAAQ,GAAG,KAAK,cAAc;EAK1D,IAAI,gBAAgB,KAAK,kBAAkB,GACzC,MAAM,IAAI,WAAW,mBAAmB,EAAC,OAAO,IAAG,CAAC;EAGtD,IAAI,eAAe,cAAc,eAAe,oBAAoB;GAClE,KAAK,aAAa;GAClB,KAAK,KAAK,IAAI,GAAG;EACnB,OAAO,IAAI,KAAK,UAAU,GAAG;GAK3B,GAFE,QAAQ,MAAO,SAAS,QAAQ,OAAO,SAAS,QAE3C,GAAG,YAAY,KAAK,aAAa,MAAM,MAAM,MAAM,GAAG;GAC7D,MAAM,KAAK,MAAM,KAAK;EACxB;CACF;;;;;;;;CASA,eAAe;EACb,MAAM,YAAY,KAAK;EACvB,KAAK,cAAc,KAAK;EACxB,OAAO;CACT;AACF;;;;;AAMA,IAAa,qBAAb,cAAwC,MAAM;CAC5C,OAAgB;AAClB"}
|