@rocicorp/zero 1.10.0-canary.0 → 1.10.0-canary.2
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/db/commit.js +1 -4
- package/out/replicache/src/db/commit.js.map +1 -1
- package/out/replicache/src/db/read.d.ts +1 -0
- package/out/replicache/src/db/read.d.ts.map +1 -1
- package/out/replicache/src/db/read.js +1 -4
- package/out/replicache/src/db/read.js.map +1 -1
- package/out/replicache/src/db/write.d.ts +2 -1
- package/out/replicache/src/db/write.d.ts.map +1 -1
- package/out/replicache/src/db/write.js +4 -51
- package/out/replicache/src/db/write.js.map +1 -1
- package/out/replicache/src/persist/clients.d.ts +1 -0
- package/out/replicache/src/persist/clients.d.ts.map +1 -1
- package/out/replicache/src/persist/clients.js +4 -31
- package/out/replicache/src/persist/clients.js.map +1 -1
- package/out/replicache/src/process-env.d.ts +28 -0
- package/out/replicache/src/process-env.d.ts.map +1 -0
- package/out/replicache/src/replicache-impl.d.ts.map +1 -1
- package/out/replicache/src/replicache-impl.js.map +1 -1
- package/out/replicache/src/scan-iterator.js +2 -14
- package/out/replicache/src/scan-iterator.js.map +1 -1
- package/out/replicache/src/subscriptions.d.ts +1 -0
- package/out/replicache/src/subscriptions.d.ts.map +1 -1
- package/out/replicache/src/subscriptions.js +2 -10
- package/out/replicache/src/subscriptions.js.map +1 -1
- package/out/replicache/src/transactions.d.ts +1 -0
- package/out/replicache/src/transactions.d.ts.map +1 -1
- package/out/replicache/src/transactions.js +2 -4
- package/out/replicache/src/transactions.js.map +1 -1
- package/out/z2s/src/compiler.d.ts.map +1 -1
- package/out/z2s/src/compiler.js +1 -22
- package/out/z2s/src/compiler.js.map +1 -1
- package/out/zero/package.js +3 -3
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/normalize.d.ts +1 -0
- package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
- package/out/zero-cache/src/config/normalize.js +10 -1
- package/out/zero-cache/src/config/normalize.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +52 -1
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +171 -22
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/custom/fetch.js +1 -1
- package/out/zero-cache/src/custom/fetch.js.map +1 -1
- package/out/zero-cache/src/db/delete-lite-db.js +2 -1
- package/out/zero-cache/src/db/delete-lite-db.js.map +1 -1
- package/out/zero-cache/src/db/migration-lite.d.ts.map +1 -1
- package/out/zero-cache/src/db/migration-lite.js +5 -2
- package/out/zero-cache/src/db/migration-lite.js.map +1 -1
- package/out/zero-cache/src/db/migration.d.ts.map +1 -1
- package/out/zero-cache/src/db/migration.js +4 -2
- package/out/zero-cache/src/db/migration.js.map +1 -1
- package/out/zero-cache/src/db/sqlite-corruption.d.ts +3 -2
- package/out/zero-cache/src/db/sqlite-corruption.d.ts.map +1 -1
- package/out/zero-cache/src/db/sqlite-corruption.js +13 -8
- package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -1
- package/out/zero-cache/src/observability/metrics.d.ts +6 -1
- package/out/zero-cache/src/observability/metrics.d.ts.map +1 -1
- package/out/zero-cache/src/observability/metrics.js +20 -2
- package/out/zero-cache/src/observability/metrics.js.map +1 -1
- package/out/zero-cache/src/server/backup-watermark-poller.js +31 -0
- package/out/zero-cache/src/server/backup-watermark-poller.js.map +1 -0
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +91 -31
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/main.d.ts.map +1 -1
- package/out/zero-cache/src/server/main.js +6 -16
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/server/replicator.js +48 -5
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.js +2 -1
- package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
- package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/server/syncer.js +1 -1
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/server/worker-urls.d.ts +1 -1
- package/out/zero-cache/src/server/worker-urls.js +2 -2
- package/out/zero-cache/src/server/worker-urls.js.map +1 -1
- package/out/zero-cache/src/services/change-source/change-source.d.ts +4 -2
- package/out/zero-cache/src/services/change-source/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts +35 -0
- package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-source/common/replica-restore.js +34 -0
- package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -0
- package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts +2 -0
- package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-schema.js +73 -1
- package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.d.ts +2 -6
- package/out/zero-cache/src/services/change-source/custom/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +14 -5
- 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 +4 -7
- 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.d.ts +7 -7
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +82 -25
- 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.d.ts +9 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js +12 -4
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts +45 -2
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +59 -8
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts +4 -2
- package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js +7 -4
- 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/init.d.ts +0 -5
- package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js +29 -41
- package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +35 -16
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js +77 -7
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts +2 -1
- package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/data.js +5 -13
- package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts +6 -3
- package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/schema-change-tags.d.ts +6 -0
- package/out/zero-cache/src/services/change-source/protocol/current/schema-change-tags.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-source/protocol/current/schema-change-tags.js +21 -0
- package/out/zero-cache/src/services/change-source/protocol/current/schema-change-tags.js.map +1 -0
- package/out/zero-cache/src/services/change-source/protocol/current/status.d.ts +1 -0
- package/out/zero-cache/src/services/change-source/protocol/current/status.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts +1 -0
- package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts +5 -6
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js +32 -9
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-monitor.d.ts +15 -5
- package/out/zero-cache/src/services/change-streamer/backup-monitor.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-monitor.js +66 -0
- package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/broadcast.d.ts +68 -78
- package/out/zero-cache/src/services/change-streamer/broadcast.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.js +84 -92
- package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts +19 -0
- package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/change-log-codec.js +50 -0
- package/out/zero-cache/src/services/change-streamer/change-log-codec.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.d.ts +141 -0
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.js +270 -0
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.d.ts +19 -0
- package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.js +39 -0
- package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +3 -4
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +12 -17
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +69 -24
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +333 -66
- 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.d.ts +56 -13
- package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/error-type-enum.d.ts +8 -0
- package/out/zero-cache/src/services/change-streamer/error-type-enum.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/forwarder.d.ts +14 -3
- package/out/zero-cache/src/services/change-streamer/forwarder.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/forwarder.js +41 -11
- package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/{litestream3-backup-monitor.d.ts → litestream3-prometheus-poller.d.ts} +19 -45
- package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/{litestream3-backup-monitor.js → litestream3-prometheus-poller.js} +57 -171
- package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/replica-poller.d.ts +15 -0
- package/out/zero-cache/src/services/change-streamer/replica-poller.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/replica-poller.js +55 -0
- package/out/zero-cache/src/services/change-streamer/replica-poller.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts +5 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.js +2 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts +14 -0
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js +86 -0
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/snapshot.d.ts +4 -1
- package/out/zero-cache/src/services/change-streamer/snapshot.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot.js +39 -14
- package/out/zero-cache/src/services/change-streamer/snapshot.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.d.ts +75 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js +261 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.d.ts +121 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.js +391 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.d.ts +62 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js +159 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.d.ts +109 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js +258 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/storer.d.ts +28 -12
- package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js +116 -121
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +61 -10
- package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.js +101 -8
- package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts +34 -0
- package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/upstream-acker.js +86 -0
- package/out/zero-cache/src/services/change-streamer/upstream-acker.js.map +1 -0
- package/out/zero-cache/src/services/life-cycle.d.ts +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +4 -3
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts +12 -11
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +107 -148
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/litestream/config-v5.yml +42 -0
- package/out/zero-cache/src/services/litestream/config.yml +3 -2
- package/out/zero-cache/src/services/litestream/metrics.d.ts +4 -4
- package/out/zero-cache/src/services/litestream/metrics.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/metrics.js +7 -10
- package/out/zero-cache/src/services/litestream/metrics.js.map +1 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-poller.d.ts +57 -0
- package/out/zero-cache/src/services/litestream/vfs-watermark-poller.d.ts.map +1 -0
- package/out/zero-cache/src/services/litestream/vfs-watermark-poller.js +137 -0
- package/out/zero-cache/src/services/litestream/vfs-watermark-poller.js.map +1 -0
- package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +4 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-log-cookies.d.ts +169 -0
- package/out/zero-cache/src/services/replicator/change-log-cookies.d.ts.map +1 -0
- package/out/zero-cache/src/services/replicator/change-log-cookies.js +434 -0
- package/out/zero-cache/src/services/replicator/change-log-cookies.js.map +1 -0
- package/out/zero-cache/src/services/replicator/change-log-db.d.ts +358 -0
- package/out/zero-cache/src/services/replicator/change-log-db.d.ts.map +1 -0
- package/out/zero-cache/src/services/replicator/change-log-db.js +477 -0
- package/out/zero-cache/src/services/replicator/change-log-db.js.map +1 -0
- package/out/zero-cache/src/services/replicator/change-log-stream-writer.d.ts +55 -0
- package/out/zero-cache/src/services/replicator/change-log-stream-writer.d.ts.map +1 -0
- package/out/zero-cache/src/services/replicator/change-log-stream-writer.js +150 -0
- package/out/zero-cache/src/services/replicator/change-log-stream-writer.js.map +1 -0
- package/out/zero-cache/src/services/replicator/change-processor.d.ts +6 -0
- package/out/zero-cache/src/services/replicator/change-processor.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.js +70 -25
- package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.d.ts +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.js +19 -6
- package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
- package/out/zero-cache/src/services/replicator/notifier.d.ts +0 -22
- package/out/zero-cache/src/services/replicator/notifier.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/notifier.js +7 -1
- package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.d.ts +17 -2
- package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.js +9 -3
- package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts +27 -0
- package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.js +32 -0
- 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 +1 -1
- package/out/zero-cache/src/services/replicator/schema/backfilling.d.ts +119 -0
- package/out/zero-cache/src/services/replicator/schema/backfilling.d.ts.map +1 -0
- package/out/zero-cache/src/services/replicator/schema/backfilling.js +214 -0
- package/out/zero-cache/src/services/replicator/schema/backfilling.js.map +1 -0
- package/out/zero-cache/src/services/replicator/schema/change-log.d.ts +3 -3
- 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/schema/replication-state.d.ts +1 -1
- package/out/zero-cache/src/services/replicator/schema/replication-state.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/replication-state.js +9 -5
- package/out/zero-cache/src/services/replicator/schema/replication-state.js.map +1 -1
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.d.ts +131 -0
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.d.ts.map +1 -0
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js +413 -0
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js.map +1 -0
- package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.d.ts +120 -0
- package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.d.ts.map +1 -0
- package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js +283 -0
- package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js.map +1 -0
- package/out/zero-cache/src/services/replicator/write-worker.js +23 -12
- package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
- package/out/zero-cache/src/services/run-ast.d.ts.map +1 -1
- package/out/zero-cache/src/services/run-ast.js +2 -1
- package/out/zero-cache/src/services/run-ast.js.map +1 -1
- package/out/zero-cache/src/services/running-state.d.ts +1 -1
- package/out/zero-cache/src/services/running-state.d.ts.map +1 -1
- package/out/zero-cache/src/services/running-state.js +1 -0
- package/out/zero-cache/src/services/running-state.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.d.ts +53 -0
- package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js +68 -0
- package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +18 -3
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +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.d.ts +11 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +64 -6
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/processes.d.ts +1 -2
- package/out/zero-cache/src/types/processes.d.ts.map +1 -1
- package/out/zero-cache/src/types/processes.js +1 -25
- package/out/zero-cache/src/types/processes.js.map +1 -1
- package/out/zero-cache/src/types/streams.d.ts +21 -0
- package/out/zero-cache/src/types/streams.d.ts.map +1 -1
- package/out/zero-cache/src/types/streams.js +25 -4
- package/out/zero-cache/src/types/streams.js.map +1 -1
- package/out/zero-cache/src/types/subscription.d.ts +11 -0
- package/out/zero-cache/src/types/subscription.d.ts.map +1 -1
- package/out/zero-cache/src/types/subscription.js +27 -0
- package/out/zero-cache/src/types/subscription.js.map +1 -1
- package/out/zero-cache/src/workers/connection.d.ts +1 -0
- package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
- package/out/zero-cache/src/workers/connection.js +24 -2
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/replicator.d.ts +25 -0
- package/out/zero-cache/src/workers/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/workers/replicator.js +33 -1
- package/out/zero-cache/src/workers/replicator.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts +12 -0
- package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +75 -0
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts +6 -7
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +156 -101
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-schema/src/builder/table-builder.d.ts +23 -0
- package/out/zero-schema/src/builder/table-builder.d.ts.map +1 -1
- package/out/zero-schema/src/builder/table-builder.js +18 -0
- package/out/zero-schema/src/builder/table-builder.js.map +1 -1
- package/out/zero-server/src/custom.js +10 -3
- package/out/zero-server/src/custom.js.map +1 -1
- package/out/zero-server/src/schema.d.ts +8 -0
- package/out/zero-server/src/schema.d.ts.map +1 -1
- package/out/zero-server/src/schema.js +63 -24
- package/out/zero-server/src/schema.js.map +1 -1
- package/out/zero-types/src/schema.d.ts +11 -0
- package/out/zero-types/src/schema.d.ts.map +1 -1
- package/out/zql/src/mutate/crud.d.ts +10 -8
- package/out/zql/src/mutate/crud.d.ts.map +1 -1
- package/out/zql/src/mutate/crud.js.map +1 -1
- package/out/zql/src/query/query-delegate-base.d.ts.map +1 -1
- package/out/zql/src/query/query-delegate-base.js +5 -1
- package/out/zql/src/query/query-delegate-base.js.map +1 -1
- package/out/zql/src/query/query-impl.d.ts +2 -2
- package/out/zql/src/query/query-impl.d.ts.map +1 -1
- package/out/zql/src/query/query-impl.js.map +1 -1
- package/out/zql/src/query/query.d.ts +83 -14
- package/out/zql/src/query/query.d.ts.map +1 -1
- package/out/zql/src/query/static-query.d.ts +1 -1
- package/out/zql/src/query/static-query.d.ts.map +1 -1
- package/out/zqlite/src/db.js +2 -2
- package/out/zqlite/src/resolve-scalar-subqueries.d.ts +15 -0
- package/out/zqlite/src/resolve-scalar-subqueries.d.ts.map +1 -1
- package/out/zqlite/src/resolve-scalar-subqueries.js +33 -14
- package/out/zqlite/src/resolve-scalar-subqueries.js.map +1 -1
- package/out/zqlite/src/sqlite-cost-model.js +5 -5
- package/out/zqlite/src/table-source.js +3 -3
- package/package.json +3 -3
- package/out/replicache/src/db/index.js +0 -91
- package/out/replicache/src/db/index.js.map +0 -1
- package/out/replicache/src/lazy.d.ts +0 -2
- package/out/replicache/src/lazy.d.ts.map +0 -1
- package/out/replicache/src/lazy.js +0 -12
- package/out/replicache/src/lazy.js.map +0 -1
- package/out/zero-cache/src/server/backup-watermark-reader.js +0 -35
- package/out/zero-cache/src/server/backup-watermark-reader.js.map +0 -1
- package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.d.ts.map +0 -1
- package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.js.map +0 -1
- package/out/zero-cache/src/services/change-streamer/replica-monitor.d.ts +0 -16
- package/out/zero-cache/src/services/change-streamer/replica-monitor.d.ts.map +0 -1
- package/out/zero-cache/src/services/change-streamer/replica-monitor.js +0 -54
- package/out/zero-cache/src/services/change-streamer/replica-monitor.js.map +0 -1
- package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.d.ts +0 -25
- package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.d.ts.map +0 -1
- package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js +0 -159
- package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js.map +0 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-reader.d.ts +0 -44
- package/out/zero-cache/src/services/litestream/vfs-watermark-reader.d.ts.map +0 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-reader.js +0 -65
- package/out/zero-cache/src/services/litestream/vfs-watermark-reader.js.map +0 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.d.ts +0 -11
- package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.d.ts.map +0 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.js +0 -74
- package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.js.map +0 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-worker.d.ts +0 -42
- package/out/zero-cache/src/services/litestream/vfs-watermark-worker.d.ts.map +0 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-worker.js +0 -117
- package/out/zero-cache/src/services/litestream/vfs-watermark-worker.js.map +0 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { assert } from "../../../../shared/src/asserts.js";
|
|
2
|
+
import { getOrCreateCounter, getOrCreateLatencyHistogram, getOrCreateValueHistogram } from "../../observability/metrics.js";
|
|
3
|
+
import { CHANGE_LOG_STREAM_TABLE, readChangeLogMeta } from "../replicator/change-log-db.js";
|
|
4
|
+
import { SQLITE_CHANGE_LOG_PLAN_SQL } from "./sqlite-change-log-reader.js";
|
|
5
|
+
import { SQLiteChangeLogPurger } from "../replicator/sqlite-change-log-purger.js";
|
|
6
|
+
import { Lock } from "@rocicorp/lock";
|
|
7
|
+
//#region ../zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.ts
|
|
8
|
+
/**
|
|
9
|
+
* The duration target for any one purge statement, enforced by the purger's
|
|
10
|
+
* measured-budget feedback. Purge runs on the writer's own
|
|
11
|
+
* connection, in the stream loop's process, so a statement's duration stalls
|
|
12
|
+
* the appending path — and the fan-out behind it — directly. The bench's
|
|
13
|
+
* 100 ms bound was derived for the replicator's write worker; this budget is
|
|
14
|
+
* measured against an event loop that also owes progress to fan-out, catchup,
|
|
15
|
+
* and websocket keepalives, so it is deliberately tighter.
|
|
16
|
+
*/
|
|
17
|
+
var DEFAULT_STATEMENT_TARGET_MS = 20;
|
|
18
|
+
/**
|
|
19
|
+
* Batches per pass. This bounds one pass's bookkeeping, not the drain: a pass
|
|
20
|
+
* that hits the limit asks its caller for an immediate continuation.
|
|
21
|
+
*/
|
|
22
|
+
var DEFAULT_MAX_BATCHES_PER_PASS = 100;
|
|
23
|
+
/**
|
|
24
|
+
* The backstop poll while waiting for the writer's open transaction to
|
|
25
|
+
* commit. The commit notification normally arrives first; this covers an
|
|
26
|
+
* interrupted stream, whose rollback does not notify.
|
|
27
|
+
*/
|
|
28
|
+
var DEFAULT_IN_TRANSACTION_POLL_MS = 1e3;
|
|
29
|
+
/**
|
|
30
|
+
* Whether the cleanup coordinator still owes this floor a pass. Deliberately
|
|
31
|
+
* not the purger's eligibility query, in two ways:
|
|
32
|
+
*
|
|
33
|
+
* - `remains`: rows younger than the retention cutoff are not eligible *yet*,
|
|
34
|
+
* but they will be, and their presence asks the cleanup coordinator for a
|
|
35
|
+
* deferred pass even on a quiet source.
|
|
36
|
+
* - `headBehindFloor`: a head behind the floor means the writer is expected
|
|
37
|
+
* to replay commits below it (e.g. after reconnecting while the backup
|
|
38
|
+
* outran this log). Those rows do not exist to count yet, writer commits
|
|
39
|
+
* only wake an in-flight waiter, and backup monitors never resend an
|
|
40
|
+
* unchanged floor — so reporting quiescence here would disarm cleanup for
|
|
41
|
+
* good. An empty log (no head) defers for the same reason.
|
|
42
|
+
*/
|
|
43
|
+
var BELOW_FLOOR_REMAINS_SQL = `
|
|
44
|
+
SELECT
|
|
45
|
+
EXISTS (
|
|
46
|
+
SELECT 1 FROM "${CHANGE_LOG_STREAM_TABLE}"
|
|
47
|
+
WHERE "watermark" < @floor
|
|
48
|
+
AND "watermark" < (SELECT max("watermark") FROM "${CHANGE_LOG_STREAM_TABLE}")
|
|
49
|
+
) AS "remains",
|
|
50
|
+
coalesce(
|
|
51
|
+
(SELECT max("watermark") FROM "${CHANGE_LOG_STREAM_TABLE}") < @floor,
|
|
52
|
+
1
|
|
53
|
+
) AS "headBehindFloor"
|
|
54
|
+
`;
|
|
55
|
+
/**
|
|
56
|
+
* Runs bounded SQLite change-log purge passes on the writer's own connection.
|
|
57
|
+
*
|
|
58
|
+
* The caller supplies the current durable cleanup floor independently of any
|
|
59
|
+
* other change-log implementation and owns the timing of subsequent passes.
|
|
60
|
+
* Within a pass this class drains bounded batches, re-checking live subscriber
|
|
61
|
+
* ACKs before each one and releasing the cleanup guard between them, so a
|
|
62
|
+
* catchup registration or snapshot reservation never waits longer than one
|
|
63
|
+
* batch.
|
|
64
|
+
*
|
|
65
|
+
* Purge windows exist only between the writer's commits: purge shares the
|
|
66
|
+
* writer's connection, so it cannot run inside the writer's open transaction,
|
|
67
|
+
* and a batch that finds one waits for the commit notification instead.
|
|
68
|
+
*
|
|
69
|
+
* The result tells the cleanup coordinator whether the next pass should be
|
|
70
|
+
* immediate or deferred. This class deliberately owns no cleanup timer: the
|
|
71
|
+
* same level-triggered coordinator continues a large drain, advances the
|
|
72
|
+
* retention clock on a quiet source, and retries changing external state.
|
|
73
|
+
*/
|
|
74
|
+
var SQLiteChangeLogPurgeScheduler = class {
|
|
75
|
+
#lc;
|
|
76
|
+
#connection;
|
|
77
|
+
#subscriberAcks;
|
|
78
|
+
#opts;
|
|
79
|
+
#now;
|
|
80
|
+
#setTimeoutFn;
|
|
81
|
+
#clearTimeoutFn;
|
|
82
|
+
#yield;
|
|
83
|
+
/**
|
|
84
|
+
* The mutex that serializes purge batches with catchup registration and
|
|
85
|
+
* reservation pauses. It is only ever held across a synchronous section —
|
|
86
|
+
* one batch, or one registration — never across a yield, which is what
|
|
87
|
+
* bounds any waiter to a single batch's duration.
|
|
88
|
+
*/
|
|
89
|
+
#lock = new Lock();
|
|
90
|
+
/**
|
|
91
|
+
* Open snapshot reservations, reference-counted by taskID. Purge pauses
|
|
92
|
+
* while any is open. Counted rather than a set so that a reservation retry
|
|
93
|
+
* -- whose cancel-then-recreate can resolve its old resume after its new
|
|
94
|
+
* suspend -- still nets out.
|
|
95
|
+
*/
|
|
96
|
+
#pausedBy = /* @__PURE__ */ new Map();
|
|
97
|
+
/** Waiters for the writer's next commit (or rollback). */
|
|
98
|
+
#writerIdleWaiters = /* @__PURE__ */ new Set();
|
|
99
|
+
#purgedRows = getOrCreateCounter("replica", "sqlite_change_log.purged_rows", "Rows removed from the SQLite change log by the purge scheduler.");
|
|
100
|
+
#passDuration = getOrCreateLatencyHistogram("replica", "sqlite_change_log.purge_cycle_duration", "Duration of one SQLite change-log purge pass, including the yields between its batches.");
|
|
101
|
+
#passBatches = getOrCreateValueHistogram("replica", "sqlite_change_log.purge_cycle_batches", {
|
|
102
|
+
description: "Purge batches run per SQLite change-log purge pass.",
|
|
103
|
+
unit: "{batch}",
|
|
104
|
+
bucketBoundaries: [
|
|
105
|
+
1,
|
|
106
|
+
2,
|
|
107
|
+
5,
|
|
108
|
+
10,
|
|
109
|
+
25,
|
|
110
|
+
50,
|
|
111
|
+
100,
|
|
112
|
+
250
|
|
113
|
+
]
|
|
114
|
+
});
|
|
115
|
+
#declines = getOrCreateCounter("replica", "sqlite_change_log.purge_declined", "SQLite change-log purge passes that ended before draining, by reason.");
|
|
116
|
+
#floorProbes = getOrCreateCounter("replica", "sqlite_change_log.purge_floor_probe", "Invariant-14 probe outcomes: whether the transaction at the purge floor is still servable after each pass. \"violation\" means history at or above the floor was purged and alerts; \"cold-log\" means the log never held it, which is routine at every fork and resumption.");
|
|
117
|
+
#cache;
|
|
118
|
+
#stopped = false;
|
|
119
|
+
/**
|
|
120
|
+
* The guard that brackets catchup registration: required-head capture and
|
|
121
|
+
* `Forwarder.add()` run under the same mutex as purge batches, so the
|
|
122
|
+
* registered ACK is visible to `Forwarder.getAcks()` before the next batch
|
|
123
|
+
* evaluates its gate.
|
|
124
|
+
*/
|
|
125
|
+
cleanupGuard = { runWhilePurgeBlocked: (register) => this.#lock.withLock(register) };
|
|
126
|
+
/**
|
|
127
|
+
* @param connection the writer's own connection (§3.3). `undefined` until
|
|
128
|
+
* the writer's first reconcile creates the file, and again after the
|
|
129
|
+
* writer fails soft and deletes it — both of which defer the pass rather
|
|
130
|
+
* than fail it, so a later coordinator pass picks up a late-appearing
|
|
131
|
+
* file without a restart.
|
|
132
|
+
* @param subscriberAcks live subscriber ACKs, re-read before every batch so
|
|
133
|
+
* the supplied floor remains safe throughout an asynchronous drain.
|
|
134
|
+
*/
|
|
135
|
+
constructor(lc, connection, subscriberAcks, opts) {
|
|
136
|
+
assert(Number.isSafeInteger(opts.retentionMs) && opts.retentionMs > 0, "sqliteChangeLogRetentionMs must be a positive integer");
|
|
137
|
+
assert(Number.isSafeInteger(opts.batchRows) && opts.batchRows > 0, "sqliteChangeLogPurgeBatchRows must be a positive integer");
|
|
138
|
+
this.#lc = lc.withContext("component", "sqlite-change-log-purge");
|
|
139
|
+
this.#connection = connection;
|
|
140
|
+
this.#subscriberAcks = subscriberAcks;
|
|
141
|
+
this.#opts = opts;
|
|
142
|
+
this.#now = opts.now ?? Date.now;
|
|
143
|
+
this.#setTimeoutFn = opts.setTimeoutFn ?? setTimeout;
|
|
144
|
+
this.#clearTimeoutFn = opts.clearTimeoutFn ?? clearTimeout;
|
|
145
|
+
this.#yield = opts.yieldFn ?? (() => new Promise((resolve) => setImmediate(resolve)));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Runs one purge pass for the current durable cleanup `floor`.
|
|
149
|
+
* Never rejects; a failed pass is logged and reported as `stopped: 'error'`.
|
|
150
|
+
*/
|
|
151
|
+
async purge(floor) {
|
|
152
|
+
if (this.#stopped) return this.#skipped(floor, "stopped");
|
|
153
|
+
try {
|
|
154
|
+
return await this.#runPass(floor);
|
|
155
|
+
} catch (e) {
|
|
156
|
+
this.#lc.warn?.("error purging the SQLite change log", e);
|
|
157
|
+
this.#declines.add(1, { reason: "error" });
|
|
158
|
+
return this.#skipped(floor, "error", "deferred");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Pauses purging while `taskID` holds a snapshot reservation. The returned
|
|
163
|
+
* promise resolves once no purge batch is in flight, i.e. once the bounds a
|
|
164
|
+
* backup monitor is about to advertise cannot be invalidated by a batch
|
|
165
|
+
* that had already been dispatched.
|
|
166
|
+
*/
|
|
167
|
+
pause(taskID) {
|
|
168
|
+
this.#pausedBy.set(taskID, (this.#pausedBy.get(taskID) ?? 0) + 1);
|
|
169
|
+
return this.#lock.withLock(() => {});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Ends one of `taskID`'s pauses. A later coordinator pass can purge once
|
|
173
|
+
* every reservation has ended. Unknown `taskID`s are ignored.
|
|
174
|
+
*/
|
|
175
|
+
resume(taskID) {
|
|
176
|
+
const count = this.#pausedBy.get(taskID);
|
|
177
|
+
if (count === void 0) return;
|
|
178
|
+
if (count > 1) {
|
|
179
|
+
this.#pausedBy.set(taskID, count - 1);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
this.#pausedBy.delete(taskID);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Notes that the writer's open transaction has ended, waking any batch
|
|
186
|
+
* waiting for a purge window. A missed notification costs one poll
|
|
187
|
+
* interval, never correctness.
|
|
188
|
+
*/
|
|
189
|
+
onWriterIdle() {
|
|
190
|
+
for (const waiter of this.#writerIdleWaiters) waiter();
|
|
191
|
+
this.#writerIdleWaiters.clear();
|
|
192
|
+
}
|
|
193
|
+
stop() {
|
|
194
|
+
this.#stopped = true;
|
|
195
|
+
this.onWriterIdle();
|
|
196
|
+
}
|
|
197
|
+
async #runPass(floor) {
|
|
198
|
+
const startedAt = performance.now();
|
|
199
|
+
let batches = 0;
|
|
200
|
+
let deletedRows = 0;
|
|
201
|
+
let stopped;
|
|
202
|
+
let db;
|
|
203
|
+
try {
|
|
204
|
+
for (;;) {
|
|
205
|
+
if (this.#stopped) {
|
|
206
|
+
stopped = "stopped";
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
if (this.#pausedBy.size > 0) {
|
|
210
|
+
stopped = "paused";
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
const connection = this.#connection();
|
|
214
|
+
if (connection === void 0) {
|
|
215
|
+
db = void 0;
|
|
216
|
+
this.#lc.debug?.("skipping SQLite change-log purge: the writer has not opened the log");
|
|
217
|
+
stopped = "writer-unavailable";
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
db = connection;
|
|
221
|
+
if (batches >= Math.max(1, this.#opts.maxBatchesPerPass ?? DEFAULT_MAX_BATCHES_PER_PASS)) {
|
|
222
|
+
stopped = "batch-limit";
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
if (connection.inTransaction) {
|
|
226
|
+
await this.#awaitWriterIdle();
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const outcome = await this.#lock.withLock(() => {
|
|
230
|
+
if (this.#connection() !== connection || connection.inTransaction) return { kind: "retry" };
|
|
231
|
+
const declined = this.#gate(floor);
|
|
232
|
+
if (declined !== void 0) return {
|
|
233
|
+
kind: "declined",
|
|
234
|
+
reason: declined
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
kind: "purged",
|
|
238
|
+
result: this.#cacheFor(connection).purger.purgeBatch({
|
|
239
|
+
externalFloor: floor,
|
|
240
|
+
retentionCutoffMs: this.#now() - this.#opts.retentionMs,
|
|
241
|
+
maxRows: this.#opts.batchRows,
|
|
242
|
+
maxDurationMs: this.#opts.statementTargetMs ?? DEFAULT_STATEMENT_TARGET_MS
|
|
243
|
+
})
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
if (outcome.kind === "retry") continue;
|
|
247
|
+
if (outcome.kind === "declined") {
|
|
248
|
+
stopped = outcome.reason;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
batches++;
|
|
252
|
+
deletedRows += outcome.result.deletedRows;
|
|
253
|
+
if (!outcome.result.moreEligible) break;
|
|
254
|
+
await this.#yield();
|
|
255
|
+
}
|
|
256
|
+
} catch (e) {
|
|
257
|
+
this.#lc.warn?.("error purging the SQLite change log", e);
|
|
258
|
+
stopped = "error";
|
|
259
|
+
}
|
|
260
|
+
const probe = db === void 0 ? void 0 : this.#probeFloor(db, floor);
|
|
261
|
+
let continuation;
|
|
262
|
+
try {
|
|
263
|
+
continuation = this.#continuation(floor, stopped);
|
|
264
|
+
} catch (e) {
|
|
265
|
+
this.#lc.warn?.("unable to determine whether SQLite change-log purge should continue", e);
|
|
266
|
+
continuation = "deferred";
|
|
267
|
+
stopped ??= "error";
|
|
268
|
+
}
|
|
269
|
+
if (deletedRows > 0) this.#purgedRows.add(deletedRows);
|
|
270
|
+
if (batches > 0) {
|
|
271
|
+
this.#passBatches.record(batches);
|
|
272
|
+
this.#passDuration.recordMs(performance.now() - startedAt);
|
|
273
|
+
}
|
|
274
|
+
if (stopped !== void 0) this.#declines.add(1, { reason: stopped });
|
|
275
|
+
if (batches > 0 || stopped !== "writer-unavailable") this.#lc.debug?.("SQLite change-log purge pass", { sqliteChangeLogPurge: {
|
|
276
|
+
floor,
|
|
277
|
+
batches,
|
|
278
|
+
deletedRows,
|
|
279
|
+
stopped,
|
|
280
|
+
probe,
|
|
281
|
+
continuation
|
|
282
|
+
} });
|
|
283
|
+
return {
|
|
284
|
+
floor,
|
|
285
|
+
batches,
|
|
286
|
+
deletedRows,
|
|
287
|
+
stopped,
|
|
288
|
+
probe,
|
|
289
|
+
continuation
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Reports whether the level-triggered cleanup coordinator should run again.
|
|
294
|
+
* Rows below the floor but inside the retention window require a deferred
|
|
295
|
+
* pass even when no new backup watermark arrives, and so does a head behind
|
|
296
|
+
* the floor: the rows the writer replays into that gap arrive after this
|
|
297
|
+
* check, with no other event armed to reap them.
|
|
298
|
+
*/
|
|
299
|
+
#continuation(floor, stopped) {
|
|
300
|
+
switch (stopped) {
|
|
301
|
+
case "batch-limit": return "immediate";
|
|
302
|
+
case "stopped": return;
|
|
303
|
+
case "paused":
|
|
304
|
+
case "subscriber-behind":
|
|
305
|
+
case "writer-unavailable":
|
|
306
|
+
case "error": return "deferred";
|
|
307
|
+
case void 0: {
|
|
308
|
+
const db = this.#connection();
|
|
309
|
+
if (db === void 0) return "deferred";
|
|
310
|
+
const { remains, headBehindFloor } = this.#cacheFor(db).belowFloorRemains.get({ floor });
|
|
311
|
+
return remains === 0 && headBehindFloor === 0 ? void 0 : "deferred";
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* No purge while a connected subscriber is behind the supplied floor. The
|
|
317
|
+
* cleanup coordinator has already provided the reconnect grace period; an
|
|
318
|
+
* empty live set places no additional constraint on the durable floor.
|
|
319
|
+
*/
|
|
320
|
+
#gate(floor) {
|
|
321
|
+
for (const ack of this.#subscriberAcks()) if (ack < floor) return "subscriber-behind";
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The live check on invariant 14 — what makes running this slice dark worth
|
|
325
|
+
* anything: in `write` mode nothing reads the log, so a wrong floor would
|
|
326
|
+
* otherwise stay invisible until the canary rollout two slices later.
|
|
327
|
+
*/
|
|
328
|
+
#probeFloor(db, floor) {
|
|
329
|
+
try {
|
|
330
|
+
const row = this.#cacheFor(db).plan.get({ fromWatermark: floor });
|
|
331
|
+
const { headWatermark, minWatermark } = row;
|
|
332
|
+
if (headWatermark === null || minWatermark === null) return;
|
|
333
|
+
let outcome;
|
|
334
|
+
if (floor > headWatermark) outcome = "ahead";
|
|
335
|
+
else if (floor < minWatermark || row.boundaryExists === 0) {
|
|
336
|
+
const { seedWatermark } = readChangeLogMeta(db);
|
|
337
|
+
outcome = seedWatermark <= floor ? "violation" : "cold-log";
|
|
338
|
+
} else outcome = "retained";
|
|
339
|
+
this.#floorProbes.add(1, { outcome });
|
|
340
|
+
if (outcome === "violation") this.#lc.error?.("SQLite change log purged history at or above the purge floor. A follower restoring to that watermark would get `too-old` and take a full restore.", { sqliteChangeLogFloorProbe: {
|
|
341
|
+
floor,
|
|
342
|
+
...row
|
|
343
|
+
} });
|
|
344
|
+
return outcome;
|
|
345
|
+
} catch (e) {
|
|
346
|
+
this.#lc.warn?.("unable to probe the SQLite change-log purge floor", e);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/** Resolves once the writer's open transaction has committed or rolled back. */
|
|
351
|
+
#awaitWriterIdle() {
|
|
352
|
+
return new Promise((resolve) => {
|
|
353
|
+
let timer;
|
|
354
|
+
const waiter = () => {
|
|
355
|
+
if (timer !== void 0) this.#clearTimeoutFn(timer);
|
|
356
|
+
this.#writerIdleWaiters.delete(waiter);
|
|
357
|
+
resolve();
|
|
358
|
+
};
|
|
359
|
+
this.#writerIdleWaiters.add(waiter);
|
|
360
|
+
timer = this.#setTimeoutFn(waiter, this.#opts.inTransactionPollMs ?? DEFAULT_IN_TRANSACTION_POLL_MS);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The per-connection statements, rebuilt if the writer's connection ever
|
|
365
|
+
* changes. The purger's prepared statements survive a reseed — SQLite
|
|
366
|
+
* re-prepares on schema change — but not a different `Database`.
|
|
367
|
+
*/
|
|
368
|
+
#cacheFor(db) {
|
|
369
|
+
if (this.#cache?.db !== db) this.#cache = {
|
|
370
|
+
db,
|
|
371
|
+
purger: new SQLiteChangeLogPurger(db),
|
|
372
|
+
plan: db.prepare(SQLITE_CHANGE_LOG_PLAN_SQL),
|
|
373
|
+
belowFloorRemains: db.prepare(BELOW_FLOOR_REMAINS_SQL)
|
|
374
|
+
};
|
|
375
|
+
return this.#cache;
|
|
376
|
+
}
|
|
377
|
+
#skipped(floor, reason, continuation) {
|
|
378
|
+
return {
|
|
379
|
+
floor,
|
|
380
|
+
batches: 0,
|
|
381
|
+
deletedRows: 0,
|
|
382
|
+
stopped: reason,
|
|
383
|
+
probe: void 0,
|
|
384
|
+
continuation
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
//#endregion
|
|
389
|
+
export { SQLiteChangeLogPurgeScheduler };
|
|
390
|
+
|
|
391
|
+
//# sourceMappingURL=sqlite-change-log-purge-scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-change-log-purge-scheduler.js","names":["#lc","#connection","#subscriberAcks","#opts","#now","#setTimeoutFn","#clearTimeoutFn","#yield","#lock","#pausedBy","#writerIdleWaiters","#purgedRows","#passDuration","#passBatches","#declines","#floorProbes","#stopped","#skipped","#runPass","#awaitWriterIdle","#gate","#cacheFor","#probeFloor","#continuation","#cache"],"sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.ts"],"sourcesContent":["import {Lock} from '@rocicorp/lock';\nimport type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport type {Database, Statement} from '../../../../zqlite/src/db.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n getOrCreateValueHistogram,\n} from '../../observability/metrics.ts';\nimport {\n CHANGE_LOG_STREAM_TABLE,\n readChangeLogMeta,\n} from '../replicator/change-log-db.ts';\nimport {SQLiteChangeLogPurger} from '../replicator/sqlite-change-log-purger.ts';\nimport type {SQLiteChangeLogCleanupGuard} from './sqlite-change-log-catchup.ts';\nimport {SQLITE_CHANGE_LOG_PLAN_SQL} from './sqlite-change-log-reader.ts';\n\n/**\n * The duration target for any one purge statement, enforced by the purger's\n * measured-budget feedback. Purge runs on the writer's own\n * connection, in the stream loop's process, so a statement's duration stalls\n * the appending path — and the fan-out behind it — directly. The bench's\n * 100 ms bound was derived for the replicator's write worker; this budget is\n * measured against an event loop that also owes progress to fan-out, catchup,\n * and websocket keepalives, so it is deliberately tighter.\n */\nconst DEFAULT_STATEMENT_TARGET_MS = 20;\n\n/**\n * Batches per pass. This bounds one pass's bookkeeping, not the drain: a pass\n * that hits the limit asks its caller for an immediate continuation.\n */\nconst DEFAULT_MAX_BATCHES_PER_PASS = 100;\n\n/**\n * The backstop poll while waiting for the writer's open transaction to\n * commit. The commit notification normally arrives first; this covers an\n * interrupted stream, whose rollback does not notify.\n */\nconst DEFAULT_IN_TRANSACTION_POLL_MS = 1_000;\n\n/** Why a pass stopped before draining the eligible set. */\nexport type PurgeStopReason =\n // The scheduler was stopped.\n | 'stopped'\n // A snapshot reservation is open.\n | 'paused'\n // The writer has not opened the change log (the file does not exist yet),\n // or has failed soft and deleted it.\n | 'writer-unavailable'\n // A connected subscriber's ACK is behind the floor. Re-checked before every\n // batch so a registration that lands mid-drain is honored by the very next\n // batch.\n | 'subscriber-behind'\n // The per-pass batch limit was reached with history still eligible.\n | 'batch-limit'\n // The pass failed; the error was logged and counted.\n | 'error';\n\n/**\n * The invariant-14 probe's classification of `plan(floor)` after a pass.\n *\n * `too-old` at the floor is not by itself a violation: a log that never held\n * that history — routine at every fork, restore, and rolling restart — returns\n * `too-old` without having purged anything. Only the seed point distinguishes\n * the two, which is why `seedWatermark` is in the v2 meta row:\n *\n * ```\n * violation ⇔ plan(floor) is `too-old` AND seedWatermark <= floor\n * ```\n */\nexport type FloorProbeOutcome =\n // plan(floor) is `range`: the transaction at the floor survives as a\n // restoring follower's catchup boundary.\n | 'retained'\n // The floor is above the log's head, i.e. the log is stale/frozen and the\n // head cap is what held retention. Nothing at the floor was ever purged.\n | 'ahead'\n // plan(floor) is `too-old` but the log was seeded above the floor: it never\n // held that history. Charted, and feeds the fork/resumption reach\n // measurement; not an alert.\n | 'cold-log'\n // plan(floor) is `too-old` and the log demonstrably once held the floor.\n // This is an invariant-14 violation: the only copy a restoring follower\n // could catch up from was purged.\n | 'violation';\n\n/** When the cleanup coordinator should run another pass. */\nexport type PurgeContinuation =\n // The pass hit its batch limit while eligible history remained.\n | 'immediate'\n // Progress depends on time or external state: retention, a subscriber ACK,\n // a reservation, the writer becoming available, or a transient error.\n | 'deferred';\n\nexport type PurgePassResult = {\n /** The external floor the pass used. */\n readonly floor: string;\n readonly batches: number;\n readonly deletedRows: number;\n /** Why the pass ended before draining, if it did. */\n readonly stopped: PurgeStopReason | undefined;\n /** The invariant-14 probe's outcome, when the log was available to probe. */\n readonly probe: FloorProbeOutcome | undefined;\n /** Whether, and how soon, the cleanup coordinator should run another pass. */\n readonly continuation: PurgeContinuation | undefined;\n};\n\nexport type SQLiteChangeLogPurgeSchedulerOptions = {\n /**\n * `sqliteChangeLogRetentionMs`: the minimum retention window, ANDed on top\n * of the external floor. It can only make purge more conservative — the\n * purger never accepts it as a standalone bound.\n */\n readonly retentionMs: number;\n /**\n * `sqliteChangeLogPurgeBatchRows`: the starting chunk size per batch. Not\n * the bound — the statement duration target is.\n */\n readonly batchRows: number;\n readonly statementTargetMs?: number | undefined;\n readonly maxBatchesPerPass?: number | undefined;\n readonly inTransactionPollMs?: number | undefined;\n /** The clock the retention cutoff is measured against. Defaults to `Date.now`. */\n readonly now?: (() => number) | undefined;\n readonly setTimeoutFn?: typeof setTimeout | undefined;\n readonly clearTimeoutFn?: typeof clearTimeout | undefined;\n /**\n * Awaited between batches so fan-out and catchup make progress. Defaults to\n * a macrotask (`setImmediate`), so pending I/O runs before the next batch.\n */\n readonly yieldFn?: (() => Promise<void>) | undefined;\n};\n\ntype ConnectionCache = {\n readonly db: Database;\n readonly purger: SQLiteChangeLogPurger;\n readonly plan: Statement;\n readonly belowFloorRemains: Statement;\n};\n\ntype PlanRow = {\n readonly headWatermark: string | null;\n readonly minWatermark: string | null;\n readonly boundaryExists: number;\n};\n\n/**\n * Whether the cleanup coordinator still owes this floor a pass. Deliberately\n * not the purger's eligibility query, in two ways:\n *\n * - `remains`: rows younger than the retention cutoff are not eligible *yet*,\n * but they will be, and their presence asks the cleanup coordinator for a\n * deferred pass even on a quiet source.\n * - `headBehindFloor`: a head behind the floor means the writer is expected\n * to replay commits below it (e.g. after reconnecting while the backup\n * outran this log). Those rows do not exist to count yet, writer commits\n * only wake an in-flight waiter, and backup monitors never resend an\n * unchanged floor — so reporting quiescence here would disarm cleanup for\n * good. An empty log (no head) defers for the same reason.\n */\nconst BELOW_FLOOR_REMAINS_SQL = /*sql*/ `\n SELECT\n EXISTS (\n SELECT 1 FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" < @floor\n AND \"watermark\" < (SELECT max(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n ) AS \"remains\",\n coalesce(\n (SELECT max(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\") < @floor,\n 1\n ) AS \"headBehindFloor\"\n`;\n\n/**\n * Runs bounded SQLite change-log purge passes on the writer's own connection.\n *\n * The caller supplies the current durable cleanup floor independently of any\n * other change-log implementation and owns the timing of subsequent passes.\n * Within a pass this class drains bounded batches, re-checking live subscriber\n * ACKs before each one and releasing the cleanup guard between them, so a\n * catchup registration or snapshot reservation never waits longer than one\n * batch.\n *\n * Purge windows exist only between the writer's commits: purge shares the\n * writer's connection, so it cannot run inside the writer's open transaction,\n * and a batch that finds one waits for the commit notification instead.\n *\n * The result tells the cleanup coordinator whether the next pass should be\n * immediate or deferred. This class deliberately owns no cleanup timer: the\n * same level-triggered coordinator continues a large drain, advances the\n * retention clock on a quiet source, and retries changing external state.\n */\nexport class SQLiteChangeLogPurgeScheduler {\n readonly #lc: LogContext;\n readonly #connection: () => Database | undefined;\n readonly #subscriberAcks: () => Iterable<string>;\n readonly #opts: SQLiteChangeLogPurgeSchedulerOptions;\n readonly #now: () => number;\n readonly #setTimeoutFn: typeof setTimeout;\n readonly #clearTimeoutFn: typeof clearTimeout;\n readonly #yield: () => Promise<void>;\n\n /**\n * The mutex that serializes purge batches with catchup registration and\n * reservation pauses. It is only ever held across a synchronous section —\n * one batch, or one registration — never across a yield, which is what\n * bounds any waiter to a single batch's duration.\n */\n readonly #lock = new Lock();\n\n /**\n * Open snapshot reservations, reference-counted by taskID. Purge pauses\n * while any is open. Counted rather than a set so that a reservation retry\n * -- whose cancel-then-recreate can resolve its old resume after its new\n * suspend -- still nets out.\n */\n readonly #pausedBy = new Map<string, number>();\n\n /** Waiters for the writer's next commit (or rollback). */\n readonly #writerIdleWaiters = new Set<() => void>();\n\n readonly #purgedRows = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.purged_rows',\n 'Rows removed from the SQLite change log by the purge scheduler.',\n );\n readonly #passDuration = getOrCreateLatencyHistogram(\n 'replica',\n 'sqlite_change_log.purge_cycle_duration',\n 'Duration of one SQLite change-log purge pass, including the yields ' +\n 'between its batches.',\n );\n readonly #passBatches = getOrCreateValueHistogram(\n 'replica',\n 'sqlite_change_log.purge_cycle_batches',\n {\n description: 'Purge batches run per SQLite change-log purge pass.',\n unit: '{batch}',\n bucketBoundaries: [1, 2, 5, 10, 25, 50, 100, 250],\n },\n );\n readonly #declines = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.purge_declined',\n 'SQLite change-log purge passes that ended before draining, by reason.',\n );\n readonly #floorProbes = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.purge_floor_probe',\n 'Invariant-14 probe outcomes: whether the transaction at the purge ' +\n 'floor is still servable after each pass. \"violation\" means history ' +\n 'at or above the floor was purged and alerts; \"cold-log\" means the ' +\n 'log never held it, which is routine at every fork and resumption.',\n );\n\n #cache: ConnectionCache | undefined;\n #stopped = false;\n\n /**\n * The guard that brackets catchup registration: required-head capture and\n * `Forwarder.add()` run under the same mutex as purge batches, so the\n * registered ACK is visible to `Forwarder.getAcks()` before the next batch\n * evaluates its gate.\n */\n readonly cleanupGuard: SQLiteChangeLogCleanupGuard = {\n runWhilePurgeBlocked: register => this.#lock.withLock(register),\n };\n\n /**\n * @param connection the writer's own connection (§3.3). `undefined` until\n * the writer's first reconcile creates the file, and again after the\n * writer fails soft and deletes it — both of which defer the pass rather\n * than fail it, so a later coordinator pass picks up a late-appearing\n * file without a restart.\n * @param subscriberAcks live subscriber ACKs, re-read before every batch so\n * the supplied floor remains safe throughout an asynchronous drain.\n */\n constructor(\n lc: LogContext,\n connection: () => Database | undefined,\n subscriberAcks: () => Iterable<string>,\n opts: SQLiteChangeLogPurgeSchedulerOptions,\n ) {\n assert(\n Number.isSafeInteger(opts.retentionMs) && opts.retentionMs > 0,\n 'sqliteChangeLogRetentionMs must be a positive integer',\n );\n assert(\n Number.isSafeInteger(opts.batchRows) && opts.batchRows > 0,\n 'sqliteChangeLogPurgeBatchRows must be a positive integer',\n );\n this.#lc = lc.withContext('component', 'sqlite-change-log-purge');\n this.#connection = connection;\n this.#subscriberAcks = subscriberAcks;\n this.#opts = opts;\n this.#now = opts.now ?? Date.now;\n this.#setTimeoutFn = opts.setTimeoutFn ?? setTimeout;\n this.#clearTimeoutFn = opts.clearTimeoutFn ?? clearTimeout;\n this.#yield =\n opts.yieldFn ??\n (() => new Promise<void>(resolve => setImmediate(resolve)));\n }\n\n /**\n * Runs one purge pass for the current durable cleanup `floor`.\n * Never rejects; a failed pass is logged and reported as `stopped: 'error'`.\n */\n async purge(floor: string): Promise<PurgePassResult> {\n if (this.#stopped) {\n return this.#skipped(floor, 'stopped');\n }\n try {\n return await this.#runPass(floor);\n } catch (e) {\n this.#lc.warn?.('error purging the SQLite change log', e);\n this.#declines.add(1, {reason: 'error'});\n return this.#skipped(floor, 'error', 'deferred');\n }\n }\n\n /**\n * Pauses purging while `taskID` holds a snapshot reservation. The returned\n * promise resolves once no purge batch is in flight, i.e. once the bounds a\n * backup monitor is about to advertise cannot be invalidated by a batch\n * that had already been dispatched.\n */\n pause(taskID: string): Promise<void> {\n this.#pausedBy.set(taskID, (this.#pausedBy.get(taskID) ?? 0) + 1);\n return this.#lock.withLock(() => {});\n }\n\n /**\n * Ends one of `taskID`'s pauses. A later coordinator pass can purge once\n * every reservation has ended. Unknown `taskID`s are ignored.\n */\n resume(taskID: string): void {\n const count = this.#pausedBy.get(taskID);\n if (count === undefined) {\n return;\n }\n if (count > 1) {\n this.#pausedBy.set(taskID, count - 1);\n return;\n }\n this.#pausedBy.delete(taskID);\n }\n\n /**\n * Notes that the writer's open transaction has ended, waking any batch\n * waiting for a purge window. A missed notification costs one poll\n * interval, never correctness.\n */\n onWriterIdle(): void {\n for (const waiter of this.#writerIdleWaiters) {\n waiter();\n }\n this.#writerIdleWaiters.clear();\n }\n\n stop(): void {\n this.#stopped = true;\n this.onWriterIdle();\n }\n\n async #runPass(floor: string): Promise<PurgePassResult> {\n const startedAt = performance.now();\n let batches = 0;\n let deletedRows = 0;\n let stopped: PurgeStopReason | undefined;\n let db: Database | undefined;\n\n try {\n for (;;) {\n if (this.#stopped) {\n stopped = 'stopped';\n break;\n }\n if (this.#pausedBy.size > 0) {\n stopped = 'paused';\n break;\n }\n const connection = this.#connection();\n if (connection === undefined) {\n db = undefined;\n this.#lc.debug?.(\n 'skipping SQLite change-log purge: the writer has not opened the log',\n );\n stopped = 'writer-unavailable';\n break;\n }\n db = connection;\n if (\n batches >=\n Math.max(\n 1,\n this.#opts.maxBatchesPerPass ?? DEFAULT_MAX_BATCHES_PER_PASS,\n )\n ) {\n stopped = 'batch-limit';\n break;\n }\n if (connection.inTransaction) {\n // Purge windows exist only between the writer's commits (§3.3).\n await this.#awaitWriterIdle();\n continue;\n }\n\n const outcome = await this.#lock.withLock(() => {\n // Everything the wait above established is re-checked under the lock:\n // the writer may have begun another transaction or failed soft while\n // this batch's acquisition was queued behind a registration.\n if (this.#connection() !== connection || connection.inTransaction) {\n return {kind: 'retry'} as const;\n }\n const declined = this.#gate(floor);\n if (declined !== undefined) {\n return {kind: 'declined', reason: declined} as const;\n }\n const result = this.#cacheFor(connection).purger.purgeBatch({\n externalFloor: floor,\n retentionCutoffMs: this.#now() - this.#opts.retentionMs,\n maxRows: this.#opts.batchRows,\n maxDurationMs:\n this.#opts.statementTargetMs ?? DEFAULT_STATEMENT_TARGET_MS,\n });\n return {kind: 'purged', result} as const;\n });\n\n if (outcome.kind === 'retry') {\n continue;\n }\n if (outcome.kind === 'declined') {\n stopped = outcome.reason;\n break;\n }\n batches++;\n deletedRows += outcome.result.deletedRows;\n if (!outcome.result.moreEligible) {\n break;\n }\n // Release the loop — and the guard, released above — so fan-out,\n // catchup, and registrations make progress between batches.\n await this.#yield();\n }\n } catch (e) {\n // Earlier batches committed independently and must remain visible in the\n // result and metrics. Finalize the partial pass, including its floor\n // probe, rather than replacing it with an empty skipped result.\n this.#lc.warn?.('error purging the SQLite change log', e);\n stopped = 'error';\n }\n\n const probe = db === undefined ? undefined : this.#probeFloor(db, floor);\n let continuation: PurgeContinuation | undefined;\n try {\n continuation = this.#continuation(floor, stopped);\n } catch (e) {\n // A connection can fail soft between the pass and this level check. Keep\n // completed batches visible and ask the coordinator to retry later.\n this.#lc.warn?.(\n 'unable to determine whether SQLite change-log purge should continue',\n e,\n );\n continuation = 'deferred';\n stopped ??= 'error';\n }\n if (deletedRows > 0) {\n this.#purgedRows.add(deletedRows);\n }\n if (batches > 0) {\n this.#passBatches.record(batches);\n this.#passDuration.recordMs(performance.now() - startedAt);\n }\n if (stopped !== undefined) {\n this.#declines.add(1, {reason: stopped});\n }\n if (batches > 0 || stopped !== 'writer-unavailable') {\n this.#lc.debug?.('SQLite change-log purge pass', {\n sqliteChangeLogPurge: {\n floor,\n batches,\n deletedRows,\n stopped,\n probe,\n continuation,\n },\n });\n }\n return {floor, batches, deletedRows, stopped, probe, continuation};\n }\n\n /**\n * Reports whether the level-triggered cleanup coordinator should run again.\n * Rows below the floor but inside the retention window require a deferred\n * pass even when no new backup watermark arrives, and so does a head behind\n * the floor: the rows the writer replays into that gap arrive after this\n * check, with no other event armed to reap them.\n */\n #continuation(\n floor: string,\n stopped: PurgeStopReason | undefined,\n ): PurgeContinuation | undefined {\n switch (stopped) {\n case 'batch-limit':\n return 'immediate';\n case 'stopped':\n return undefined;\n case 'paused':\n case 'subscriber-behind':\n case 'writer-unavailable':\n case 'error':\n return 'deferred';\n case undefined: {\n // A read is safe even inside the writer's open transaction; it just\n // observes the uncommitted head, which is never below the floor.\n const db = this.#connection();\n if (db === undefined) {\n return 'deferred';\n }\n const {remains, headBehindFloor} = this.#cacheFor(\n db,\n ).belowFloorRemains.get<{\n remains: number;\n headBehindFloor: number;\n }>({floor});\n return remains === 0 && headBehindFloor === 0 ? undefined : 'deferred';\n }\n }\n }\n\n /**\n * No purge while a connected subscriber is behind the supplied floor. The\n * cleanup coordinator has already provided the reconnect grace period; an\n * empty live set places no additional constraint on the durable floor.\n */\n #gate(floor: string): 'subscriber-behind' | undefined {\n for (const ack of this.#subscriberAcks()) {\n if (ack < floor) {\n return 'subscriber-behind';\n }\n }\n return undefined;\n }\n\n /**\n * The live check on invariant 14 — what makes running this slice dark worth\n * anything: in `write` mode nothing reads the log, so a wrong floor would\n * otherwise stay invisible until the canary rollout two slices later.\n */\n #probeFloor(db: Database, floor: string): FloorProbeOutcome | undefined {\n try {\n const cache = this.#cacheFor(db);\n const row = cache.plan.get<PlanRow>({fromWatermark: floor});\n const {headWatermark, minWatermark} = row;\n if (headWatermark === null || minWatermark === null) {\n return undefined;\n }\n let outcome: FloorProbeOutcome;\n if (floor > headWatermark) {\n outcome = 'ahead';\n } else if (floor < minWatermark || row.boundaryExists === 0) {\n const {seedWatermark} = readChangeLogMeta(db);\n outcome = seedWatermark <= floor ? 'violation' : 'cold-log';\n } else {\n outcome = 'retained';\n }\n this.#floorProbes.add(1, {outcome});\n if (outcome === 'violation') {\n this.#lc.error?.(\n 'SQLite change log purged history at or above the purge floor. A ' +\n 'follower restoring to that watermark would get `too-old` and ' +\n 'take a full restore.',\n {sqliteChangeLogFloorProbe: {floor, ...row}},\n );\n }\n return outcome;\n } catch (e) {\n this.#lc.warn?.('unable to probe the SQLite change-log purge floor', e);\n return undefined;\n }\n }\n\n /** Resolves once the writer's open transaction has committed or rolled back. */\n #awaitWriterIdle(): Promise<void> {\n return new Promise<void>(resolve => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const waiter = () => {\n if (timer !== undefined) {\n this.#clearTimeoutFn(timer);\n }\n this.#writerIdleWaiters.delete(waiter);\n resolve();\n };\n this.#writerIdleWaiters.add(waiter);\n timer = this.#setTimeoutFn(\n waiter,\n this.#opts.inTransactionPollMs ?? DEFAULT_IN_TRANSACTION_POLL_MS,\n );\n });\n }\n\n /**\n * The per-connection statements, rebuilt if the writer's connection ever\n * changes. The purger's prepared statements survive a reseed — SQLite\n * re-prepares on schema change — but not a different `Database`.\n */\n #cacheFor(db: Database): ConnectionCache {\n if (this.#cache?.db !== db) {\n this.#cache = {\n db,\n purger: new SQLiteChangeLogPurger(db),\n plan: db.prepare(SQLITE_CHANGE_LOG_PLAN_SQL),\n belowFloorRemains: db.prepare(BELOW_FLOOR_REMAINS_SQL),\n };\n }\n return this.#cache;\n }\n\n #skipped(\n floor: string,\n reason: PurgeStopReason,\n continuation?: PurgeContinuation | undefined,\n ): PurgePassResult {\n return {\n floor,\n batches: 0,\n deletedRows: 0,\n stopped: reason,\n probe: undefined,\n continuation,\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA0BA,IAAM,8BAA8B;;;;;AAMpC,IAAM,+BAA+B;;;;;;AAOrC,IAAM,iCAAiC;;;;;;;;;;;;;;;AA0HvC,IAAM,0BAAkC;;;uBAGjB,wBAAwB;;2DAEY,wBAAwB;;;uCAG5C,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;AAwB/D,IAAa,gCAAb,MAA2C;CACzC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;;;;;;CAQA,QAAiB,IAAI,KAAK;;;;;;;CAQ1B,4BAAqB,IAAI,IAAoB;;CAG7C,qCAA8B,IAAI,IAAgB;CAElD,cAAuB,mBACrB,WACA,iCACA,iEACF;CACA,gBAAyB,4BACvB,WACA,0CACA,yFAEF;CACA,eAAwB,0BACtB,WACA,yCACA;EACE,aAAa;EACb,MAAM;EACN,kBAAkB;GAAC;GAAG;GAAG;GAAG;GAAI;GAAI;GAAI;GAAK;EAAG;CAClD,CACF;CACA,YAAqB,mBACnB,WACA,oCACA,uEACF;CACA,eAAwB,mBACtB,WACA,uCACA,8QAIF;CAEA;CACA,WAAW;;;;;;;CAQX,eAAqD,EACnD,uBAAsB,aAAY,KAAKQ,MAAM,SAAS,QAAQ,EAChE;;;;;;;;;;CAWA,YACE,IACA,YACA,gBACA,MACA;EACA,OACE,OAAO,cAAc,KAAK,WAAW,KAAK,KAAK,cAAc,GAC7D,uDACF;EACA,OACE,OAAO,cAAc,KAAK,SAAS,KAAK,KAAK,YAAY,GACzD,0DACF;EACA,KAAKR,MAAM,GAAG,YAAY,aAAa,yBAAyB;EAChE,KAAKC,cAAc;EACnB,KAAKC,kBAAkB;EACvB,KAAKC,QAAQ;EACb,KAAKC,OAAO,KAAK,OAAO,KAAK;EAC7B,KAAKC,gBAAgB,KAAK,gBAAgB;EAC1C,KAAKC,kBAAkB,KAAK,kBAAkB;EAC9C,KAAKC,SACH,KAAK,kBACE,IAAI,SAAc,YAAW,aAAa,OAAO,CAAC;CAC7D;;;;;CAMA,MAAM,MAAM,OAAyC;EACnD,IAAI,KAAKS,UACP,OAAO,KAAKC,SAAS,OAAO,SAAS;EAEvC,IAAI;GACF,OAAO,MAAM,KAAKC,SAAS,KAAK;EAClC,SAAS,GAAG;GACV,KAAKlB,IAAI,OAAO,uCAAuC,CAAC;GACxD,KAAKc,UAAU,IAAI,GAAG,EAAC,QAAQ,QAAO,CAAC;GACvC,OAAO,KAAKG,SAAS,OAAO,SAAS,UAAU;EACjD;CACF;;;;;;;CAQA,MAAM,QAA+B;EACnC,KAAKR,UAAU,IAAI,SAAS,KAAKA,UAAU,IAAI,MAAM,KAAK,KAAK,CAAC;EAChE,OAAO,KAAKD,MAAM,eAAe,CAAC,CAAC;CACrC;;;;;CAMA,OAAO,QAAsB;EAC3B,MAAM,QAAQ,KAAKC,UAAU,IAAI,MAAM;EACvC,IAAI,UAAU,KAAA,GACZ;EAEF,IAAI,QAAQ,GAAG;GACb,KAAKA,UAAU,IAAI,QAAQ,QAAQ,CAAC;GACpC;EACF;EACA,KAAKA,UAAU,OAAO,MAAM;CAC9B;;;;;;CAOA,eAAqB;EACnB,KAAK,MAAM,UAAU,KAAKC,oBACxB,OAAO;EAET,KAAKA,mBAAmB,MAAM;CAChC;CAEA,OAAa;EACX,KAAKM,WAAW;EAChB,KAAK,aAAa;CACpB;CAEA,MAAME,SAAS,OAAyC;EACtD,MAAM,YAAY,YAAY,IAAI;EAClC,IAAI,UAAU;EACd,IAAI,cAAc;EAClB,IAAI;EACJ,IAAI;EAEJ,IAAI;GACF,SAAS;IACP,IAAI,KAAKF,UAAU;KACjB,UAAU;KACV;IACF;IACA,IAAI,KAAKP,UAAU,OAAO,GAAG;KAC3B,UAAU;KACV;IACF;IACA,MAAM,aAAa,KAAKR,YAAY;IACpC,IAAI,eAAe,KAAA,GAAW;KAC5B,KAAK,KAAA;KACL,KAAKD,IAAI,QACP,qEACF;KACA,UAAU;KACV;IACF;IACA,KAAK;IACL,IACE,WACA,KAAK,IACH,GACA,KAAKG,MAAM,qBAAqB,4BAClC,GACA;KACA,UAAU;KACV;IACF;IACA,IAAI,WAAW,eAAe;KAE5B,MAAM,KAAKgB,iBAAiB;KAC5B;IACF;IAEA,MAAM,UAAU,MAAM,KAAKX,MAAM,eAAe;KAI9C,IAAI,KAAKP,YAAY,MAAM,cAAc,WAAW,eAClD,OAAO,EAAC,MAAM,QAAO;KAEvB,MAAM,WAAW,KAAKmB,MAAM,KAAK;KACjC,IAAI,aAAa,KAAA,GACf,OAAO;MAAC,MAAM;MAAY,QAAQ;KAAQ;KAS5C,OAAO;MAAC,MAAM;MAAU,QAPT,KAAKC,UAAU,UAAU,EAAE,OAAO,WAAW;OAC1D,eAAe;OACf,mBAAmB,KAAKjB,KAAK,IAAI,KAAKD,MAAM;OAC5C,SAAS,KAAKA,MAAM;OACpB,eACE,KAAKA,MAAM,qBAAqB;MACpC,CACwB;KAAM;IAChC,CAAC;IAED,IAAI,QAAQ,SAAS,SACnB;IAEF,IAAI,QAAQ,SAAS,YAAY;KAC/B,UAAU,QAAQ;KAClB;IACF;IACA;IACA,eAAe,QAAQ,OAAO;IAC9B,IAAI,CAAC,QAAQ,OAAO,cAClB;IAIF,MAAM,KAAKI,OAAO;GACpB;EACF,SAAS,GAAG;GAIV,KAAKP,IAAI,OAAO,uCAAuC,CAAC;GACxD,UAAU;EACZ;EAEA,MAAM,QAAQ,OAAO,KAAA,IAAY,KAAA,IAAY,KAAKsB,YAAY,IAAI,KAAK;EACvE,IAAI;EACJ,IAAI;GACF,eAAe,KAAKC,cAAc,OAAO,OAAO;EAClD,SAAS,GAAG;GAGV,KAAKvB,IAAI,OACP,uEACA,CACF;GACA,eAAe;GACf,YAAY;EACd;EACA,IAAI,cAAc,GAChB,KAAKW,YAAY,IAAI,WAAW;EAElC,IAAI,UAAU,GAAG;GACf,KAAKE,aAAa,OAAO,OAAO;GAChC,KAAKD,cAAc,SAAS,YAAY,IAAI,IAAI,SAAS;EAC3D;EACA,IAAI,YAAY,KAAA,GACd,KAAKE,UAAU,IAAI,GAAG,EAAC,QAAQ,QAAO,CAAC;EAEzC,IAAI,UAAU,KAAK,YAAY,sBAC7B,KAAKd,IAAI,QAAQ,gCAAgC,EAC/C,sBAAsB;GACpB;GACA;GACA;GACA;GACA;GACA;EACF,EACF,CAAC;EAEH,OAAO;GAAC;GAAO;GAAS;GAAa;GAAS;GAAO;EAAY;CACnE;;;;;;;;CASA,cACE,OACA,SAC+B;EAC/B,QAAQ,SAAR;GACE,KAAK,eACH,OAAO;GACT,KAAK,WACH;GACF,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK,SACH,OAAO;GACT,KAAK,KAAA,GAAW;IAGd,MAAM,KAAK,KAAKC,YAAY;IAC5B,IAAI,OAAO,KAAA,GACT,OAAO;IAET,MAAM,EAAC,SAAS,oBAAmB,KAAKoB,UACtC,EACF,EAAE,kBAAkB,IAGjB,EAAC,MAAK,CAAC;IACV,OAAO,YAAY,KAAK,oBAAoB,IAAI,KAAA,IAAY;GAC9D;EACF;CACF;;;;;;CAOA,MAAM,OAAgD;EACpD,KAAK,MAAM,OAAO,KAAKnB,gBAAgB,GACrC,IAAI,MAAM,OACR,OAAO;CAIb;;;;;;CAOA,YAAY,IAAc,OAA8C;EACtE,IAAI;GAEF,MAAM,MADQ,KAAKmB,UAAU,EACjB,EAAM,KAAK,IAAa,EAAC,eAAe,MAAK,CAAC;GAC1D,MAAM,EAAC,eAAe,iBAAgB;GACtC,IAAI,kBAAkB,QAAQ,iBAAiB,MAC7C;GAEF,IAAI;GACJ,IAAI,QAAQ,eACV,UAAU;QACL,IAAI,QAAQ,gBAAgB,IAAI,mBAAmB,GAAG;IAC3D,MAAM,EAAC,kBAAiB,kBAAkB,EAAE;IAC5C,UAAU,iBAAiB,QAAQ,cAAc;GACnD,OACE,UAAU;GAEZ,KAAKN,aAAa,IAAI,GAAG,EAAC,QAAO,CAAC;GAClC,IAAI,YAAY,aACd,KAAKf,IAAI,QACP,qJAGA,EAAC,2BAA2B;IAAC;IAAO,GAAG;GAAG,EAAC,CAC7C;GAEF,OAAO;EACT,SAAS,GAAG;GACV,KAAKA,IAAI,OAAO,qDAAqD,CAAC;GACtE;EACF;CACF;;CAGA,mBAAkC;EAChC,OAAO,IAAI,SAAc,YAAW;GAClC,IAAI;GACJ,MAAM,eAAe;IACnB,IAAI,UAAU,KAAA,GACZ,KAAKM,gBAAgB,KAAK;IAE5B,KAAKI,mBAAmB,OAAO,MAAM;IACrC,QAAQ;GACV;GACA,KAAKA,mBAAmB,IAAI,MAAM;GAClC,QAAQ,KAAKL,cACX,QACA,KAAKF,MAAM,uBAAuB,8BACpC;EACF,CAAC;CACH;;;;;;CAOA,UAAU,IAA+B;EACvC,IAAI,KAAKqB,QAAQ,OAAO,IACtB,KAAKA,SAAS;GACZ;GACA,QAAQ,IAAI,sBAAsB,EAAE;GACpC,MAAM,GAAG,QAAQ,0BAA0B;GAC3C,mBAAmB,GAAG,QAAQ,uBAAuB;EACvD;EAEF,OAAO,KAAKA;CACd;CAEA,SACE,OACA,QACA,cACiB;EACjB,OAAO;GACL;GACA,SAAS;GACT,aAAa;GACb,SAAS;GACT,OAAO,KAAA;GACP;EACF;CACF;AACF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { LogContext } from '@rocicorp/logger';
|
|
2
|
+
import type { WatermarkedChange } from './change-streamer.ts';
|
|
3
|
+
export type CatchupPlan = {
|
|
4
|
+
readonly kind: 'range';
|
|
5
|
+
readonly minWatermark: string;
|
|
6
|
+
readonly headWatermark: string;
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: 'ahead';
|
|
9
|
+
readonly headWatermark: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: 'too-old';
|
|
12
|
+
readonly minWatermark: string;
|
|
13
|
+
readonly headWatermark: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: 'not-ready';
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Seeks to the final position so validating a boundary does not scan every row
|
|
19
|
+
* of a large transaction. Exported so the focused query-plan test covers the
|
|
20
|
+
* production subquery.
|
|
21
|
+
*/
|
|
22
|
+
export declare const SQLITE_CHANGE_LOG_BOUNDARY_SQL = "\n SELECT\n \"precommit\" = @fromWatermark AND\n \"tag\" = 'commit' AS \"boundaryExists\"\n FROM \"_zero.changeLogStream\"\n WHERE \"watermark\" = @fromWatermark\n ORDER BY \"pos\" DESC\n LIMIT 1\n";
|
|
23
|
+
/**
|
|
24
|
+
* The log describes its own bounds; nothing here reads the replica.
|
|
25
|
+
* `max("watermark")` is the head because a transaction's rows commit atomically,
|
|
26
|
+
* so the log never ends mid-transaction.
|
|
27
|
+
*
|
|
28
|
+
* `min` and `max` are deliberately kept as *separate* scalar subqueries: SQLite
|
|
29
|
+
* applies its single-aggregate index optimization only when the aggregate is
|
|
30
|
+
* alone in its query, so `SELECT min(x), max(x) FROM t` degrades to a full scan
|
|
31
|
+
* while two scalar subqueries are two index seeks. The query-plan test pins
|
|
32
|
+
* this.
|
|
33
|
+
*
|
|
34
|
+
* Exported so that test can cover the production query.
|
|
35
|
+
*/
|
|
36
|
+
export declare const SQLITE_CHANGE_LOG_PLAN_SQL = "\n SELECT\n (SELECT max(\"watermark\") FROM \"_zero.changeLogStream\")\n AS \"headWatermark\",\n (SELECT min(\"watermark\") FROM \"_zero.changeLogStream\")\n AS \"minWatermark\",\n coalesce((\n SELECT\n \"precommit\" = @fromWatermark AND\n \"tag\" = 'commit' AS \"boundaryExists\"\n FROM \"_zero.changeLogStream\"\n WHERE \"watermark\" = @fromWatermark\n ORDER BY \"pos\" DESC\n LIMIT 1\n), 0) AS \"boundaryExists\"\n";
|
|
37
|
+
/** Exported so the focused query-plan test covers the production query. */
|
|
38
|
+
export declare const SQLITE_CHANGE_LOG_READ_BATCH_SQL = "\n SELECT\n \"watermark\",\n \"pos\",\n \"tag\",\n \"change\"\n FROM \"_zero.changeLogStream\"\n WHERE (\"watermark\", \"pos\") > (?, ?)\n AND \"watermark\" <= ?\n ORDER BY \"watermark\", \"pos\"\n LIMIT ?\n";
|
|
39
|
+
/**
|
|
40
|
+
* Reads the change log without retaining a SQLite snapshot while a consumer
|
|
41
|
+
* processes a batch.
|
|
42
|
+
*
|
|
43
|
+
* The log lives in its own database beside the replica
|
|
44
|
+
* (`changeLogFileName(replicaFile)`), which this reader opens read-only; it
|
|
45
|
+
* never opens the replica. The connection therefore keeps the journal mode
|
|
46
|
+
* configured by the change-log writer. Callers pin a head with {@link plan}
|
|
47
|
+
* before passing it to {@link read}.
|
|
48
|
+
*
|
|
49
|
+
* A change-streamer can start before the replicator has created the log, so the
|
|
50
|
+
* stream table may not exist yet. Statements are prepared on first use rather
|
|
51
|
+
* than in the constructor, and {@link plan} reports `not-ready` until they can
|
|
52
|
+
* be; a reader constructed too early becomes usable once the writer catches up.
|
|
53
|
+
*/
|
|
54
|
+
export declare class SQLiteChangeLogReader implements Disposable {
|
|
55
|
+
#private;
|
|
56
|
+
constructor(lc: LogContext, changeLogFile: string);
|
|
57
|
+
plan(fromWatermark: string): CatchupPlan;
|
|
58
|
+
read(fromWatermark: string, throughWatermark: string, batchSize: number, signal?: AbortSignal | undefined): AsyncIterable<readonly WatermarkedChange[]>;
|
|
59
|
+
close(): void;
|
|
60
|
+
[Symbol.dispose](): void;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=sqlite-change-log-reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-change-log-reader.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-reader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AASjD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,sBAAsB,CAAC;AAE5D,MAAM,MAAM,WAAW,GACnB;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,GACD;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAAC,GACxD;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,GAKD;IAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAC,CAAC;AAiBjC;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,uNAQ1C,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,0BAA0B,ucAOtC,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,gCAAgC,wOAW5C,CAAC;AAMF;;;;;;;;;;;;;;GAcG;AACH,qBAAa,qBAAsB,YAAW,UAAU;;gBAK1C,EAAE,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM;IAQjD,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,WAAW;IAqBjC,IAAI,CACT,aAAa,EAAE,MAAM,EACrB,gBAAgB,EAAE,MAAM,EACxB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,GAC/B,aAAa,CAAC,SAAS,iBAAiB,EAAE,CAAC;IA2D9C,KAAK,IAAI,IAAI;IAQb,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAuCzB"}
|