@rocicorp/zero 1.3.0-canary.1 → 1.3.0-canary.3
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/analyze-query/src/bin-analyze.js +7 -7
- package/out/analyze-query/src/bin-transform.js +3 -3
- package/out/ast-to-zql/src/bin.js +2 -2
- package/out/shared/src/logging.d.ts.map +1 -1
- package/out/shared/src/logging.js +1 -1
- package/out/shared/src/logging.js.map +1 -1
- 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/zero/package.js +91 -89
- 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/src/zero-out.js +1 -1
- package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
- package/out/zero-cache/src/auth/auth.js.map +1 -1
- package/out/zero-cache/src/auth/load-permissions.js +2 -2
- package/out/zero-cache/src/auth/write-authorizer.d.ts.map +1 -1
- package/out/zero-cache/src/auth/write-authorizer.js +5 -14
- package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
- package/out/zero-cache/src/config/network.d.ts +1 -1
- package/out/zero-cache/src/config/network.d.ts.map +1 -1
- package/out/zero-cache/src/config/network.js +1 -1
- package/out/zero-cache/src/config/network.js.map +1 -1
- package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
- package/out/zero-cache/src/config/normalize.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +5 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +16 -3
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/db/lite-tables.d.ts.map +1 -1
- package/out/zero-cache/src/db/lite-tables.js +3 -3
- package/out/zero-cache/src/db/lite-tables.js.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.d.ts +43 -40
- package/out/zero-cache/src/db/transaction-pool.d.ts.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.js +76 -56
- package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
- package/out/zero-cache/src/observability/events.d.ts.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/scripts/decommission.js +1 -1
- package/out/zero-cache/src/scripts/deploy-permissions.js +2 -2
- package/out/zero-cache/src/scripts/permissions.js +1 -1
- package/out/zero-cache/src/server/anonymous-otel-start.d.ts.map +1 -1
- package/out/zero-cache/src/server/anonymous-otel-start.js +3 -3
- package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +26 -11
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/logging.d.ts +1 -3
- package/out/zero-cache/src/server/logging.d.ts.map +1 -1
- package/out/zero-cache/src/server/logging.js +6 -3
- package/out/zero-cache/src/server/logging.js.map +1 -1
- package/out/zero-cache/src/server/main.d.ts.map +1 -1
- package/out/zero-cache/src/server/main.js +26 -26
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/mutator.js +4 -2
- package/out/zero-cache/src/server/mutator.js.map +1 -1
- package/out/zero-cache/src/server/otel-log-sink.d.ts.map +1 -1
- package/out/zero-cache/src/server/otel-log-sink.js +0 -2
- package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
- package/out/zero-cache/src/server/otel-start.d.ts +1 -1
- package/out/zero-cache/src/server/otel-start.d.ts.map +1 -1
- package/out/zero-cache/src/server/otel-start.js +7 -3
- package/out/zero-cache/src/server/otel-start.js.map +1 -1
- package/out/zero-cache/src/server/reaper.js +6 -6
- package/out/zero-cache/src/server/reaper.js.map +1 -1
- package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/server/replicator.js +5 -3
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.js +2 -2
- 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 +13 -12
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/server/worker-dispatcher.js +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/replica-schema.js +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +4 -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 +2 -2
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +58 -3
- 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 +208 -51
- 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.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-monitor.js +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +1 -1
- 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 +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.d.ts +5 -1
- 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 +10 -7
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/replica-monitor.js +2 -2
- package/out/zero-cache/src/services/change-streamer/storer.d.ts +19 -2
- package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js +69 -5
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/services/heapz.d.ts.map +1 -1
- package/out/zero-cache/src/services/heapz.js +1 -1
- package/out/zero-cache/src/services/heapz.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts +2 -1
- package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +10 -7
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts +15 -4
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +31 -31
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.d.ts +28 -28
- package/out/zero-cache/src/services/replicator/change-processor.js +2 -2
- package/out/zero-cache/src/services/replicator/incremental-sync.js +1 -1
- package/out/zero-cache/src/services/replicator/schema/replication-state.js +1 -1
- package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
- package/out/zero-cache/src/services/replicator/write-worker.js +3 -3
- 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 -2
- package/out/zero-cache/src/services/run-ast.js.map +1 -1
- package/out/zero-cache/src/services/statz.d.ts.map +1 -1
- package/out/zero-cache/src/services/statz.js +2 -2
- package/out/zero-cache/src/services/statz.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/active-users-gauge.js +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +2 -2
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts.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-purger.js +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js +2 -2
- package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +6 -16
- 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 +29 -37
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js +3 -3
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +2 -2
- 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 +6 -6
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/profiler.d.ts.map +1 -1
- package/out/zero-cache/src/types/profiler.js.map +1 -1
- 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.d.ts +1 -1
- package/out/zero-cache/src/types/streams.d.ts.map +1 -1
- package/out/zero-cache/src/types/streams.js.map +1 -1
- package/out/zero-cache/src/types/websocket-handoff.d.ts +1 -1
- package/out/zero-cache/src/types/websocket-handoff.d.ts.map +1 -1
- package/out/zero-cache/src/types/websocket-handoff.js.map +1 -1
- package/out/zero-cache/src/workers/connection.d.ts +1 -1
- package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/mutator.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +2 -2
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/crud-impl.d.ts.map +1 -1
- package/out/zero-client/src/client/crud-impl.js +4 -13
- package/out/zero-client/src/client/crud-impl.js.map +1 -1
- package/out/zero-client/src/client/ivm-branch.d.ts.map +1 -1
- package/out/zero-client/src/client/ivm-branch.js +4 -13
- package/out/zero-client/src/client/ivm-branch.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-protocol/src/error.d.ts.map +1 -1
- package/out/zero-protocol/src/error.js +1 -1
- package/out/zero-protocol/src/error.js.map +1 -1
- package/out/zero-solid/src/solid-view.d.ts.map +1 -1
- package/out/zero-solid/src/solid-view.js +13 -13
- package/out/zero-solid/src/solid-view.js.map +1 -1
- package/out/zql/src/builder/builder.d.ts.map +1 -1
- package/out/zql/src/builder/builder.js.map +1 -1
- package/out/zql/src/ivm/array-view.d.ts.map +1 -1
- package/out/zql/src/ivm/array-view.js +26 -1
- package/out/zql/src/ivm/array-view.js.map +1 -1
- package/out/zql/src/ivm/change-index-enum.d.ts +9 -0
- package/out/zql/src/ivm/change-index-enum.d.ts.map +1 -0
- package/out/zql/src/ivm/change-index.d.ts +5 -0
- package/out/zql/src/ivm/change-index.d.ts.map +1 -0
- package/out/zql/src/ivm/change-type-enum.d.ts +9 -0
- package/out/zql/src/ivm/change-type-enum.d.ts.map +1 -0
- package/out/zql/src/ivm/change-type.d.ts +5 -0
- package/out/zql/src/ivm/change-type.d.ts.map +1 -0
- package/out/zql/src/ivm/change.d.ts +20 -22
- package/out/zql/src/ivm/change.d.ts.map +1 -1
- package/out/zql/src/ivm/change.js +33 -0
- package/out/zql/src/ivm/change.js.map +1 -0
- package/out/zql/src/ivm/exists.d.ts.map +1 -1
- package/out/zql/src/ivm/exists.js +27 -38
- package/out/zql/src/ivm/exists.js.map +1 -1
- package/out/zql/src/ivm/fan-in.d.ts +3 -2
- package/out/zql/src/ivm/fan-in.d.ts.map +1 -1
- package/out/zql/src/ivm/fan-in.js.map +1 -1
- package/out/zql/src/ivm/fan-out.d.ts +1 -1
- package/out/zql/src/ivm/fan-out.d.ts.map +1 -1
- package/out/zql/src/ivm/fan-out.js +1 -1
- package/out/zql/src/ivm/fan-out.js.map +1 -1
- package/out/zql/src/ivm/filter-operators.d.ts +3 -3
- package/out/zql/src/ivm/filter-operators.d.ts.map +1 -1
- package/out/zql/src/ivm/filter-operators.js.map +1 -1
- package/out/zql/src/ivm/filter-push.d.ts.map +1 -1
- package/out/zql/src/ivm/filter-push.js +7 -7
- package/out/zql/src/ivm/filter-push.js.map +1 -1
- package/out/zql/src/ivm/filter.d.ts +1 -1
- package/out/zql/src/ivm/filter.d.ts.map +1 -1
- package/out/zql/src/ivm/filter.js.map +1 -1
- package/out/zql/src/ivm/flipped-join.d.ts.map +1 -1
- package/out/zql/src/ivm/flipped-join.js +49 -58
- package/out/zql/src/ivm/flipped-join.js.map +1 -1
- package/out/zql/src/ivm/join-utils.d.ts +2 -6
- package/out/zql/src/ivm/join-utils.d.ts.map +1 -1
- package/out/zql/src/ivm/join-utils.js +25 -25
- package/out/zql/src/ivm/join-utils.js.map +1 -1
- package/out/zql/src/ivm/join.d.ts.map +1 -1
- package/out/zql/src/ivm/join.js +32 -51
- package/out/zql/src/ivm/join.js.map +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.d.ts +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.d.ts.map +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js +5 -10
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +51 -59
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/push-accumulated.d.ts +3 -2
- package/out/zql/src/ivm/push-accumulated.d.ts.map +1 -1
- package/out/zql/src/ivm/push-accumulated.js +98 -122
- package/out/zql/src/ivm/push-accumulated.js.map +1 -1
- package/out/zql/src/ivm/skip.d.ts +1 -1
- package/out/zql/src/ivm/skip.d.ts.map +1 -1
- package/out/zql/src/ivm/skip.js +2 -2
- package/out/zql/src/ivm/skip.js.map +1 -1
- package/out/zql/src/ivm/source-change-index-enum.d.ts +7 -0
- package/out/zql/src/ivm/source-change-index-enum.d.ts.map +1 -0
- package/out/zql/src/ivm/source-change-index.d.ts +5 -0
- package/out/zql/src/ivm/source-change-index.d.ts.map +1 -0
- package/out/zql/src/ivm/source.d.ts +11 -13
- package/out/zql/src/ivm/source.d.ts.map +1 -1
- package/out/zql/src/ivm/source.js +26 -0
- package/out/zql/src/ivm/source.js.map +1 -0
- package/out/zql/src/ivm/take.d.ts.map +1 -1
- package/out/zql/src/ivm/take.js +27 -50
- package/out/zql/src/ivm/take.js.map +1 -1
- package/out/zql/src/ivm/union-fan-in.d.ts +2 -1
- package/out/zql/src/ivm/union-fan-in.d.ts.map +1 -1
- package/out/zql/src/ivm/union-fan-in.js +3 -3
- package/out/zql/src/ivm/union-fan-in.js.map +1 -1
- package/out/zql/src/ivm/union-fan-out.d.ts.map +1 -1
- package/out/zql/src/ivm/union-fan-out.js +1 -1
- package/out/zql/src/ivm/union-fan-out.js.map +1 -1
- package/out/zql/src/planner/planner-debug.d.ts +2 -2
- 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-graph.d.ts +1 -1
- package/out/zql/src/planner/planner-graph.d.ts.map +1 -1
- package/out/zql/src/planner/planner-graph.js.map +1 -1
- package/out/zqlite/src/internal/sql-inline.d.ts.map +1 -1
- package/out/zqlite/src/internal/sql-inline.js.map +1 -1
- package/out/zqlite/src/query-builder.d.ts.map +1 -1
- package/out/zqlite/src/query-builder.js.map +1 -1
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +11 -11
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +95 -93
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection-context-manager.js","names":["#lc","#connections","#group","#validateLegacyJWT","#now","#revalidateIntervalMs","#retransformIntervalMs","#queryConfig","#pushConfig","#removeConnection","#nextInsertionOrder","#refreshBackgroundConnectionContext","#updateBackgroundRetransformDeadline","#mustGetConnectionContext","#demoteConnection","#getConnectionContext","#nextRevalidateAt","#getBackgroundConnectionContext"],"sources":["../../../../../../zero-cache/src/services/view-syncer/connection-context-manager.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {InitConnectionBody} from '../../../../zero-protocol/src/connect.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {\n authEquals,\n resolveAuth,\n type Auth,\n type ValidateLegacyJWT,\n} from '../../auth/auth.ts';\nimport {compileUrlPattern} from '../../custom/fetch.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {ConnectParams} from '../../workers/connect-params.ts';\nimport type {UpdateAuthBody} from '../../../../zero-protocol/src/update-auth.ts';\nimport type {ZeroConfig} from '../../config/zero-config.ts';\n\nexport type ConnectionState = 'provisional' | 'validated';\n\n/**\n * Identifies one live websocket for a client slot.\n */\nexport type ConnectionSelector = {\n readonly clientID: string;\n readonly wsID: string;\n};\n\ntype FetchConfig = ZeroConfig['query'];\n\nexport type HeaderOptions = {\n apiKey?: string | undefined;\n customHeaders?: Record<string, string> | undefined;\n allowedClientHeaders?: readonly string[] | undefined;\n cookie?: string | undefined;\n origin?: string | undefined;\n};\n\nexport type ConnectionFetchContext = {\n url: string | undefined;\n allowedUrlPatterns: URLPattern[] | undefined;\n headerOptions: HeaderOptions;\n};\n\n/**\n * A snapshot of one live connection tracked by the manager.\n *\n * `revalidateAt` is only populated while the connection is `validated`.\n */\nexport type ConnectionContext = {\n state: ConnectionState;\n\n readonly clientID: string;\n readonly wsID: string;\n readonly userID: string | undefined;\n\n auth: Auth | undefined;\n\n readonly profileID: string | null;\n readonly baseCookie: string | null;\n readonly protocolVersion: number;\n\n revision: number;\n\n revalidateAt: number | undefined;\n\n readonly insertionOrder: number;\n\n readonly queryContext: ConnectionFetchContext;\n readonly pushContext: ConnectionFetchContext;\n};\n\n/**\n * Group-scoped auth state shared across the live connections.\n *\n * The background connection is the validated connection currently used for\n * shared background work. Retransform happens on a group level, and uses\n * the background connection's credential to refetch the latest queries.\n *\n * Since auth can be pinned to logged-out users, `userID` can be undefined\n * even after validation.\n */\nexport type GroupAuthState = {\n userID: string | undefined;\n validated: boolean;\n\n backgroundConnection: ConnectionSelector | undefined;\n retransformAt: number | undefined;\n // Defer all maintenance in case a transient failure occurs.\n maintenanceNotBeforeAt: number | undefined;\n};\n\nexport type ConnectionContextManager = {\n registerConnection(\n selector: ConnectionSelector,\n connectParams: ConnectParams,\n auth?: Auth,\n ): Readonly<ConnectionContext>;\n\n initConnection(\n selector: ConnectionSelector,\n body: InitConnectionBody,\n ): Readonly<ConnectionContext>;\n\n updateAuth(\n selector: ConnectionSelector,\n body: UpdateAuthBody,\n ): Promise<Readonly<ConnectionContext>>;\n\n validateConnection(\n selector: ConnectionSelector,\n revision: number,\n ):\n | Readonly<{\n connection: ConnectionContext;\n group: GroupAuthState;\n }>\n | undefined;\n\n failConnection(\n selector: ConnectionSelector,\n revision: number,\n ): Readonly<ConnectionContext> | undefined;\n closeConnection(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined;\n\n markBackgroundRetransformSuccess(\n selector: ConnectionSelector,\n revision: number,\n ): void;\n\n deferMaintenance(kind: 'revalidate' | 'retransform'): void;\n\n getConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined;\n mustGetConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext>;\n\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined;\n mustGetBackgroundConnectionContext(): Readonly<ConnectionContext>;\n\n getGroupState(): Readonly<GroupAuthState>;\n\n planMaintenance(): {\n dueRevalidations: Readonly<ConnectionContext>[];\n dueRetransform: boolean;\n earliestDeadlineAt: number | undefined;\n };\n};\n\n/**\n * State machine for the auth state of a single `ViewSyncerService`.\n *\n * Connections are registered as `provisional`, optionally backfilled with\n * `initConnection` metadata, and then promoted to `validated` once their\n * stored `userID` is confirmed as valid. The manager also tracks which\n * validated connection currently serves as the group's background connection.\n *\n * This is intentionally side-effect free.\n */\nexport class ConnectionContextManagerImpl implements ConnectionContextManager {\n readonly #lc: LogContext;\n\n // The live connection records, keyed by clientID\n readonly #connections = new Map<string, ConnectionContext>();\n readonly #group: GroupAuthState = {\n userID: undefined,\n backgroundConnection: undefined,\n retransformAt: undefined,\n maintenanceNotBeforeAt: undefined,\n validated: false,\n };\n\n readonly #validateLegacyJWT: ValidateLegacyJWT | undefined;\n\n readonly #now: () => number;\n readonly #revalidateIntervalMs: number | undefined;\n readonly #retransformIntervalMs: number | undefined;\n readonly #queryConfig: FetchConfig | undefined;\n readonly #pushConfig: FetchConfig | undefined;\n #nextInsertionOrder = 0;\n\n constructor(\n lc: LogContext,\n revalidateIntervalSeconds?: number,\n retransformIntervalSeconds?: number,\n queryConfig?: FetchConfig,\n pushConfig?: FetchConfig,\n validateLegacyJWT?: ValidateLegacyJWT,\n now?: () => number,\n ) {\n this.#lc = lc;\n this.#now = now ?? Date.now;\n this.#revalidateIntervalMs =\n revalidateIntervalSeconds === undefined\n ? undefined\n : revalidateIntervalSeconds * 1000;\n this.#retransformIntervalMs =\n retransformIntervalSeconds === undefined\n ? undefined\n : retransformIntervalSeconds * 1000;\n this.#queryConfig = queryConfig;\n this.#pushConfig = pushConfig;\n this.#validateLegacyJWT = validateLegacyJWT;\n }\n\n /**\n * Creates or replaces the live record for a websocket connection.\n *\n * Re-registering the same `clientID` drops the old socket record and starts\n * the replacement back in `provisional` state.\n */\n registerConnection(\n selector: ConnectionSelector,\n connectParams: ConnectParams,\n auth?: Auth,\n ): Readonly<ConnectionContext> {\n this.#removeConnection(selector);\n\n const sharedHeaders = {\n customHeaders: undefined,\n token: auth?.raw,\n origin: connectParams.origin,\n userID: connectParams.userID,\n };\n\n const connection: ConnectionContext = {\n state: 'provisional',\n\n clientID: connectParams.clientID,\n wsID: connectParams.wsID,\n revision: 0,\n userID: connectParams.userID,\n auth,\n\n profileID: connectParams.profileID,\n baseCookie: connectParams.baseCookie,\n protocolVersion: connectParams.protocolVersion,\n\n revalidateAt: undefined,\n\n queryContext: {\n url: this.#queryConfig?.url?.[0],\n allowedUrlPatterns: this.#queryConfig?.url?.map(compileUrlPattern),\n headerOptions: {\n ...sharedHeaders,\n apiKey: this.#queryConfig?.apiKey,\n allowedClientHeaders: this.#queryConfig?.allowedClientHeaders,\n cookie: this.#queryConfig?.forwardCookies\n ? connectParams.httpCookie\n : undefined,\n },\n },\n pushContext: {\n url: this.#pushConfig?.url?.[0],\n allowedUrlPatterns: this.#pushConfig?.url?.map(compileUrlPattern),\n headerOptions: {\n ...sharedHeaders,\n apiKey: this.#pushConfig?.apiKey,\n allowedClientHeaders: this.#pushConfig?.allowedClientHeaders,\n cookie: this.#pushConfig?.forwardCookies\n ? connectParams.httpCookie\n : undefined,\n },\n },\n\n insertionOrder: ++this.#nextInsertionOrder,\n };\n this.#connections.set(connection.clientID, connection);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return snapshotConnection(connection);\n }\n\n /**\n * Backfills `initConnection` data for sockets that were registered before the\n * client could send its full init payload.\n *\n * This updates metadata only; it does not validate the connection.\n */\n initConnection(\n selector: ConnectionSelector,\n body: InitConnectionBody,\n ): Readonly<ConnectionContext> {\n const connection = this.#mustGetConnectionContext(selector);\n\n if (body.userQueryURL) {\n connection.queryContext.url = body.userQueryURL;\n }\n if (body.userQueryHeaders) {\n connection.queryContext.headerOptions.customHeaders =\n body.userQueryHeaders;\n }\n if (body.userPushURL) {\n connection.pushContext.url = body.userPushURL;\n }\n if (body.userPushHeaders) {\n connection.pushContext.headerOptions.customHeaders = body.userPushHeaders;\n }\n\n connection.revision++;\n this.#demoteConnection(connection);\n\n return snapshotConnection(connection);\n }\n\n /**\n * A material auth change demotes the connection back to provisional until it\n * is validated again.\n */\n async updateAuth(\n selector: ConnectionSelector,\n body: UpdateAuthBody,\n ): Promise<Readonly<ConnectionContext>> {\n const connection = this.#mustGetConnectionContext(selector);\n\n const nextAuth = await resolveAuth(\n this.#lc,\n connection.auth,\n connection.userID,\n body.auth,\n this.#validateLegacyJWT,\n );\n\n const authChanged = !authEquals(connection.auth, nextAuth);\n connection.auth = nextAuth;\n if (authChanged) {\n connection.revision++;\n this.#demoteConnection(connection);\n }\n\n return snapshotConnection(connection);\n }\n\n /**\n * Validates one connection against the group's pinned `userID`.\n *\n * The first successful validation binds the group `userID`. Later\n * validations must match it. Validation also refreshes the connection's\n * revalidation deadline and may pick the connection as the group\n * background connection if none is currently available. If the websocket is\n * gone by the time async validation finishes, this becomes a no-op.\n */\n validateConnection(\n selector: ConnectionSelector,\n revision: number,\n ):\n | Readonly<{\n connection: ConnectionContext;\n group: GroupAuthState;\n }>\n | undefined {\n const connection = this.#getConnectionContext(selector);\n if (!connection) {\n return undefined;\n }\n\n if (connection.revision !== revision) {\n this.#lc.debug?.('Skipping validateConnection for stale revision', {\n clientID: selector.clientID,\n attemptedRevision: revision,\n currentRevision: connection.revision,\n });\n return undefined;\n }\n\n if (this.#group.validated && this.#group.userID !== connection.userID) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Unauthorized,\n message:\n 'Client groups are pinned to a single userID. Connection userID does not match existing client group userID.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n\n if (!this.#group.validated) {\n this.#group.validated = true;\n this.#group.userID = connection.userID;\n }\n\n connection.state = 'validated';\n connection.revalidateAt = this.#nextRevalidateAt();\n this.#refreshBackgroundConnectionContext(connection);\n this.#updateBackgroundRetransformDeadline(false);\n\n return {\n connection: snapshotConnection(connection),\n group: this.getGroupState(),\n };\n }\n\n /** Removes one connection due to failed auth and updates all derived background/deadline state. */\n failConnection(\n selector: ConnectionSelector,\n revision: number,\n ): ConnectionContext | undefined {\n return this.#removeConnection(selector, revision);\n }\n\n /** Removes one disconnected connection and updates all derived background/deadline state. */\n closeConnection(selector: ConnectionSelector): ConnectionContext | undefined {\n return this.#removeConnection(selector);\n }\n\n /**\n * Records a successful background retransform. This starts a fresh interval\n * from the manager clock when shared retransform is schedulable, or\n * clears the deadline if it is not.\n */\n markBackgroundRetransformSuccess(\n selector: ConnectionSelector,\n revision: number,\n ): void {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection) {\n return;\n }\n if (\n selector !== undefined &&\n (backgroundConnection.clientID !== selector.clientID ||\n backgroundConnection.wsID !== selector.wsID ||\n backgroundConnection.revision !== revision)\n ) {\n return;\n }\n this.#updateBackgroundRetransformDeadline(true);\n }\n\n deferMaintenance(kind: 'revalidate' | 'retransform'): void {\n const intervalMs =\n kind === 'revalidate'\n ? this.#revalidateIntervalMs\n : this.#retransformIntervalMs;\n if (intervalMs === undefined) {\n return;\n }\n this.#group.maintenanceNotBeforeAt = Math.max(\n this.#group.maintenanceNotBeforeAt ?? 0,\n this.#now() + intervalMs,\n );\n }\n\n /** Returns the current live record for a client slot, if any. */\n getConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined {\n return snapshotConnection(this.#getConnectionContext(selector));\n }\n\n /** Returns the live record for one websocket or throws if it is unavailable. */\n mustGetConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> {\n return snapshotConnection(this.#mustGetConnectionContext(selector));\n }\n\n /** Returns the current background connection, if one exists. */\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined {\n return snapshotConnection(this.#getBackgroundConnectionContext());\n }\n\n mustGetBackgroundConnectionContext(): Readonly<ConnectionContext> {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.InvalidConnectionRequest,\n message:\n 'No validated connection is available for shared query work.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n return backgroundConnection;\n }\n\n /** Returns the shared group auth state. */\n getGroupState(): Readonly<GroupAuthState> {\n return snapshotGroup(this.#group);\n }\n\n /**\n * Reports which maintenance work is currently due.\n *\n * The result is a pure snapshot: callers decide which actions to run and\n * when to wake up next. `earliestDeadlineAt` is the earliest outstanding\n * maintenance deadline, including overdue work, unless a transient failure\n * has deferred all scheduled maintenance until `maintenanceNotBeforeAt`.\n */\n planMaintenance(): {\n dueRevalidations: Readonly<ConnectionContext>[];\n dueRetransform: boolean;\n earliestDeadlineAt: number | undefined;\n } {\n const dueRevalidations: Readonly<ConnectionContext>[] = [];\n const now = this.#now();\n let earliestDeadlineAt = this.#group.retransformAt;\n\n for (const connection of this.#connections.values()) {\n if (\n connection.state !== 'validated' ||\n connection.revalidateAt === undefined\n ) {\n continue;\n }\n if (connection.revalidateAt <= now) {\n dueRevalidations.push(snapshotConnection(connection));\n }\n earliestDeadlineAt = minDefined(\n earliestDeadlineAt,\n connection.revalidateAt,\n );\n }\n\n const dueRetransform =\n this.#group.retransformAt !== undefined &&\n this.#group.retransformAt <= now;\n const maintenanceNotBeforeAt = this.#group.maintenanceNotBeforeAt;\n\n if (\n maintenanceNotBeforeAt !== undefined &&\n maintenanceNotBeforeAt > now &&\n earliestDeadlineAt !== undefined\n ) {\n return {\n dueRevalidations: [],\n dueRetransform: false,\n earliestDeadlineAt: Math.max(\n earliestDeadlineAt,\n maintenanceNotBeforeAt,\n ),\n };\n }\n\n return {\n dueRevalidations: dueRevalidations.sort(compareByInsertionOrder),\n dueRetransform,\n earliestDeadlineAt,\n };\n }\n\n #removeConnection(\n selector: ConnectionSelector,\n revision?: number,\n ): Readonly<ConnectionContext> | undefined {\n const connection = this.#getConnectionContext(selector);\n\n if (!connection) {\n return undefined;\n }\n\n // If the revision has changed, we should not remove the connection\n if (revision !== undefined && connection.revision !== revision) {\n this.#lc.debug?.('Ignoring failConnection for stale revision', {\n clientID: selector.clientID,\n wsID: selector.wsID,\n attemptedRevision: revision,\n currentRevision: connection.revision,\n });\n return undefined;\n }\n\n const snapshot = snapshotConnection(connection);\n\n this.#connections.delete(connection.clientID);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n\n return snapshot;\n }\n\n #demoteConnection(connection: ConnectionContext): void {\n connection.state = 'provisional';\n connection.revalidateAt = undefined;\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n }\n\n /**\n * Keeps the background connection sticky while it remains validated.\n *\n * When a newly validated `preferred` connection is provided, it is promoted\n * only if there is no current validated background connection. Otherwise the\n * existing background connection stays in place until it disappears or is\n * demoted, at which point the newest validated connection is selected.\n */\n #refreshBackgroundConnectionContext(preferred?: ConnectionContext): void {\n if (preferred?.state === 'validated') {\n const currentBackgroundConnection =\n this.#getBackgroundConnectionContext();\n if (\n currentBackgroundConnection?.clientID === preferred.clientID &&\n currentBackgroundConnection.wsID === preferred.wsID\n ) {\n return;\n }\n if (currentBackgroundConnection !== undefined) {\n return;\n }\n this.#group.backgroundConnection = {\n clientID: preferred.clientID,\n wsID: preferred.wsID,\n };\n this.#lc.debug?.('Selected background connection for shared auth work', {\n clientID: preferred.clientID,\n wsID: preferred.wsID,\n revision: preferred.revision,\n reason: 'preferred-validated',\n });\n return;\n }\n\n const currentBackgroundConnection = this.#getBackgroundConnectionContext();\n if (currentBackgroundConnection?.state === 'validated') {\n return;\n }\n\n const nextBackgroundConnection = [...this.#connections.values()]\n .filter(connection => connection.state === 'validated')\n .sort(comparePreferredValidatedConnection)\n .at(0);\n this.#group.backgroundConnection = nextBackgroundConnection\n ? {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n }\n : undefined;\n if (nextBackgroundConnection) {\n this.#lc.debug?.('Selected background connection for shared auth work', {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n revision: nextBackgroundConnection.revision,\n reason: 'fallback-validated',\n });\n }\n }\n\n #getBackgroundConnectionContext(): ConnectionContext | undefined {\n const backgroundConnection = this.#group.backgroundConnection;\n if (!backgroundConnection) {\n return undefined;\n }\n return this.#getConnectionContext(backgroundConnection);\n }\n\n #getConnectionContext(\n selector: ConnectionSelector,\n ): ConnectionContext | undefined {\n const connection = this.#connections.get(selector.clientID);\n if (!connection) {\n return undefined;\n }\n if (connection.wsID !== selector.wsID) {\n return undefined;\n }\n return connection;\n }\n\n #mustGetConnectionContext(selector: ConnectionSelector): ConnectionContext {\n const connection = this.#getConnectionContext(selector);\n\n if (!connection) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.InvalidConnectionRequest,\n message:\n 'Connection auth state was not available for this websocket.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n\n return connection;\n }\n\n /**\n * Keeps the group background retransform deadline coherent with current\n * schedulability.\n *\n * When `reset` is false, this seeds a deadline only when shared retransform\n * is now possible and no deadline exists yet, preserving any existing\n * cadence. When `reset` is true, it starts a fresh interval from `#now()` if\n * retransform is schedulable, or clears the deadline if it is not.\n */\n #updateBackgroundRetransformDeadline(reset: boolean) {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection || this.#retransformIntervalMs === undefined) {\n this.#group.retransformAt = undefined;\n return;\n }\n\n if (reset || this.#group.retransformAt === undefined) {\n this.#group.retransformAt = this.#now() + this.#retransformIntervalMs;\n }\n }\n\n #nextRevalidateAt() {\n return this.#revalidateIntervalMs === undefined\n ? undefined\n : this.#now() + this.#revalidateIntervalMs;\n }\n}\n\nfunction snapshotConnection<T extends ConnectionContext | undefined>(\n connection: T,\n): T extends undefined ? T | undefined : Readonly<T> {\n if (!connection) {\n return undefined as T extends undefined ? T | undefined : Readonly<T>;\n }\n return {\n ...connection,\n queryContext: {\n ...connection.queryContext,\n headerOptions: {\n ...connection.queryContext.headerOptions,\n customHeaders: connection.queryContext.headerOptions.customHeaders\n ? {...connection.queryContext.headerOptions.customHeaders}\n : undefined,\n },\n },\n pushContext: {\n ...connection.pushContext,\n headerOptions: {\n ...connection.pushContext.headerOptions,\n customHeaders: connection.pushContext.headerOptions.customHeaders\n ? {...connection.pushContext.headerOptions.customHeaders}\n : undefined,\n },\n },\n } as T extends undefined ? T | undefined : Readonly<T>;\n}\n\nfunction snapshotGroup(group: GroupAuthState): Readonly<GroupAuthState> {\n return {\n ...group,\n backgroundConnection: group.backgroundConnection\n ? {...group.backgroundConnection}\n : undefined,\n };\n}\n\nfunction compareByInsertionOrder(\n a: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n b: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n) {\n return a.insertionOrder - b.insertionOrder || a.wsID.localeCompare(b.wsID);\n}\n\nfunction comparePreferredValidatedConnection(\n a: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n b: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n) {\n return b.insertionOrder - a.insertionOrder || b.wsID.localeCompare(a.wsID);\n}\n\nfunction minDefined(a: number | undefined, b: number | undefined) {\n if (a === undefined) {\n return b;\n }\n if (b === undefined) {\n return a;\n }\n return Math.min(a, b);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"connection-context-manager.js","names":["#lc","#connections","#group","#validateLegacyJWT","#now","#revalidateIntervalMs","#retransformIntervalMs","#queryConfig","#pushConfig","#removeConnection","#nextInsertionOrder","#refreshBackgroundConnectionContext","#updateBackgroundRetransformDeadline","#mustGetConnectionContext","#demoteConnection","#getConnectionContext","#nextRevalidateAt","#getBackgroundConnectionContext"],"sources":["../../../../../../zero-cache/src/services/view-syncer/connection-context-manager.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {InitConnectionBody} from '../../../../zero-protocol/src/connect.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport type {UpdateAuthBody} from '../../../../zero-protocol/src/update-auth.ts';\nimport {\n authEquals,\n resolveAuth,\n type Auth,\n type ValidateLegacyJWT,\n} from '../../auth/auth.ts';\nimport type {ZeroConfig} from '../../config/zero-config.ts';\nimport {compileUrlPattern} from '../../custom/fetch.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {ConnectParams} from '../../workers/connect-params.ts';\n\nexport type ConnectionState = 'provisional' | 'validated';\n\n/**\n * Identifies one live websocket for a client slot.\n */\nexport type ConnectionSelector = {\n readonly clientID: string;\n readonly wsID: string;\n};\n\ntype FetchConfig = ZeroConfig['query'];\n\nexport type HeaderOptions = {\n apiKey?: string | undefined;\n customHeaders?: Record<string, string> | undefined;\n allowedClientHeaders?: readonly string[] | undefined;\n cookie?: string | undefined;\n origin?: string | undefined;\n};\n\nexport type ConnectionFetchContext = {\n url: string | undefined;\n allowedUrlPatterns: URLPattern[] | undefined;\n headerOptions: HeaderOptions;\n};\n\n/**\n * A snapshot of one live connection tracked by the manager.\n *\n * `revalidateAt` is only populated while the connection is `validated`.\n */\nexport type ConnectionContext = {\n state: ConnectionState;\n\n readonly clientID: string;\n readonly wsID: string;\n readonly userID: string | undefined;\n\n auth: Auth | undefined;\n\n readonly profileID: string | null;\n readonly baseCookie: string | null;\n readonly protocolVersion: number;\n\n revision: number;\n\n revalidateAt: number | undefined;\n\n readonly insertionOrder: number;\n\n readonly queryContext: ConnectionFetchContext;\n readonly pushContext: ConnectionFetchContext;\n};\n\n/**\n * Group-scoped auth state shared across the live connections.\n *\n * The background connection is the validated connection currently used for\n * shared background work. Retransform happens on a group level, and uses\n * the background connection's credential to refetch the latest queries.\n *\n * Since auth can be pinned to logged-out users, `userID` can be undefined\n * even after validation.\n */\nexport type GroupAuthState = {\n userID: string | undefined;\n validated: boolean;\n\n backgroundConnection: ConnectionSelector | undefined;\n retransformAt: number | undefined;\n // Defer all maintenance in case a transient failure occurs.\n maintenanceNotBeforeAt: number | undefined;\n};\n\nexport type ConnectionContextManager = {\n registerConnection(\n selector: ConnectionSelector,\n connectParams: ConnectParams,\n auth?: Auth,\n ): Readonly<ConnectionContext>;\n\n initConnection(\n selector: ConnectionSelector,\n body: InitConnectionBody,\n ): Readonly<ConnectionContext>;\n\n updateAuth(\n selector: ConnectionSelector,\n body: UpdateAuthBody,\n ): Promise<Readonly<ConnectionContext>>;\n\n validateConnection(\n selector: ConnectionSelector,\n revision: number,\n ):\n | Readonly<{\n connection: ConnectionContext;\n group: GroupAuthState;\n }>\n | undefined;\n\n failConnection(\n selector: ConnectionSelector,\n revision: number,\n ): Readonly<ConnectionContext> | undefined;\n closeConnection(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined;\n\n markBackgroundRetransformSuccess(\n selector: ConnectionSelector,\n revision: number,\n ): void;\n\n deferMaintenance(kind: 'revalidate' | 'retransform'): void;\n\n getConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined;\n mustGetConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext>;\n\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined;\n mustGetBackgroundConnectionContext(): Readonly<ConnectionContext>;\n\n getGroupState(): Readonly<GroupAuthState>;\n\n planMaintenance(): {\n dueRevalidations: Readonly<ConnectionContext>[];\n dueRetransform: boolean;\n earliestDeadlineAt: number | undefined;\n };\n};\n\n/**\n * State machine for the auth state of a single `ViewSyncerService`.\n *\n * Connections are registered as `provisional`, optionally backfilled with\n * `initConnection` metadata, and then promoted to `validated` once their\n * stored `userID` is confirmed as valid. The manager also tracks which\n * validated connection currently serves as the group's background connection.\n *\n * This is intentionally side-effect free.\n */\nexport class ConnectionContextManagerImpl implements ConnectionContextManager {\n readonly #lc: LogContext;\n\n // The live connection records, keyed by clientID\n readonly #connections = new Map<string, ConnectionContext>();\n readonly #group: GroupAuthState = {\n userID: undefined,\n backgroundConnection: undefined,\n retransformAt: undefined,\n maintenanceNotBeforeAt: undefined,\n validated: false,\n };\n\n readonly #validateLegacyJWT: ValidateLegacyJWT | undefined;\n\n readonly #now: () => number;\n readonly #revalidateIntervalMs: number | undefined;\n readonly #retransformIntervalMs: number | undefined;\n readonly #queryConfig: FetchConfig | undefined;\n readonly #pushConfig: FetchConfig | undefined;\n #nextInsertionOrder = 0;\n\n constructor(\n lc: LogContext,\n revalidateIntervalSeconds?: number,\n retransformIntervalSeconds?: number,\n queryConfig?: FetchConfig,\n pushConfig?: FetchConfig,\n validateLegacyJWT?: ValidateLegacyJWT,\n now?: () => number,\n ) {\n this.#lc = lc;\n this.#now = now ?? Date.now;\n this.#revalidateIntervalMs =\n revalidateIntervalSeconds === undefined\n ? undefined\n : revalidateIntervalSeconds * 1000;\n this.#retransformIntervalMs =\n retransformIntervalSeconds === undefined\n ? undefined\n : retransformIntervalSeconds * 1000;\n this.#queryConfig = queryConfig;\n this.#pushConfig = pushConfig;\n this.#validateLegacyJWT = validateLegacyJWT;\n }\n\n /**\n * Creates or replaces the live record for a websocket connection.\n *\n * Re-registering the same `clientID` drops the old socket record and starts\n * the replacement back in `provisional` state.\n */\n registerConnection(\n selector: ConnectionSelector,\n connectParams: ConnectParams,\n auth?: Auth,\n ): Readonly<ConnectionContext> {\n this.#removeConnection(selector);\n\n const sharedHeaders = {\n customHeaders: undefined,\n token: auth?.raw,\n origin: connectParams.origin,\n userID: connectParams.userID,\n };\n\n const connection: ConnectionContext = {\n state: 'provisional',\n\n clientID: connectParams.clientID,\n wsID: connectParams.wsID,\n revision: 0,\n userID: connectParams.userID,\n auth,\n\n profileID: connectParams.profileID,\n baseCookie: connectParams.baseCookie,\n protocolVersion: connectParams.protocolVersion,\n\n revalidateAt: undefined,\n\n queryContext: {\n url: this.#queryConfig?.url?.[0],\n allowedUrlPatterns: this.#queryConfig?.url?.map(compileUrlPattern),\n headerOptions: {\n ...sharedHeaders,\n apiKey: this.#queryConfig?.apiKey,\n allowedClientHeaders: this.#queryConfig?.allowedClientHeaders,\n cookie: this.#queryConfig?.forwardCookies\n ? connectParams.httpCookie\n : undefined,\n },\n },\n pushContext: {\n url: this.#pushConfig?.url?.[0],\n allowedUrlPatterns: this.#pushConfig?.url?.map(compileUrlPattern),\n headerOptions: {\n ...sharedHeaders,\n apiKey: this.#pushConfig?.apiKey,\n allowedClientHeaders: this.#pushConfig?.allowedClientHeaders,\n cookie: this.#pushConfig?.forwardCookies\n ? connectParams.httpCookie\n : undefined,\n },\n },\n\n insertionOrder: ++this.#nextInsertionOrder,\n };\n this.#connections.set(connection.clientID, connection);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return snapshotConnection(connection);\n }\n\n /**\n * Backfills `initConnection` data for sockets that were registered before the\n * client could send its full init payload.\n *\n * This updates metadata only; it does not validate the connection.\n */\n initConnection(\n selector: ConnectionSelector,\n body: InitConnectionBody,\n ): Readonly<ConnectionContext> {\n const connection = this.#mustGetConnectionContext(selector);\n\n if (body.userQueryURL) {\n connection.queryContext.url = body.userQueryURL;\n }\n if (body.userQueryHeaders) {\n connection.queryContext.headerOptions.customHeaders =\n body.userQueryHeaders;\n }\n if (body.userPushURL) {\n connection.pushContext.url = body.userPushURL;\n }\n if (body.userPushHeaders) {\n connection.pushContext.headerOptions.customHeaders = body.userPushHeaders;\n }\n\n connection.revision++;\n this.#demoteConnection(connection);\n\n return snapshotConnection(connection);\n }\n\n /**\n * A material auth change demotes the connection back to provisional until it\n * is validated again.\n */\n async updateAuth(\n selector: ConnectionSelector,\n body: UpdateAuthBody,\n ): Promise<Readonly<ConnectionContext>> {\n const connection = this.#mustGetConnectionContext(selector);\n\n const nextAuth = await resolveAuth(\n this.#lc,\n connection.auth,\n connection.userID,\n body.auth,\n this.#validateLegacyJWT,\n );\n\n const authChanged = !authEquals(connection.auth, nextAuth);\n connection.auth = nextAuth;\n if (authChanged) {\n connection.revision++;\n this.#demoteConnection(connection);\n }\n\n return snapshotConnection(connection);\n }\n\n /**\n * Validates one connection against the group's pinned `userID`.\n *\n * The first successful validation binds the group `userID`. Later\n * validations must match it. Validation also refreshes the connection's\n * revalidation deadline and may pick the connection as the group\n * background connection if none is currently available. If the websocket is\n * gone by the time async validation finishes, this becomes a no-op.\n */\n validateConnection(\n selector: ConnectionSelector,\n revision: number,\n ):\n | Readonly<{\n connection: ConnectionContext;\n group: GroupAuthState;\n }>\n | undefined {\n const connection = this.#getConnectionContext(selector);\n if (!connection) {\n return undefined;\n }\n\n if (connection.revision !== revision) {\n this.#lc.debug?.('Skipping validateConnection for stale revision', {\n clientID: selector.clientID,\n attemptedRevision: revision,\n currentRevision: connection.revision,\n });\n return undefined;\n }\n\n if (this.#group.validated && this.#group.userID !== connection.userID) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Unauthorized,\n message:\n 'Client groups are pinned to a single userID. Connection userID does not match existing client group userID.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n\n if (!this.#group.validated) {\n this.#group.validated = true;\n this.#group.userID = connection.userID;\n }\n\n connection.state = 'validated';\n connection.revalidateAt = this.#nextRevalidateAt();\n this.#refreshBackgroundConnectionContext(connection);\n this.#updateBackgroundRetransformDeadline(false);\n\n return {\n connection: snapshotConnection(connection),\n group: this.getGroupState(),\n };\n }\n\n /** Removes one connection due to failed auth and updates all derived background/deadline state. */\n failConnection(\n selector: ConnectionSelector,\n revision: number,\n ): ConnectionContext | undefined {\n return this.#removeConnection(selector, revision);\n }\n\n /** Removes one disconnected connection and updates all derived background/deadline state. */\n closeConnection(selector: ConnectionSelector): ConnectionContext | undefined {\n return this.#removeConnection(selector);\n }\n\n /**\n * Records a successful background retransform. This starts a fresh interval\n * from the manager clock when shared retransform is schedulable, or\n * clears the deadline if it is not.\n */\n markBackgroundRetransformSuccess(\n selector: ConnectionSelector,\n revision: number,\n ): void {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection) {\n return;\n }\n if (\n selector !== undefined &&\n (backgroundConnection.clientID !== selector.clientID ||\n backgroundConnection.wsID !== selector.wsID ||\n backgroundConnection.revision !== revision)\n ) {\n return;\n }\n this.#updateBackgroundRetransformDeadline(true);\n }\n\n deferMaintenance(kind: 'revalidate' | 'retransform'): void {\n const intervalMs =\n kind === 'revalidate'\n ? this.#revalidateIntervalMs\n : this.#retransformIntervalMs;\n if (intervalMs === undefined) {\n return;\n }\n this.#group.maintenanceNotBeforeAt = Math.max(\n this.#group.maintenanceNotBeforeAt ?? 0,\n this.#now() + intervalMs,\n );\n }\n\n /** Returns the current live record for a client slot, if any. */\n getConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined {\n return snapshotConnection(this.#getConnectionContext(selector));\n }\n\n /** Returns the live record for one websocket or throws if it is unavailable. */\n mustGetConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> {\n return snapshotConnection(this.#mustGetConnectionContext(selector));\n }\n\n /** Returns the current background connection, if one exists. */\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined {\n return snapshotConnection(this.#getBackgroundConnectionContext());\n }\n\n mustGetBackgroundConnectionContext(): Readonly<ConnectionContext> {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.InvalidConnectionRequest,\n message:\n 'No validated connection is available for shared query work.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n return backgroundConnection;\n }\n\n /** Returns the shared group auth state. */\n getGroupState(): Readonly<GroupAuthState> {\n return snapshotGroup(this.#group);\n }\n\n /**\n * Reports which maintenance work is currently due.\n *\n * The result is a pure snapshot: callers decide which actions to run and\n * when to wake up next. `earliestDeadlineAt` is the earliest outstanding\n * maintenance deadline, including overdue work, unless a transient failure\n * has deferred all scheduled maintenance until `maintenanceNotBeforeAt`.\n */\n planMaintenance(): {\n dueRevalidations: Readonly<ConnectionContext>[];\n dueRetransform: boolean;\n earliestDeadlineAt: number | undefined;\n } {\n const dueRevalidations: Readonly<ConnectionContext>[] = [];\n const now = this.#now();\n let earliestDeadlineAt = this.#group.retransformAt;\n\n for (const connection of this.#connections.values()) {\n if (\n connection.state !== 'validated' ||\n connection.revalidateAt === undefined\n ) {\n continue;\n }\n if (connection.revalidateAt <= now) {\n dueRevalidations.push(snapshotConnection(connection));\n }\n earliestDeadlineAt = minDefined(\n earliestDeadlineAt,\n connection.revalidateAt,\n );\n }\n\n const dueRetransform =\n this.#group.retransformAt !== undefined &&\n this.#group.retransformAt <= now;\n const maintenanceNotBeforeAt = this.#group.maintenanceNotBeforeAt;\n\n if (\n maintenanceNotBeforeAt !== undefined &&\n maintenanceNotBeforeAt > now &&\n earliestDeadlineAt !== undefined\n ) {\n return {\n dueRevalidations: [],\n dueRetransform: false,\n earliestDeadlineAt: Math.max(\n earliestDeadlineAt,\n maintenanceNotBeforeAt,\n ),\n };\n }\n\n return {\n dueRevalidations: dueRevalidations.sort(compareByInsertionOrder),\n dueRetransform,\n earliestDeadlineAt,\n };\n }\n\n #removeConnection(\n selector: ConnectionSelector,\n revision?: number,\n ): Readonly<ConnectionContext> | undefined {\n const connection = this.#getConnectionContext(selector);\n\n if (!connection) {\n return undefined;\n }\n\n // If the revision has changed, we should not remove the connection\n if (revision !== undefined && connection.revision !== revision) {\n this.#lc.debug?.('Ignoring failConnection for stale revision', {\n clientID: selector.clientID,\n wsID: selector.wsID,\n attemptedRevision: revision,\n currentRevision: connection.revision,\n });\n return undefined;\n }\n\n const snapshot = snapshotConnection(connection);\n\n this.#connections.delete(connection.clientID);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n\n return snapshot;\n }\n\n #demoteConnection(connection: ConnectionContext): void {\n connection.state = 'provisional';\n connection.revalidateAt = undefined;\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n }\n\n /**\n * Keeps the background connection sticky while it remains validated.\n *\n * When a newly validated `preferred` connection is provided, it is promoted\n * only if there is no current validated background connection. Otherwise the\n * existing background connection stays in place until it disappears or is\n * demoted, at which point the newest validated connection is selected.\n */\n #refreshBackgroundConnectionContext(preferred?: ConnectionContext): void {\n if (preferred?.state === 'validated') {\n const currentBackgroundConnection =\n this.#getBackgroundConnectionContext();\n if (\n currentBackgroundConnection?.clientID === preferred.clientID &&\n currentBackgroundConnection.wsID === preferred.wsID\n ) {\n return;\n }\n if (currentBackgroundConnection !== undefined) {\n return;\n }\n this.#group.backgroundConnection = {\n clientID: preferred.clientID,\n wsID: preferred.wsID,\n };\n this.#lc.debug?.('Selected background connection for shared auth work', {\n clientID: preferred.clientID,\n wsID: preferred.wsID,\n revision: preferred.revision,\n reason: 'preferred-validated',\n });\n return;\n }\n\n const currentBackgroundConnection = this.#getBackgroundConnectionContext();\n if (currentBackgroundConnection?.state === 'validated') {\n return;\n }\n\n const nextBackgroundConnection = [...this.#connections.values()]\n .filter(connection => connection.state === 'validated')\n .sort(comparePreferredValidatedConnection)\n .at(0);\n this.#group.backgroundConnection = nextBackgroundConnection\n ? {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n }\n : undefined;\n if (nextBackgroundConnection) {\n this.#lc.debug?.('Selected background connection for shared auth work', {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n revision: nextBackgroundConnection.revision,\n reason: 'fallback-validated',\n });\n }\n }\n\n #getBackgroundConnectionContext(): ConnectionContext | undefined {\n const backgroundConnection = this.#group.backgroundConnection;\n if (!backgroundConnection) {\n return undefined;\n }\n return this.#getConnectionContext(backgroundConnection);\n }\n\n #getConnectionContext(\n selector: ConnectionSelector,\n ): ConnectionContext | undefined {\n const connection = this.#connections.get(selector.clientID);\n if (!connection) {\n return undefined;\n }\n if (connection.wsID !== selector.wsID) {\n return undefined;\n }\n return connection;\n }\n\n #mustGetConnectionContext(selector: ConnectionSelector): ConnectionContext {\n const connection = this.#getConnectionContext(selector);\n\n if (!connection) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.InvalidConnectionRequest,\n message:\n 'Connection auth state was not available for this websocket.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n\n return connection;\n }\n\n /**\n * Keeps the group background retransform deadline coherent with current\n * schedulability.\n *\n * When `reset` is false, this seeds a deadline only when shared retransform\n * is now possible and no deadline exists yet, preserving any existing\n * cadence. When `reset` is true, it starts a fresh interval from `#now()` if\n * retransform is schedulable, or clears the deadline if it is not.\n */\n #updateBackgroundRetransformDeadline(reset: boolean) {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection || this.#retransformIntervalMs === undefined) {\n this.#group.retransformAt = undefined;\n return;\n }\n\n if (reset || this.#group.retransformAt === undefined) {\n this.#group.retransformAt = this.#now() + this.#retransformIntervalMs;\n }\n }\n\n #nextRevalidateAt() {\n return this.#revalidateIntervalMs === undefined\n ? undefined\n : this.#now() + this.#revalidateIntervalMs;\n }\n}\n\nfunction snapshotConnection<T extends ConnectionContext | undefined>(\n connection: T,\n): T extends undefined ? T | undefined : Readonly<T> {\n if (!connection) {\n return undefined as T extends undefined ? T | undefined : Readonly<T>;\n }\n return {\n ...connection,\n queryContext: {\n ...connection.queryContext,\n headerOptions: {\n ...connection.queryContext.headerOptions,\n customHeaders: connection.queryContext.headerOptions.customHeaders\n ? {...connection.queryContext.headerOptions.customHeaders}\n : undefined,\n },\n },\n pushContext: {\n ...connection.pushContext,\n headerOptions: {\n ...connection.pushContext.headerOptions,\n customHeaders: connection.pushContext.headerOptions.customHeaders\n ? {...connection.pushContext.headerOptions.customHeaders}\n : undefined,\n },\n },\n } as T extends undefined ? T | undefined : Readonly<T>;\n}\n\nfunction snapshotGroup(group: GroupAuthState): Readonly<GroupAuthState> {\n return {\n ...group,\n backgroundConnection: group.backgroundConnection\n ? {...group.backgroundConnection}\n : undefined,\n };\n}\n\nfunction compareByInsertionOrder(\n a: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n b: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n) {\n return a.insertionOrder - b.insertionOrder || a.wsID.localeCompare(b.wsID);\n}\n\nfunction comparePreferredValidatedConnection(\n a: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n b: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n) {\n return b.insertionOrder - a.insertionOrder || b.wsID.localeCompare(a.wsID);\n}\n\nfunction minDefined(a: number | undefined, b: number | undefined) {\n if (a === undefined) {\n return b;\n }\n if (b === undefined) {\n return a;\n }\n return Math.min(a, b);\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { promiseVoid } from "../../../../shared/src/resolved-promises.js";
|
|
2
|
-
import { RunningState } from "../running-state.js";
|
|
3
2
|
import { cvrSchema } from "../../types/shards.js";
|
|
4
3
|
import "../../types/pg.js";
|
|
4
|
+
import { RunningState } from "../running-state.js";
|
|
5
5
|
import { READ_COMMITTED } from "../../db/mode-enum.js";
|
|
6
6
|
import { runTx } from "../../db/run-transaction.js";
|
|
7
7
|
//#region ../zero-cache/src/services/view-syncer/cvr-purger.ts
|
|
@@ -363,7 +363,7 @@ var CVRStore = class {
|
|
|
363
363
|
const start = afterVersion ? versionString(afterVersion) : "";
|
|
364
364
|
const end = versionString(upToCVR.version);
|
|
365
365
|
lc.debug?.(`scanning config patches for clients from ${start}`);
|
|
366
|
-
const reader = new TransactionPool(lc, READONLY).run(this.#db);
|
|
366
|
+
const reader = new TransactionPool(lc, { mode: READONLY }).run(this.#db);
|
|
367
367
|
try {
|
|
368
368
|
await reader.processReadTask((tx) => checkVersion(tx, this.#schema, this.#id, current));
|
|
369
369
|
const [allDesires, queryRows] = await reader.processReadTask((tx) => Promise.all([tx`
|
|
@@ -724,7 +724,7 @@ var CVRStore = class {
|
|
|
724
724
|
async inspectQueries(lc, ttlClock, clientID) {
|
|
725
725
|
const db = this.#db;
|
|
726
726
|
const clientGroupID = this.#id;
|
|
727
|
-
const reader = new TransactionPool(lc, READONLY).run(db);
|
|
727
|
+
const reader = new TransactionPool(lc, { mode: READONLY }).run(db);
|
|
728
728
|
try {
|
|
729
729
|
return await reader.processReadTask((tx) => tx`
|
|
730
730
|
SELECT DISTINCT ON (d."clientID", d."queryHash")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cvr-store.js","names":["#schema","#taskID","#id","#failService","#db","#writes","#pendingRowRecordUpdates","#forceUpdates","#rowCache","#loadAttemptIntervalMs","#maxLoadAttempts","#pendingQueryUpdates","#pendingDesireUpdates","#pendingQueryPartialUpdates","#load","#cvr","#pendingInstanceWrite","#updateQueryFields","#checkVersionAndOwnership","#flush","#rowCount","#flushDesires","#flushQueries"],"sources":["../../../../../../zero-cache/src/services/view-syncer/cvr-store.ts"],"sourcesContent":["import {trace} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport type {MaybeRow, PendingQuery, Row} from 'postgres';\nimport {startAsyncSpan} from '../../../../otel/src/span.ts';\nimport {version} from '../../../../otel/src/version.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {CustomKeyMap} from '../../../../shared/src/custom-key-map.ts';\nimport {CustomKeySet} from '../../../../shared/src/custom-key-set.ts';\nimport {\n deepEqual,\n type ReadonlyJSONValue,\n} from '../../../../shared/src/json.ts';\nimport {sleep} from '../../../../shared/src/sleep.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {astSchema} from '../../../../zero-protocol/src/ast.ts';\nimport {clientSchemaSchema} from '../../../../zero-protocol/src/client-schema.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport type {InspectQueryRow} from '../../../../zero-protocol/src/inspect-down.ts';\nimport {clampTTL, DEFAULT_TTL_MS} from '../../../../zql/src/query/ttl.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {TransactionPool} from '../../db/transaction-pool.ts';\nimport {recordRowsSynced} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {PostgresDB, PostgresTransaction} from '../../types/pg.ts';\nimport {rowIDString} from '../../types/row-key.ts';\nimport {cvrSchema, type ShardID} from '../../types/shards.ts';\nimport type {Patch, PatchToVersion} from './client-handler.ts';\nimport type {CVR, CVRSnapshot} from './cvr.ts';\nimport {RowRecordCache} from './row-record-cache.ts';\nimport {\n type ClientsRow,\n type DesiresRow,\n type InstancesRow,\n type QueriesRow,\n type RowsRow,\n} from './schema/cvr.ts';\nimport {\n type ClientQueryRecord,\n type ClientRecord,\n cmpVersions,\n type CustomQueryRecord,\n type CVRVersion,\n EMPTY_CVR_VERSION,\n type InternalQueryRecord,\n type NullableCVRVersion,\n type QueryPatch,\n type QueryRecord,\n queryRecordToQueryRow,\n type RowID,\n type RowRecord,\n versionFromString,\n versionString,\n} from './schema/types.ts';\nimport {\n type TTLClock,\n ttlClockAsNumber,\n ttlClockFromNumber,\n} from './ttl-clock.ts';\n\nexport type CVRFlushStats = {\n instances: number;\n queries: number;\n desires: number;\n clients: number;\n rows: number;\n rowsDeferred: number;\n statements: number;\n};\n\nlet flushCounter = 0;\n\n/**\n * Convert TTL/timestamp values for both old (seconds-based) and new (ms-based) columns.\n * Old columns: inactivatedAt (TIMESTAMPTZ), ttl (INTERVAL) - need conversion ms->seconds\n * New columns: inactivatedAtMs (DOUBLE PRECISION), ttlMs (DOUBLE PRECISION) - store ms directly\n */\nfunction convertTTLValues(\n inactivatedAt: TTLClock | undefined,\n ttl: number,\n): {\n ttlInterval: number | null;\n ttlMs: number | null;\n inactivatedAtTimestamp: TTLClock | null;\n inactivatedAtMs: TTLClock | null;\n} {\n return {\n ttlInterval: ttl < 0 ? null : ttl / 1000, // INTERVAL needs seconds\n ttlMs: ttl < 0 ? null : ttl, // New column stores ms directly\n inactivatedAtTimestamp:\n inactivatedAt === undefined\n ? null\n : ttlClockFromNumber(ttlClockAsNumber(inactivatedAt) / 1000),\n inactivatedAtMs: inactivatedAt ?? null,\n };\n}\n\nconst tracer = trace.getTracer('cvr-store', version);\n\n/**\n * QueriesRow with queryArgs as a stringified JSON value.\n * Used for batched config writes where queryArgs are pre-stringified\n * to handle the postgres.js boolean array bug.\n */\ntype StringifiedQueriesRow = Omit<QueriesRow, 'queryArgs'> & {\n queryArgs: string | null;\n};\n\nfunction asQuery(row: QueriesRow): QueryRecord {\n const maybeVersion = (s: string | null) =>\n s === null ? undefined : versionFromString(s);\n\n if (row.clientAST === null) {\n // custom query\n assert(\n row.queryName !== null && row.queryArgs !== null,\n 'queryName and queryArgs must be set for custom queries',\n );\n return {\n type: 'custom',\n id: row.queryHash,\n name: row.queryName,\n args: row.queryArgs,\n patchVersion: maybeVersion(row.patchVersion),\n clientState: {},\n transformationHash: row.transformationHash ?? undefined,\n transformationVersion: maybeVersion(row.transformationVersion),\n } satisfies CustomQueryRecord;\n }\n\n const ast = astSchema.parse(row.clientAST);\n return row.internal\n ? ({\n type: 'internal',\n id: row.queryHash,\n ast,\n transformationHash: row.transformationHash ?? undefined,\n transformationVersion: maybeVersion(row.transformationVersion),\n } satisfies InternalQueryRecord)\n : ({\n type: 'client',\n id: row.queryHash,\n ast,\n patchVersion: maybeVersion(row.patchVersion),\n clientState: {},\n transformationHash: row.transformationHash ?? undefined,\n transformationVersion: maybeVersion(row.transformationVersion),\n } satisfies ClientQueryRecord);\n}\n\n// The time to wait between load attempts.\nconst LOAD_ATTEMPT_INTERVAL_MS = 500;\n// The maximum number of load() attempts if the rowsVersion is behind.\n// This currently results in a maximum catchup time of ~5 seconds, after\n// which we give up and consider the CVR invalid.\n//\n// TODO: Make this configurable with something like --max-catchup-wait-ms,\n// as it is technically application specific.\nconst MAX_LOAD_ATTEMPTS = 10;\n\nexport class CVRStore {\n readonly #schema: string;\n readonly #taskID: string;\n readonly #id: string;\n readonly #failService: (e: unknown) => void;\n readonly #db: PostgresDB;\n readonly #writes: Set<{\n stats: Partial<CVRFlushStats>;\n write: (\n tx: PostgresTransaction,\n lastConnectTime: number,\n ) => PendingQuery<MaybeRow[]>;\n }> = new Set();\n // Stored separately so repeated putInstance() calls (e.g. setClientSchema,\n // setProfileID, and the final call in #flush) replace each other rather than\n // accumulating as independent statements in #writes.\n #pendingInstanceWrite:\n | ((\n tx: PostgresTransaction,\n lastConnectTime: number,\n ) => PendingQuery<MaybeRow[]>)\n | undefined = undefined;\n readonly #pendingRowRecordUpdates = new CustomKeyMap<RowID, RowRecord | null>(\n rowIDString,\n );\n readonly #forceUpdates = new CustomKeySet<RowID>(rowIDString);\n readonly #rowCache: RowRecordCache;\n readonly #loadAttemptIntervalMs: number;\n readonly #maxLoadAttempts: number;\n #rowCount: number = 0;\n readonly #pendingQueryUpdates = new Map<string, StringifiedQueriesRow>();\n readonly #pendingDesireUpdates = new Map<string, DesiresRow>();\n readonly #pendingQueryPartialUpdates = new Map<string, Partial<QueriesRow>>();\n\n constructor(\n lc: LogContext,\n cvrDb: PostgresDB,\n shard: ShardID,\n taskID: string,\n cvrID: string,\n failService: (e: unknown) => void,\n loadAttemptIntervalMs = LOAD_ATTEMPT_INTERVAL_MS,\n maxLoadAttempts = MAX_LOAD_ATTEMPTS,\n deferredRowFlushThreshold = 100, // somewhat arbitrary\n setTimeoutFn = setTimeout,\n ) {\n this.#failService = failService;\n this.#db = cvrDb;\n this.#schema = cvrSchema(shard);\n this.#taskID = taskID;\n this.#id = cvrID;\n this.#rowCache = new RowRecordCache(\n lc,\n cvrDb,\n shard,\n cvrID,\n failService,\n deferredRowFlushThreshold,\n setTimeoutFn,\n );\n this.#loadAttemptIntervalMs = loadAttemptIntervalMs;\n this.#maxLoadAttempts = maxLoadAttempts;\n }\n\n #cvr(table: string) {\n return this.#db(`${this.#schema}.${table}`);\n }\n\n #updateQueryFields(queryHash: string, fields: Partial<QueriesRow>): void {\n // Track as partial-only update for batched flush\n this.#pendingQueryPartialUpdates.set(queryHash, fields);\n }\n\n load(lc: LogContext, lastConnectTime: number): Promise<CVR> {\n return startAsyncSpan(tracer, 'cvr.load', async () => {\n let err: RowsVersionBehindError | undefined;\n for (let i = 0; i < this.#maxLoadAttempts; i++) {\n if (i > 0) {\n await sleep(this.#loadAttemptIntervalMs);\n }\n const result = await this.#load(lc, lastConnectTime);\n if (result instanceof RowsVersionBehindError) {\n lc.info?.(`attempt ${i + 1}: ${String(result)}`);\n err = result;\n continue;\n }\n return result;\n }\n assert(err, 'Expected error to be set after retry loop exhausted');\n throw new ClientNotFoundError(\n `max attempts exceeded waiting for CVR@${err.cvrVersion} to catch up from ${err.rowsVersion}`,\n );\n });\n }\n\n async #load(\n lc: LogContext,\n lastConnectTime: number,\n ): Promise<CVR | RowsVersionBehindError> {\n const start = Date.now();\n\n const id = this.#id;\n const cvr: CVR = {\n id,\n version: EMPTY_CVR_VERSION,\n lastActive: 0,\n ttlClock: ttlClockFromNumber(0), // TTL clock starts at 0, not Date.now()\n replicaVersion: null,\n clients: {},\n queries: {},\n clientSchema: null,\n profileID: null,\n };\n\n const [instance, clientsRows, queryRows, desiresRows] = await runTx(\n this.#db,\n tx => {\n lc.debug?.(`CVR tx started after ${Date.now() - start} ms`);\n return [\n tx<\n (Omit<InstancesRow, 'clientGroupID'> & {\n profileID: string | null;\n deleted: boolean;\n rowsVersion: string | null;\n })[]\n >`SELECT cvr.\"version\",\n \"lastActive\",\n \"ttlClock\",\n \"replicaVersion\",\n \"owner\",\n \"grantedAt\",\n \"clientSchema\",\n \"profileID\",\n \"deleted\",\n rows.\"version\" as \"rowsVersion\"\n FROM ${this.#cvr('instances')} AS cvr\n LEFT JOIN ${this.#cvr('rowsVersion')} AS rows\n ON cvr.\"clientGroupID\" = rows.\"clientGroupID\"\n WHERE cvr.\"clientGroupID\" = ${id}`,\n tx<Pick<ClientsRow, 'clientID'>[]>`SELECT \"clientID\" FROM ${this.#cvr(\n 'clients',\n )}\n WHERE \"clientGroupID\" = ${id}`,\n tx<QueriesRow[]>`SELECT * FROM ${this.#cvr('queries')}\n WHERE \"clientGroupID\" = ${id} AND deleted IS DISTINCT FROM true`,\n tx<DesiresRow[]>`SELECT\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttlMs\" AS \"ttl\",\n \"inactivatedAtMs\" AS \"inactivatedAt\"\n FROM ${this.#cvr('desires')}\n WHERE \"clientGroupID\" = ${id}`,\n ];\n },\n {mode: Mode.READONLY},\n );\n lc.debug?.(\n `CVR tx completed after ${Date.now() - start} ms ` +\n `(${clientsRows.length} clients, ${queryRows.length} queries, ${desiresRows.length} desires)`,\n );\n\n if (instance.length === 0) {\n // This is the first time we see this CVR.\n this.putInstance({\n version: cvr.version,\n lastActive: 0,\n ttlClock: ttlClockFromNumber(0), // TTL clock starts at 0 for new instances\n replicaVersion: null,\n clientSchema: null,\n profileID: null,\n });\n } else {\n assert(\n instance.length === 1,\n () => `Expected exactly one CVR instance, got ${instance.length}`,\n );\n const {\n version,\n lastActive,\n ttlClock,\n replicaVersion,\n owner,\n grantedAt,\n rowsVersion,\n clientSchema,\n profileID,\n deleted,\n } = instance[0];\n\n if (deleted) {\n throw new ClientNotFoundError(\n 'Client has been purged due to inactivity',\n );\n }\n\n if (owner !== this.#taskID) {\n if ((grantedAt ?? 0) > lastConnectTime) {\n throw new OwnershipError(owner, grantedAt, lastConnectTime);\n } else {\n // Fire-and-forget an ownership change to signal the current owner.\n // Note that the query is structured such that it only succeeds in the\n // correct conditions (i.e. gated on `grantedAt`).\n void this.#db`\n UPDATE ${this.#cvr('instances')} \n SET \"owner\" = ${this.#taskID}, \n \"grantedAt\" = ${lastConnectTime}\n WHERE \"clientGroupID\" = ${this.#id} AND\n (\"grantedAt\" IS NULL OR\n \"grantedAt\" <= to_timestamp(${lastConnectTime / 1000}))\n `\n .execute()\n .catch(this.#failService);\n }\n }\n\n if (version !== (rowsVersion ?? EMPTY_CVR_VERSION.stateVersion)) {\n // This will cause the load() method to wait for row catchup and retry.\n // Assuming the ownership signal succeeds, the current owner will stop\n // modifying the CVR and flush its pending row changes.\n return new RowsVersionBehindError(version, rowsVersion);\n }\n\n cvr.version = versionFromString(version);\n cvr.lastActive = lastActive;\n cvr.ttlClock = ttlClock;\n cvr.replicaVersion = replicaVersion;\n cvr.profileID = profileID;\n\n try {\n cvr.clientSchema =\n clientSchema === null\n ? null\n : v.parse(clientSchema, clientSchemaSchema);\n } catch (e) {\n throw new InvalidClientSchemaError(e);\n }\n }\n\n for (const row of clientsRows) {\n cvr.clients[row.clientID] = {\n id: row.clientID,\n desiredQueryIDs: [],\n };\n }\n\n for (const row of queryRows) {\n const query = asQuery(row);\n cvr.queries[row.queryHash] = query;\n }\n\n for (const row of desiresRows) {\n const client = cvr.clients[row.clientID];\n // Note: row.inactivatedAt is mapped from inactivatedAtMs in the SQL query\n if (!row.deleted && row.inactivatedAt === null) {\n if (client) {\n client.desiredQueryIDs.push(row.queryHash);\n } else {\n // This can happen if the client was deleted but the queries are still alive.\n lc.debug?.(\n `Not adding to desiredQueryIDs for client ${row.clientID} because it has been deleted.`,\n );\n }\n }\n\n const query = cvr.queries[row.queryHash];\n if (\n query &&\n query.type !== 'internal' &&\n (!row.deleted || row.inactivatedAt !== null)\n ) {\n query.clientState[row.clientID] = {\n inactivatedAt: row.inactivatedAt ?? undefined,\n ttl: clampTTL(row.ttl ?? DEFAULT_TTL_MS),\n version: versionFromString(row.patchVersion),\n };\n }\n }\n\n lc.info?.(\n `loaded cvr@${versionString(cvr.version)} (${Date.now() - start} ms)`,\n );\n\n // why do we not sort `desiredQueryIDs` here?\n\n return cvr;\n }\n\n getRowRecords(): Promise<ReadonlyMap<RowID, RowRecord>> {\n return this.#rowCache.getRowRecords();\n }\n\n putRowRecord(row: RowRecord): void {\n this.#pendingRowRecordUpdates.set(row.id, row);\n }\n\n /**\n * Note: Removing a row from the CVR should be represented by a\n * {@link putRowRecord()} with `refCounts: null` in order to properly\n * produce the appropriate delete patch when catching up old clients.\n *\n * This `delRowRecord()` method, on the other hand, is only used for\n * \"canceling\" the put of a row that was not in the CVR in the first place.\n */\n delRowRecord(id: RowID): void {\n this.#pendingRowRecordUpdates.set(id, null);\n }\n\n /**\n * Overrides the default logic that removes no-op writes and forces\n * the updates for the given row `ids`. This has no effect if there\n * are no corresponding puts or dels for the associated row records.\n */\n forceUpdates(...ids: RowID[]) {\n for (const id of ids) {\n this.#forceUpdates.add(id);\n }\n }\n\n /**\n * Updates the `ttlClock` of the CVR instance. The ttlClock starts at 0 when\n * the CVR instance is first created and increments based on elapsed time\n * since the base time established by the ViewSyncerService.\n */\n async updateTTLClock(ttlClock: TTLClock, lastActive: number): Promise<void> {\n await this.#db`UPDATE ${this.#cvr('instances')}\n SET \"lastActive\" = ${lastActive},\n \"ttlClock\" = ${ttlClock}\n WHERE \"clientGroupID\" = ${this.#id}`.execute();\n }\n\n /**\n * @returns This returns the current `ttlClock` of the CVR instance. The ttlClock\n * represents elapsed time since the instance was created (starting from 0).\n * If the CVR has never been initialized for this client group, it returns\n * `undefined`.\n */\n async getTTLClock(): Promise<TTLClock | undefined> {\n const result = await this.#db<Pick<InstancesRow, 'ttlClock'>[]>`\n SELECT \"ttlClock\" FROM ${this.#cvr('instances')}\n WHERE \"clientGroupID\" = ${this.#id}`.values();\n if (result.length === 0) {\n // This can happen if the CVR has not been initialized yet.\n return undefined;\n }\n assert(\n result.length === 1,\n () => `Expected exactly one rowsVersion result, got ${result.length}`,\n );\n return result[0][0];\n }\n\n putInstance({\n version,\n replicaVersion,\n lastActive,\n clientSchema,\n profileID,\n ttlClock,\n }: Pick<\n CVRSnapshot,\n | 'version'\n | 'replicaVersion'\n | 'lastActive'\n | 'clientSchema'\n | 'profileID'\n | 'ttlClock'\n >): void {\n // Overwrite any previously queued instance write — only the last call\n // matters since they all target the same row.\n this.#pendingInstanceWrite = (tx, lastConnectTime) => {\n const change: InstancesRow = {\n clientGroupID: this.#id,\n version: versionString(version),\n lastActive,\n ttlClock,\n replicaVersion,\n owner: this.#taskID,\n grantedAt: lastConnectTime,\n clientSchema,\n profileID,\n };\n return tx`\n INSERT INTO ${this.#cvr('instances')} ${tx(change)} \n ON CONFLICT (\"clientGroupID\") DO UPDATE SET ${tx(change)}`;\n };\n }\n\n markQueryAsDeleted(version: CVRVersion, queryPatch: QueryPatch): void {\n this.#updateQueryFields(queryPatch.id, {\n patchVersion: versionString(version),\n deleted: true,\n transformationHash: null,\n transformationVersion: null,\n });\n }\n\n putQuery(query: QueryRecord): void {\n const change = queryRecordToQueryRow(this.#id, query);\n\n const c = {\n ...change,\n // Pre-stringify queryArgs to handle postgres.js boolean array bug\n queryArgs:\n change.queryArgs !== null ? JSON.stringify(change.queryArgs) : null,\n transformationHash: change.transformationHash ?? null,\n transformationVersion: change.transformationVersion ?? null,\n deleted: change.deleted ?? false,\n };\n this.#pendingQueryUpdates.set(query.id, c);\n }\n\n updateQuery(query: QueryRecord) {\n const maybeVersionString = (v: CVRVersion | undefined) =>\n v ? versionString(v) : null;\n this.#updateQueryFields(query.id, {\n patchVersion:\n query.type === 'internal'\n ? null\n : maybeVersionString(query.patchVersion),\n transformationHash: query.transformationHash ?? null,\n transformationVersion: maybeVersionString(query.transformationVersion),\n deleted: false,\n });\n }\n\n insertClient(client: ClientRecord): void {\n const change: ClientsRow = {\n clientGroupID: this.#id,\n clientID: client.id,\n };\n\n this.#writes.add({\n stats: {clients: 1},\n write: tx => tx`INSERT INTO ${this.#cvr('clients')} ${tx(change)}`,\n });\n }\n\n deleteClient(clientID: string) {\n this.#writes.add({\n stats: {clients: 1},\n write: sql =>\n sql`DELETE FROM ${this.#cvr('clients')}\n WHERE \"clientGroupID\" = ${this.#id}\n AND \"clientID\" = ${clientID}`,\n });\n }\n\n putDesiredQuery(\n newVersion: CVRVersion,\n query: {id: string},\n client: {id: string},\n deleted: boolean,\n inactivatedAt: TTLClock | undefined,\n ttl: number,\n ): void {\n const {ttlMs, inactivatedAtMs} = convertTTLValues(inactivatedAt, ttl);\n\n const change: DesiresRow = {\n clientGroupID: this.#id,\n clientID: client.id,\n deleted,\n inactivatedAt: inactivatedAtMs,\n patchVersion: versionString(newVersion),\n queryHash: query.id,\n ttl: ttlMs,\n };\n\n // Use composite key to deduplicate/replace entries for the same client-query pair\n const key = `${client.id}:${query.id}`;\n this.#pendingDesireUpdates.set(key, change);\n }\n\n catchupRowPatches(\n lc: LogContext,\n afterVersion: NullableCVRVersion,\n upToCVR: CVRSnapshot,\n current: CVRVersion,\n excludeQueryHashes: string[] = [],\n ): AsyncGenerator<RowsRow[], void, undefined> {\n return this.#rowCache.catchupRowPatches(\n lc,\n afterVersion,\n upToCVR,\n current,\n excludeQueryHashes,\n );\n }\n\n async catchupConfigPatches(\n lc: LogContext,\n afterVersion: NullableCVRVersion,\n upToCVR: CVRSnapshot,\n current: CVRVersion,\n ): Promise<PatchToVersion[]> {\n if (cmpVersions(afterVersion, upToCVR.version) >= 0) {\n return [];\n }\n\n const startMs = Date.now();\n const start = afterVersion ? versionString(afterVersion) : '';\n const end = versionString(upToCVR.version);\n lc.debug?.(`scanning config patches for clients from ${start}`);\n\n const reader = new TransactionPool(lc, Mode.READONLY).run(this.#db);\n try {\n // Verify that we are reading the right version of the CVR.\n await reader.processReadTask(tx =>\n checkVersion(tx, this.#schema, this.#id, current),\n );\n\n const [allDesires, queryRows] = await reader.processReadTask(tx =>\n Promise.all([\n tx<DesiresRow[]>`\n SELECT * FROM ${this.#cvr('desires')}\n WHERE \"clientGroupID\" = ${this.#id}\n AND \"patchVersion\" > ${start}\n AND \"patchVersion\" <= ${end}`,\n tx<Pick<QueriesRow, 'deleted' | 'queryHash' | 'patchVersion'>[]>`\n SELECT deleted, \"queryHash\", \"patchVersion\" FROM ${this.#cvr('queries')}\n WHERE \"clientGroupID\" = ${this.#id}\n AND \"patchVersion\" > ${start}\n AND \"patchVersion\" <= ${end}`,\n ]),\n );\n\n const patches: PatchToVersion[] = [];\n for (const row of queryRows) {\n const {queryHash: id} = row;\n const patch: Patch = row.deleted\n ? {type: 'query', op: 'del', id}\n : {type: 'query', op: 'put', id};\n const v = row.patchVersion;\n assert(v, 'patchVersion must be set for query patches');\n patches.push({patch, toVersion: versionFromString(v)});\n }\n for (const row of allDesires) {\n const {clientID, queryHash: id} = row;\n const patch: Patch = row.deleted\n ? {type: 'query', op: 'del', id, clientID}\n : {type: 'query', op: 'put', id, clientID};\n patches.push({patch, toVersion: versionFromString(row.patchVersion)});\n }\n\n lc.debug?.(\n `${patches.length} config patches (${Date.now() - startMs} ms)`,\n );\n return patches;\n } finally {\n reader.setDone();\n }\n }\n\n #flushQueries(\n tx: PostgresTransaction,\n lc: LogContext,\n ): PendingQuery<Row[]>[] {\n // Merge partial updates into full updates\n const partialOnly = new Map<string, Partial<QueriesRow>>();\n for (const [queryHash, partial] of this.#pendingQueryPartialUpdates) {\n const existing = this.#pendingQueryUpdates.get(queryHash);\n if (existing) {\n // Merge partial into full update\n Object.assign(existing, partial);\n } else {\n // Track partial-only updates to batch separately\n partialOnly.set(queryHash, partial);\n }\n }\n\n const queries: PendingQuery<Row[]>[] = [];\n\n // Batch full updates\n if (this.#pendingQueryUpdates.size > 0) {\n const rows = [...this.#pendingQueryUpdates.values()];\n lc.debug?.(`Batch flushing ${rows.length} full query updates`);\n\n queries.push(tx`\n INSERT INTO ${this.#cvr('queries')} (\n \"clientGroupID\",\n \"queryHash\",\n \"clientAST\",\n \"queryName\",\n \"queryArgs\",\n \"patchVersion\",\n \"transformationHash\",\n \"transformationVersion\",\n \"internal\",\n \"deleted\"\n )\n SELECT\n \"clientGroupID\",\n \"queryHash\",\n \"clientAST\",\n \"queryName\",\n CASE\n WHEN \"queryArgs\" IS NULL THEN NULL\n ELSE \"queryArgs\"::json\n END,\n \"patchVersion\",\n \"transformationHash\",\n \"transformationVersion\",\n \"internal\",\n \"deleted\"\n FROM json_to_recordset(${rows}) AS x(\n \"clientGroupID\" TEXT,\n \"queryHash\" TEXT,\n \"clientAST\" JSONB,\n \"queryName\" TEXT,\n \"queryArgs\" TEXT,\n \"patchVersion\" TEXT,\n \"transformationHash\" TEXT,\n \"transformationVersion\" TEXT,\n \"internal\" BOOLEAN,\n \"deleted\" BOOLEAN\n )\n ON CONFLICT (\"clientGroupID\", \"queryHash\") DO UPDATE SET\n \"clientAST\" = excluded.\"clientAST\",\n \"queryName\" = excluded.\"queryName\",\n \"queryArgs\" = CASE \n WHEN excluded.\"queryArgs\" IS NULL THEN NULL\n ELSE excluded.\"queryArgs\"::json\n END,\n \"patchVersion\" = excluded.\"patchVersion\",\n \"transformationHash\" = excluded.\"transformationHash\",\n \"transformationVersion\" = excluded.\"transformationVersion\",\n \"internal\" = excluded.\"internal\",\n \"deleted\" = excluded.\"deleted\"\n `);\n }\n\n // Batch partial-only updates\n if (partialOnly.size > 0) {\n lc.debug?.(`Batch flushing ${partialOnly.size} partial query updates`);\n const rows = Array.from(\n partialOnly.entries(),\n ([queryHash, partial]) => ({\n clientGroupID: this.#id,\n queryHash,\n patchVersionSet: partial.patchVersion !== undefined,\n patchVersion: partial.patchVersion ?? null,\n deletedSet: partial.deleted !== undefined,\n deleted: partial.deleted ?? null,\n transformationHashSet: partial.transformationHash !== undefined,\n transformationHash: partial.transformationHash ?? null,\n transformationVersionSet: partial.transformationVersion !== undefined,\n transformationVersion: partial.transformationVersion ?? null,\n }),\n );\n queries.push(tx`\n UPDATE ${this.#cvr('queries')} AS q\n SET\n \"patchVersion\" = CASE\n WHEN u.\"patchVersionSet\" THEN u.\"patchVersion\"\n ELSE q.\"patchVersion\"\n END,\n \"deleted\" = CASE\n WHEN u.\"deletedSet\" THEN u.\"deleted\"\n ELSE q.\"deleted\"\n END,\n \"transformationHash\" = CASE\n WHEN u.\"transformationHashSet\" THEN u.\"transformationHash\"\n ELSE q.\"transformationHash\"\n END,\n \"transformationVersion\" = CASE\n WHEN u.\"transformationVersionSet\" THEN u.\"transformationVersion\"\n ELSE q.\"transformationVersion\"\n END\n FROM json_to_recordset(${rows}) AS u(\n \"clientGroupID\" TEXT,\n \"queryHash\" TEXT,\n \"patchVersionSet\" BOOLEAN,\n \"patchVersion\" TEXT,\n \"deletedSet\" BOOLEAN,\n \"deleted\" BOOLEAN,\n \"transformationHashSet\" BOOLEAN,\n \"transformationHash\" TEXT,\n \"transformationVersionSet\" BOOLEAN,\n \"transformationVersion\" TEXT\n )\n WHERE q.\"clientGroupID\" = u.\"clientGroupID\"\n AND q.\"queryHash\" = u.\"queryHash\"\n `);\n }\n\n return queries;\n }\n\n #flushDesires(\n tx: PostgresTransaction,\n lc: LogContext,\n ): PendingQuery<Row[]> | null {\n if (this.#pendingDesireUpdates.size === 0) {\n return null;\n }\n\n const rows = Array.from(this.#pendingDesireUpdates.values(), row => {\n const {ttlInterval, ttlMs, inactivatedAtTimestamp, inactivatedAtMs} =\n convertTTLValues(row.inactivatedAt ?? undefined, row.ttl ?? -1);\n return {\n clientGroupID: row.clientGroupID,\n clientID: row.clientID,\n queryHash: row.queryHash,\n patchVersion: row.patchVersion,\n deleted: row.deleted,\n ttl: ttlInterval,\n ttlMs,\n inactivatedAt: inactivatedAtTimestamp,\n inactivatedAtMs,\n };\n });\n\n lc.debug?.(`Batch flushing ${rows.length} desire updates`);\n\n return tx`\n INSERT INTO ${this.#cvr('desires')} (\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttl\",\n \"ttlMs\",\n \"inactivatedAt\",\n \"inactivatedAtMs\"\n )\n SELECT\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttl\",\n \"ttlMs\",\n CASE\n WHEN \"inactivatedAt\" IS NULL THEN NULL\n -- Divide by 1000 because postgres.js serializeTimestamp treats numbers as ms\n -- and to_timestamp expects seconds. This matches non-batched behavior.\n ELSE to_timestamp(\"inactivatedAt\" / 1000.0)\n END,\n \"inactivatedAtMs\"\n FROM json_to_recordset(${rows}) AS x(\n \"clientGroupID\" TEXT,\n \"clientID\" TEXT,\n \"queryHash\" TEXT,\n \"patchVersion\" TEXT,\n \"deleted\" BOOLEAN,\n \"ttl\" INTERVAL,\n \"ttlMs\" DOUBLE PRECISION,\n \"inactivatedAt\" DOUBLE PRECISION,\n \"inactivatedAtMs\" DOUBLE PRECISION\n )\n ON CONFLICT (\"clientGroupID\", \"clientID\", \"queryHash\") DO UPDATE SET\n \"patchVersion\" = excluded.\"patchVersion\",\n \"deleted\" = excluded.\"deleted\",\n \"ttl\" = excluded.\"ttl\",\n \"ttlMs\" = excluded.\"ttlMs\",\n \"inactivatedAt\" = excluded.\"inactivatedAt\",\n \"inactivatedAtMs\" = excluded.\"inactivatedAtMs\"\n `;\n }\n\n async #checkVersionAndOwnership(\n lc: LogContext,\n tx: PostgresTransaction,\n expectedCurrentVersion: CVRVersion,\n lastConnectTime: number,\n ): Promise<void> {\n const start = Date.now();\n lc.debug?.('checking cvr version and ownership');\n const result = await tx<\n Pick<InstancesRow, 'version' | 'owner' | 'grantedAt'>[]\n >`SELECT \"version\", \"owner\", \"grantedAt\" FROM ${this.#cvr('instances')}\n WHERE \"clientGroupID\" = ${this.#id}\n FOR UPDATE`;\n const expected = versionString(expectedCurrentVersion);\n const {version, owner, grantedAt} =\n result.length > 0\n ? result[0]\n : {\n version: EMPTY_CVR_VERSION.stateVersion,\n owner: null,\n grantedAt: null,\n };\n lc.debug?.(\n 'checked cvr version and ownership in ' + (Date.now() - start) + ' ms',\n );\n if (owner !== this.#taskID && (grantedAt ?? 0) > lastConnectTime) {\n throw new OwnershipError(owner, grantedAt, lastConnectTime);\n }\n if (version !== expected) {\n throw new ConcurrentModificationException(expected, version);\n }\n }\n\n async #flush(\n lc: LogContext,\n expectedCurrentVersion: CVRVersion,\n cvr: CVRSnapshot,\n lastConnectTime: number,\n ): Promise<CVRFlushStats | null> {\n const stats: CVRFlushStats = {\n instances: 0,\n queries: 0,\n desires: 0,\n clients: 0,\n rows: 0,\n rowsDeferred: 0,\n statements: 0,\n };\n if (this.#pendingRowRecordUpdates.size) {\n const existingRowRecords = await this.getRowRecords();\n this.#rowCount = existingRowRecords.size;\n for (const [id, row] of this.#pendingRowRecordUpdates.entries()) {\n if (this.#forceUpdates.has(id)) {\n continue;\n }\n const existing = existingRowRecords.get(id);\n if (\n // Don't delete or add an unreferenced row if it's not in the CVR.\n (existing === undefined && !row?.refCounts) ||\n // Don't write a row record that exactly matches what's in the CVR.\n deepEqual(\n (row ?? undefined) as ReadonlyJSONValue | undefined,\n existing as ReadonlyJSONValue | undefined,\n )\n ) {\n this.#pendingRowRecordUpdates.delete(id);\n }\n }\n }\n if (\n this.#pendingRowRecordUpdates.size === 0 &&\n this.#writes.size === 0 &&\n this.#pendingInstanceWrite === undefined &&\n this.#pendingQueryUpdates.size === 0 &&\n this.#pendingQueryPartialUpdates.size === 0 &&\n this.#pendingDesireUpdates.size === 0\n ) {\n return null;\n }\n // Note: The CVR instance itself is only updated if there are material\n // changes (i.e. changes to the CVR contents) to flush.\n this.putInstance(cvr);\n const start = Date.now();\n lc.debug?.('flush tx beginning');\n\n // Use an async callback so we can await the version/ownership check and\n // validate it INSIDE the transaction. If validation fails, the exception\n // causes postgres.js to ROLLBACK, ensuring no writes are committed on error.\n const results = await runTx(\n this.#db,\n async tx => {\n lc.debug?.(`flush tx begun after ${Date.now() - start} ms`);\n\n // Acquire row-level lock and validate version/ownership before queuing writes.\n // Throwing here (inside the begin callback) rolls back the transaction so that\n // no writes are committed when concurrent modification or ownership errors occur.\n await this.#checkVersionAndOwnership(\n lc,\n tx,\n expectedCurrentVersion,\n lastConnectTime,\n );\n\n const writeQueries = [];\n if (this.#pendingInstanceWrite) {\n writeQueries.push(this.#pendingInstanceWrite(tx, lastConnectTime));\n stats.instances++;\n stats.statements++;\n }\n for (const write of this.#writes) {\n stats.clients += write.stats.clients ?? 0;\n stats.rows += write.stats.rows ?? 0;\n\n writeQueries.push(write.write(tx, lastConnectTime));\n stats.statements++;\n }\n\n // Batch flush config writes\n // Flush queries first (desires depend on queries via foreign key)\n const hasQueryUpdates =\n this.#pendingQueryUpdates.size > 0 ||\n this.#pendingQueryPartialUpdates.size > 0;\n\n const desireFlush = this.#flushDesires(tx, lc);\n\n let queryFlushes: PendingQuery<Row[]>[] = [];\n if (hasQueryUpdates) {\n queryFlushes = this.#flushQueries(tx, lc);\n\n // Count both full updates and partial-only updates\n const partialOnlyCount = [\n ...this.#pendingQueryPartialUpdates.keys(),\n ].filter(key => !this.#pendingQueryUpdates.has(key)).length;\n\n stats.queries = this.#pendingQueryUpdates.size + partialOnlyCount;\n stats.statements +=\n (this.#pendingQueryUpdates.size > 0 ? 1 : 0) +\n (partialOnlyCount > 0 ? 1 : 0);\n\n if (desireFlush) {\n stats.desires = this.#pendingDesireUpdates.size;\n stats.statements++;\n }\n } else if (desireFlush) {\n stats.desires = this.#pendingDesireUpdates.size;\n stats.statements++;\n }\n\n const rowUpdates = this.#rowCache.executeRowUpdates(\n tx,\n cvr.version,\n this.#pendingRowRecordUpdates,\n 'allow-defer',\n lc,\n );\n stats.statements += rowUpdates.length;\n\n // Pipeline writes now that the version check has passed.\n const pipelined = [\n ...writeQueries,\n ...queryFlushes,\n ...(desireFlush ? [desireFlush] : []),\n ...rowUpdates,\n ];\n\n lc.debug?.(`returning ${pipelined.length} queries for pipelining`);\n\n // Explicitly await all pipelined queries. When the begin callback is async,\n // postgres.js does not call Promise.all() on the return value the way it does\n // for sync callbacks, so we must do it ourselves.\n return Promise.all(pipelined);\n },\n {mode: Mode.READ_COMMITTED},\n );\n\n lc.debug?.(`flush tx completed after ${Date.now() - start} ms`);\n\n // Calculate how many row update queries were in the pipeline.\n // Note: the version check was awaited separately and is not in the results array.\n const baseQueries =\n (this.#pendingInstanceWrite ? 1 : 0) +\n this.#writes.size +\n (this.#pendingQueryUpdates.size > 0 ? 1 : 0) +\n ([...this.#pendingQueryPartialUpdates.keys()].filter(\n key => !this.#pendingQueryUpdates.has(key),\n ).length > 0\n ? 1\n : 0) +\n (this.#pendingDesireUpdates.size > 0 ? 1 : 0);\n const rowUpdateCount = results.length - baseQueries;\n\n const rowsFlushed = rowUpdateCount > 0;\n if (!rowsFlushed) {\n stats.rowsDeferred = this.#pendingRowRecordUpdates.size;\n } else {\n stats.rows += this.#pendingRowRecordUpdates.size;\n }\n\n this.#rowCount = await this.#rowCache.apply(\n this.#pendingRowRecordUpdates,\n cvr.version,\n rowsFlushed,\n );\n recordRowsSynced(this.#rowCount);\n\n return stats;\n }\n\n get rowCount(): number {\n return this.#rowCount;\n }\n\n async flush(\n lc: LogContext,\n expectedCurrentVersion: CVRVersion,\n cvr: CVRSnapshot,\n lastConnectTime: number,\n ): Promise<CVRFlushStats | null> {\n const start = performance.now();\n lc = lc.withContext('cvrFlushID', flushCounter++);\n try {\n const stats = await this.#flush(\n lc,\n expectedCurrentVersion,\n cvr,\n lastConnectTime,\n );\n if (stats) {\n const elapsed = performance.now() - start;\n lc.info?.(\n `flushed cvr@${versionString(cvr.version)} ` +\n `${JSON.stringify(stats)} in (${elapsed} ms)`,\n );\n this.#rowCache.recordSyncFlushStats(stats, elapsed);\n }\n return stats;\n } catch (e) {\n // Clear cached state if an error (e.g. ConcurrentModificationException) is encountered.\n this.#rowCache.clear();\n throw e;\n } finally {\n this.#writes.clear();\n this.#pendingInstanceWrite = undefined;\n this.#pendingRowRecordUpdates.clear();\n this.#forceUpdates.clear();\n this.#pendingQueryUpdates.clear();\n this.#pendingDesireUpdates.clear();\n this.#pendingQueryPartialUpdates.clear();\n }\n }\n\n hasPendingUpdates(): boolean {\n return this.#rowCache.hasPendingUpdates();\n }\n\n /** Resolves when all pending updates are flushed. */\n flushed(lc: LogContext): Promise<void> {\n return this.#rowCache.flushed(lc);\n }\n\n async inspectQueries(\n lc: LogContext,\n ttlClock: TTLClock,\n clientID?: string,\n ): Promise<InspectQueryRow[]> {\n const db = this.#db;\n const clientGroupID = this.#id;\n\n const reader = new TransactionPool(lc, Mode.READONLY).run(db);\n try {\n return await reader.processReadTask(\n tx => tx<InspectQueryRow[]>`\n SELECT DISTINCT ON (d.\"clientID\", d.\"queryHash\")\n d.\"clientID\",\n d.\"queryHash\" AS \"queryID\",\n COALESCE(d.\"ttlMs\", ${DEFAULT_TTL_MS}) AS \"ttl\",\n d.\"inactivatedAtMs\" AS \"inactivatedAt\",\n (SELECT COUNT(*)::INT FROM ${this.#cvr('rows')} r \n WHERE r.\"clientGroupID\" = d.\"clientGroupID\" \n AND r.\"refCounts\" ? d.\"queryHash\") AS \"rowCount\",\n q.\"clientAST\" AS \"ast\",\n (q.\"patchVersion\" IS NOT NULL) AS \"got\",\n COALESCE(d.\"deleted\", FALSE) AS \"deleted\",\n q.\"queryName\" AS \"name\",\n q.\"queryArgs\" AS \"args\"\n FROM ${this.#cvr('desires')} d\n LEFT JOIN ${this.#cvr('queries')} q\n ON q.\"clientGroupID\" = d.\"clientGroupID\"\n AND q.\"queryHash\" = d.\"queryHash\"\n WHERE d.\"clientGroupID\" = ${clientGroupID}\n ${clientID ? tx`AND d.\"clientID\" = ${clientID}` : tx``}\n AND NOT (\n d.\"inactivatedAtMs\" IS NOT NULL \n AND d.\"ttlMs\" IS NOT NULL \n AND (d.\"inactivatedAtMs\" + d.\"ttlMs\") <= ${ttlClockAsNumber(ttlClock)}\n )\n ORDER BY d.\"clientID\", d.\"queryHash\"`,\n );\n } finally {\n reader.setDone();\n }\n }\n}\n\n/**\n * This is similar to {@link CVRStore.#checkVersionAndOwnership} except\n * that it only checks the version and is suitable for snapshot reads\n * (i.e. by doing a plain `SELECT` rather than a `SELECT ... FOR UPDATE`).\n */\nexport async function checkVersion(\n tx: PostgresTransaction,\n schema: string,\n clientGroupID: string,\n expectedCurrentVersion: CVRVersion,\n): Promise<void> {\n const expected = versionString(expectedCurrentVersion);\n const result = await tx<Pick<InstancesRow, 'version'>[]>`\n SELECT version FROM ${tx(schema)}.instances \n WHERE \"clientGroupID\" = ${clientGroupID}`;\n const {version} =\n result.length > 0 ? result[0] : {version: EMPTY_CVR_VERSION.stateVersion};\n if (version !== expected) {\n throw new ConcurrentModificationException(expected, version);\n }\n}\n\nexport class ClientNotFoundError extends ProtocolErrorWithLevel {\n constructor(message: string) {\n super(\n {\n kind: ErrorKind.ClientNotFound,\n message,\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n}\n\nexport class ConcurrentModificationException extends ProtocolErrorWithLevel {\n readonly name = 'ConcurrentModificationException';\n\n constructor(expectedVersion: string, actualVersion: string) {\n super(\n {\n kind: ErrorKind.Internal,\n message: `CVR has been concurrently modified. Expected ${expectedVersion}, got ${actualVersion}`,\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n}\n\nexport class OwnershipError extends ProtocolErrorWithLevel {\n readonly name = 'OwnershipError';\n\n constructor(\n owner: string | null,\n grantedAt: number | null,\n lastConnectTime: number,\n ) {\n super(\n {\n kind: ErrorKind.Rehome,\n message:\n `CVR ownership was transferred to ${owner} at ` +\n `${new Date(grantedAt ?? 0).toISOString()} ` +\n `(last connect time: ${new Date(lastConnectTime).toISOString()})`,\n maxBackoffMs: 0,\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n );\n }\n}\n\nexport class InvalidClientSchemaError extends ProtocolErrorWithLevel {\n readonly name = 'InvalidClientSchemaError';\n\n constructor(cause: unknown) {\n super(\n {\n kind: ErrorKind.SchemaVersionNotSupported,\n message: `Could not parse clientSchema stored in CVR: ${String(cause)}`,\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n {cause},\n );\n }\n}\n\nexport class RowsVersionBehindError extends Error {\n readonly name = 'RowsVersionBehindError';\n readonly cvrVersion: string;\n readonly rowsVersion: string | null;\n\n constructor(cvrVersion: string, rowsVersion: string | null) {\n super(`rowsVersion (${rowsVersion}) is behind CVR ${cvrVersion}`);\n this.cvrVersion = cvrVersion;\n this.rowsVersion = rowsVersion;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cvr-store.js","names":["#schema","#taskID","#id","#failService","#db","#writes","#pendingRowRecordUpdates","#forceUpdates","#rowCache","#loadAttemptIntervalMs","#maxLoadAttempts","#pendingQueryUpdates","#pendingDesireUpdates","#pendingQueryPartialUpdates","#load","#cvr","#pendingInstanceWrite","#updateQueryFields","#checkVersionAndOwnership","#flush","#rowCount","#flushDesires","#flushQueries"],"sources":["../../../../../../zero-cache/src/services/view-syncer/cvr-store.ts"],"sourcesContent":["import {trace} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport type {MaybeRow, PendingQuery, Row} from 'postgres';\nimport {startAsyncSpan} from '../../../../otel/src/span.ts';\nimport {version} from '../../../../otel/src/version.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {CustomKeyMap} from '../../../../shared/src/custom-key-map.ts';\nimport {CustomKeySet} from '../../../../shared/src/custom-key-set.ts';\nimport {\n deepEqual,\n type ReadonlyJSONValue,\n} from '../../../../shared/src/json.ts';\nimport {sleep} from '../../../../shared/src/sleep.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {astSchema} from '../../../../zero-protocol/src/ast.ts';\nimport {clientSchemaSchema} from '../../../../zero-protocol/src/client-schema.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport type {InspectQueryRow} from '../../../../zero-protocol/src/inspect-down.ts';\nimport {clampTTL, DEFAULT_TTL_MS} from '../../../../zql/src/query/ttl.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {TransactionPool} from '../../db/transaction-pool.ts';\nimport {recordRowsSynced} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {PostgresDB, PostgresTransaction} from '../../types/pg.ts';\nimport {rowIDString} from '../../types/row-key.ts';\nimport {cvrSchema, type ShardID} from '../../types/shards.ts';\nimport type {Patch, PatchToVersion} from './client-handler.ts';\nimport type {CVR, CVRSnapshot} from './cvr.ts';\nimport {RowRecordCache} from './row-record-cache.ts';\nimport {\n type ClientsRow,\n type DesiresRow,\n type InstancesRow,\n type QueriesRow,\n type RowsRow,\n} from './schema/cvr.ts';\nimport {\n type ClientQueryRecord,\n type ClientRecord,\n cmpVersions,\n type CustomQueryRecord,\n type CVRVersion,\n EMPTY_CVR_VERSION,\n type InternalQueryRecord,\n type NullableCVRVersion,\n type QueryPatch,\n type QueryRecord,\n queryRecordToQueryRow,\n type RowID,\n type RowRecord,\n versionFromString,\n versionString,\n} from './schema/types.ts';\nimport {\n type TTLClock,\n ttlClockAsNumber,\n ttlClockFromNumber,\n} from './ttl-clock.ts';\n\nexport type CVRFlushStats = {\n instances: number;\n queries: number;\n desires: number;\n clients: number;\n rows: number;\n rowsDeferred: number;\n statements: number;\n};\n\nlet flushCounter = 0;\n\n/**\n * Convert TTL/timestamp values for both old (seconds-based) and new (ms-based) columns.\n * Old columns: inactivatedAt (TIMESTAMPTZ), ttl (INTERVAL) - need conversion ms->seconds\n * New columns: inactivatedAtMs (DOUBLE PRECISION), ttlMs (DOUBLE PRECISION) - store ms directly\n */\nfunction convertTTLValues(\n inactivatedAt: TTLClock | undefined,\n ttl: number,\n): {\n ttlInterval: number | null;\n ttlMs: number | null;\n inactivatedAtTimestamp: TTLClock | null;\n inactivatedAtMs: TTLClock | null;\n} {\n return {\n ttlInterval: ttl < 0 ? null : ttl / 1000, // INTERVAL needs seconds\n ttlMs: ttl < 0 ? null : ttl, // New column stores ms directly\n inactivatedAtTimestamp:\n inactivatedAt === undefined\n ? null\n : ttlClockFromNumber(ttlClockAsNumber(inactivatedAt) / 1000),\n inactivatedAtMs: inactivatedAt ?? null,\n };\n}\n\nconst tracer = trace.getTracer('cvr-store', version);\n\n/**\n * QueriesRow with queryArgs as a stringified JSON value.\n * Used for batched config writes where queryArgs are pre-stringified\n * to handle the postgres.js boolean array bug.\n */\ntype StringifiedQueriesRow = Omit<QueriesRow, 'queryArgs'> & {\n queryArgs: string | null;\n};\n\nfunction asQuery(row: QueriesRow): QueryRecord {\n const maybeVersion = (s: string | null) =>\n s === null ? undefined : versionFromString(s);\n\n if (row.clientAST === null) {\n // custom query\n assert(\n row.queryName !== null && row.queryArgs !== null,\n 'queryName and queryArgs must be set for custom queries',\n );\n return {\n type: 'custom',\n id: row.queryHash,\n name: row.queryName,\n args: row.queryArgs,\n patchVersion: maybeVersion(row.patchVersion),\n clientState: {},\n transformationHash: row.transformationHash ?? undefined,\n transformationVersion: maybeVersion(row.transformationVersion),\n } satisfies CustomQueryRecord;\n }\n\n const ast = astSchema.parse(row.clientAST);\n return row.internal\n ? ({\n type: 'internal',\n id: row.queryHash,\n ast,\n transformationHash: row.transformationHash ?? undefined,\n transformationVersion: maybeVersion(row.transformationVersion),\n } satisfies InternalQueryRecord)\n : ({\n type: 'client',\n id: row.queryHash,\n ast,\n patchVersion: maybeVersion(row.patchVersion),\n clientState: {},\n transformationHash: row.transformationHash ?? undefined,\n transformationVersion: maybeVersion(row.transformationVersion),\n } satisfies ClientQueryRecord);\n}\n\n// The time to wait between load attempts.\nconst LOAD_ATTEMPT_INTERVAL_MS = 500;\n// The maximum number of load() attempts if the rowsVersion is behind.\n// This currently results in a maximum catchup time of ~5 seconds, after\n// which we give up and consider the CVR invalid.\n//\n// TODO: Make this configurable with something like --max-catchup-wait-ms,\n// as it is technically application specific.\nconst MAX_LOAD_ATTEMPTS = 10;\n\nexport class CVRStore {\n readonly #schema: string;\n readonly #taskID: string;\n readonly #id: string;\n readonly #failService: (e: unknown) => void;\n readonly #db: PostgresDB;\n readonly #writes: Set<{\n stats: Partial<CVRFlushStats>;\n write: (\n tx: PostgresTransaction,\n lastConnectTime: number,\n ) => PendingQuery<MaybeRow[]>;\n }> = new Set();\n // Stored separately so repeated putInstance() calls (e.g. setClientSchema,\n // setProfileID, and the final call in #flush) replace each other rather than\n // accumulating as independent statements in #writes.\n #pendingInstanceWrite:\n | ((\n tx: PostgresTransaction,\n lastConnectTime: number,\n ) => PendingQuery<MaybeRow[]>)\n | undefined = undefined;\n readonly #pendingRowRecordUpdates = new CustomKeyMap<RowID, RowRecord | null>(\n rowIDString,\n );\n readonly #forceUpdates = new CustomKeySet<RowID>(rowIDString);\n readonly #rowCache: RowRecordCache;\n readonly #loadAttemptIntervalMs: number;\n readonly #maxLoadAttempts: number;\n #rowCount: number = 0;\n readonly #pendingQueryUpdates = new Map<string, StringifiedQueriesRow>();\n readonly #pendingDesireUpdates = new Map<string, DesiresRow>();\n readonly #pendingQueryPartialUpdates = new Map<string, Partial<QueriesRow>>();\n\n constructor(\n lc: LogContext,\n cvrDb: PostgresDB,\n shard: ShardID,\n taskID: string,\n cvrID: string,\n failService: (e: unknown) => void,\n loadAttemptIntervalMs = LOAD_ATTEMPT_INTERVAL_MS,\n maxLoadAttempts = MAX_LOAD_ATTEMPTS,\n deferredRowFlushThreshold = 100, // somewhat arbitrary\n setTimeoutFn = setTimeout,\n ) {\n this.#failService = failService;\n this.#db = cvrDb;\n this.#schema = cvrSchema(shard);\n this.#taskID = taskID;\n this.#id = cvrID;\n this.#rowCache = new RowRecordCache(\n lc,\n cvrDb,\n shard,\n cvrID,\n failService,\n deferredRowFlushThreshold,\n setTimeoutFn,\n );\n this.#loadAttemptIntervalMs = loadAttemptIntervalMs;\n this.#maxLoadAttempts = maxLoadAttempts;\n }\n\n #cvr(table: string) {\n return this.#db(`${this.#schema}.${table}`);\n }\n\n #updateQueryFields(queryHash: string, fields: Partial<QueriesRow>): void {\n // Track as partial-only update for batched flush\n this.#pendingQueryPartialUpdates.set(queryHash, fields);\n }\n\n load(lc: LogContext, lastConnectTime: number): Promise<CVR> {\n return startAsyncSpan(tracer, 'cvr.load', async () => {\n let err: RowsVersionBehindError | undefined;\n for (let i = 0; i < this.#maxLoadAttempts; i++) {\n if (i > 0) {\n await sleep(this.#loadAttemptIntervalMs);\n }\n const result = await this.#load(lc, lastConnectTime);\n if (result instanceof RowsVersionBehindError) {\n lc.info?.(`attempt ${i + 1}: ${String(result)}`);\n err = result;\n continue;\n }\n return result;\n }\n assert(err, 'Expected error to be set after retry loop exhausted');\n throw new ClientNotFoundError(\n `max attempts exceeded waiting for CVR@${err.cvrVersion} to catch up from ${err.rowsVersion}`,\n );\n });\n }\n\n async #load(\n lc: LogContext,\n lastConnectTime: number,\n ): Promise<CVR | RowsVersionBehindError> {\n const start = Date.now();\n\n const id = this.#id;\n const cvr: CVR = {\n id,\n version: EMPTY_CVR_VERSION,\n lastActive: 0,\n ttlClock: ttlClockFromNumber(0), // TTL clock starts at 0, not Date.now()\n replicaVersion: null,\n clients: {},\n queries: {},\n clientSchema: null,\n profileID: null,\n };\n\n const [instance, clientsRows, queryRows, desiresRows] = await runTx(\n this.#db,\n tx => {\n lc.debug?.(`CVR tx started after ${Date.now() - start} ms`);\n return [\n tx<\n (Omit<InstancesRow, 'clientGroupID'> & {\n profileID: string | null;\n deleted: boolean;\n rowsVersion: string | null;\n })[]\n >`SELECT cvr.\"version\",\n \"lastActive\",\n \"ttlClock\",\n \"replicaVersion\",\n \"owner\",\n \"grantedAt\",\n \"clientSchema\",\n \"profileID\",\n \"deleted\",\n rows.\"version\" as \"rowsVersion\"\n FROM ${this.#cvr('instances')} AS cvr\n LEFT JOIN ${this.#cvr('rowsVersion')} AS rows\n ON cvr.\"clientGroupID\" = rows.\"clientGroupID\"\n WHERE cvr.\"clientGroupID\" = ${id}`,\n tx<Pick<ClientsRow, 'clientID'>[]>`SELECT \"clientID\" FROM ${this.#cvr(\n 'clients',\n )}\n WHERE \"clientGroupID\" = ${id}`,\n tx<QueriesRow[]>`SELECT * FROM ${this.#cvr('queries')}\n WHERE \"clientGroupID\" = ${id} AND deleted IS DISTINCT FROM true`,\n tx<DesiresRow[]>`SELECT\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttlMs\" AS \"ttl\",\n \"inactivatedAtMs\" AS \"inactivatedAt\"\n FROM ${this.#cvr('desires')}\n WHERE \"clientGroupID\" = ${id}`,\n ];\n },\n {mode: Mode.READONLY},\n );\n lc.debug?.(\n `CVR tx completed after ${Date.now() - start} ms ` +\n `(${clientsRows.length} clients, ${queryRows.length} queries, ${desiresRows.length} desires)`,\n );\n\n if (instance.length === 0) {\n // This is the first time we see this CVR.\n this.putInstance({\n version: cvr.version,\n lastActive: 0,\n ttlClock: ttlClockFromNumber(0), // TTL clock starts at 0 for new instances\n replicaVersion: null,\n clientSchema: null,\n profileID: null,\n });\n } else {\n assert(\n instance.length === 1,\n () => `Expected exactly one CVR instance, got ${instance.length}`,\n );\n const {\n version,\n lastActive,\n ttlClock,\n replicaVersion,\n owner,\n grantedAt,\n rowsVersion,\n clientSchema,\n profileID,\n deleted,\n } = instance[0];\n\n if (deleted) {\n throw new ClientNotFoundError(\n 'Client has been purged due to inactivity',\n );\n }\n\n if (owner !== this.#taskID) {\n if ((grantedAt ?? 0) > lastConnectTime) {\n throw new OwnershipError(owner, grantedAt, lastConnectTime);\n } else {\n // Fire-and-forget an ownership change to signal the current owner.\n // Note that the query is structured such that it only succeeds in the\n // correct conditions (i.e. gated on `grantedAt`).\n void this.#db`\n UPDATE ${this.#cvr('instances')} \n SET \"owner\" = ${this.#taskID}, \n \"grantedAt\" = ${lastConnectTime}\n WHERE \"clientGroupID\" = ${this.#id} AND\n (\"grantedAt\" IS NULL OR\n \"grantedAt\" <= to_timestamp(${lastConnectTime / 1000}))\n `\n .execute()\n .catch(this.#failService);\n }\n }\n\n if (version !== (rowsVersion ?? EMPTY_CVR_VERSION.stateVersion)) {\n // This will cause the load() method to wait for row catchup and retry.\n // Assuming the ownership signal succeeds, the current owner will stop\n // modifying the CVR and flush its pending row changes.\n return new RowsVersionBehindError(version, rowsVersion);\n }\n\n cvr.version = versionFromString(version);\n cvr.lastActive = lastActive;\n cvr.ttlClock = ttlClock;\n cvr.replicaVersion = replicaVersion;\n cvr.profileID = profileID;\n\n try {\n cvr.clientSchema =\n clientSchema === null\n ? null\n : v.parse(clientSchema, clientSchemaSchema);\n } catch (e) {\n throw new InvalidClientSchemaError(e);\n }\n }\n\n for (const row of clientsRows) {\n cvr.clients[row.clientID] = {\n id: row.clientID,\n desiredQueryIDs: [],\n };\n }\n\n for (const row of queryRows) {\n const query = asQuery(row);\n cvr.queries[row.queryHash] = query;\n }\n\n for (const row of desiresRows) {\n const client = cvr.clients[row.clientID];\n // Note: row.inactivatedAt is mapped from inactivatedAtMs in the SQL query\n if (!row.deleted && row.inactivatedAt === null) {\n if (client) {\n client.desiredQueryIDs.push(row.queryHash);\n } else {\n // This can happen if the client was deleted but the queries are still alive.\n lc.debug?.(\n `Not adding to desiredQueryIDs for client ${row.clientID} because it has been deleted.`,\n );\n }\n }\n\n const query = cvr.queries[row.queryHash];\n if (\n query &&\n query.type !== 'internal' &&\n (!row.deleted || row.inactivatedAt !== null)\n ) {\n query.clientState[row.clientID] = {\n inactivatedAt: row.inactivatedAt ?? undefined,\n ttl: clampTTL(row.ttl ?? DEFAULT_TTL_MS),\n version: versionFromString(row.patchVersion),\n };\n }\n }\n\n lc.info?.(\n `loaded cvr@${versionString(cvr.version)} (${Date.now() - start} ms)`,\n );\n\n // why do we not sort `desiredQueryIDs` here?\n\n return cvr;\n }\n\n getRowRecords(): Promise<ReadonlyMap<RowID, RowRecord>> {\n return this.#rowCache.getRowRecords();\n }\n\n putRowRecord(row: RowRecord): void {\n this.#pendingRowRecordUpdates.set(row.id, row);\n }\n\n /**\n * Note: Removing a row from the CVR should be represented by a\n * {@link putRowRecord()} with `refCounts: null` in order to properly\n * produce the appropriate delete patch when catching up old clients.\n *\n * This `delRowRecord()` method, on the other hand, is only used for\n * \"canceling\" the put of a row that was not in the CVR in the first place.\n */\n delRowRecord(id: RowID): void {\n this.#pendingRowRecordUpdates.set(id, null);\n }\n\n /**\n * Overrides the default logic that removes no-op writes and forces\n * the updates for the given row `ids`. This has no effect if there\n * are no corresponding puts or dels for the associated row records.\n */\n forceUpdates(...ids: RowID[]) {\n for (const id of ids) {\n this.#forceUpdates.add(id);\n }\n }\n\n /**\n * Updates the `ttlClock` of the CVR instance. The ttlClock starts at 0 when\n * the CVR instance is first created and increments based on elapsed time\n * since the base time established by the ViewSyncerService.\n */\n async updateTTLClock(ttlClock: TTLClock, lastActive: number): Promise<void> {\n await this.#db`UPDATE ${this.#cvr('instances')}\n SET \"lastActive\" = ${lastActive},\n \"ttlClock\" = ${ttlClock}\n WHERE \"clientGroupID\" = ${this.#id}`.execute();\n }\n\n /**\n * @returns This returns the current `ttlClock` of the CVR instance. The ttlClock\n * represents elapsed time since the instance was created (starting from 0).\n * If the CVR has never been initialized for this client group, it returns\n * `undefined`.\n */\n async getTTLClock(): Promise<TTLClock | undefined> {\n const result = await this.#db<Pick<InstancesRow, 'ttlClock'>[]>`\n SELECT \"ttlClock\" FROM ${this.#cvr('instances')}\n WHERE \"clientGroupID\" = ${this.#id}`.values();\n if (result.length === 0) {\n // This can happen if the CVR has not been initialized yet.\n return undefined;\n }\n assert(\n result.length === 1,\n () => `Expected exactly one rowsVersion result, got ${result.length}`,\n );\n return result[0][0];\n }\n\n putInstance({\n version,\n replicaVersion,\n lastActive,\n clientSchema,\n profileID,\n ttlClock,\n }: Pick<\n CVRSnapshot,\n | 'version'\n | 'replicaVersion'\n | 'lastActive'\n | 'clientSchema'\n | 'profileID'\n | 'ttlClock'\n >): void {\n // Overwrite any previously queued instance write — only the last call\n // matters since they all target the same row.\n this.#pendingInstanceWrite = (tx, lastConnectTime) => {\n const change: InstancesRow = {\n clientGroupID: this.#id,\n version: versionString(version),\n lastActive,\n ttlClock,\n replicaVersion,\n owner: this.#taskID,\n grantedAt: lastConnectTime,\n clientSchema,\n profileID,\n };\n return tx`\n INSERT INTO ${this.#cvr('instances')} ${tx(change)} \n ON CONFLICT (\"clientGroupID\") DO UPDATE SET ${tx(change)}`;\n };\n }\n\n markQueryAsDeleted(version: CVRVersion, queryPatch: QueryPatch): void {\n this.#updateQueryFields(queryPatch.id, {\n patchVersion: versionString(version),\n deleted: true,\n transformationHash: null,\n transformationVersion: null,\n });\n }\n\n putQuery(query: QueryRecord): void {\n const change = queryRecordToQueryRow(this.#id, query);\n\n const c = {\n ...change,\n // Pre-stringify queryArgs to handle postgres.js boolean array bug\n queryArgs:\n change.queryArgs !== null ? JSON.stringify(change.queryArgs) : null,\n transformationHash: change.transformationHash ?? null,\n transformationVersion: change.transformationVersion ?? null,\n deleted: change.deleted ?? false,\n };\n this.#pendingQueryUpdates.set(query.id, c);\n }\n\n updateQuery(query: QueryRecord) {\n const maybeVersionString = (v: CVRVersion | undefined) =>\n v ? versionString(v) : null;\n this.#updateQueryFields(query.id, {\n patchVersion:\n query.type === 'internal'\n ? null\n : maybeVersionString(query.patchVersion),\n transformationHash: query.transformationHash ?? null,\n transformationVersion: maybeVersionString(query.transformationVersion),\n deleted: false,\n });\n }\n\n insertClient(client: ClientRecord): void {\n const change: ClientsRow = {\n clientGroupID: this.#id,\n clientID: client.id,\n };\n\n this.#writes.add({\n stats: {clients: 1},\n write: tx => tx`INSERT INTO ${this.#cvr('clients')} ${tx(change)}`,\n });\n }\n\n deleteClient(clientID: string) {\n this.#writes.add({\n stats: {clients: 1},\n write: sql =>\n sql`DELETE FROM ${this.#cvr('clients')}\n WHERE \"clientGroupID\" = ${this.#id}\n AND \"clientID\" = ${clientID}`,\n });\n }\n\n putDesiredQuery(\n newVersion: CVRVersion,\n query: {id: string},\n client: {id: string},\n deleted: boolean,\n inactivatedAt: TTLClock | undefined,\n ttl: number,\n ): void {\n const {ttlMs, inactivatedAtMs} = convertTTLValues(inactivatedAt, ttl);\n\n const change: DesiresRow = {\n clientGroupID: this.#id,\n clientID: client.id,\n deleted,\n inactivatedAt: inactivatedAtMs,\n patchVersion: versionString(newVersion),\n queryHash: query.id,\n ttl: ttlMs,\n };\n\n // Use composite key to deduplicate/replace entries for the same client-query pair\n const key = `${client.id}:${query.id}`;\n this.#pendingDesireUpdates.set(key, change);\n }\n\n catchupRowPatches(\n lc: LogContext,\n afterVersion: NullableCVRVersion,\n upToCVR: CVRSnapshot,\n current: CVRVersion,\n excludeQueryHashes: string[] = [],\n ): AsyncGenerator<RowsRow[], void, undefined> {\n return this.#rowCache.catchupRowPatches(\n lc,\n afterVersion,\n upToCVR,\n current,\n excludeQueryHashes,\n );\n }\n\n async catchupConfigPatches(\n lc: LogContext,\n afterVersion: NullableCVRVersion,\n upToCVR: CVRSnapshot,\n current: CVRVersion,\n ): Promise<PatchToVersion[]> {\n if (cmpVersions(afterVersion, upToCVR.version) >= 0) {\n return [];\n }\n\n const startMs = Date.now();\n const start = afterVersion ? versionString(afterVersion) : '';\n const end = versionString(upToCVR.version);\n lc.debug?.(`scanning config patches for clients from ${start}`);\n\n const reader = new TransactionPool(lc, {mode: Mode.READONLY}).run(this.#db);\n try {\n // Verify that we are reading the right version of the CVR.\n await reader.processReadTask(tx =>\n checkVersion(tx, this.#schema, this.#id, current),\n );\n\n const [allDesires, queryRows] = await reader.processReadTask(tx =>\n Promise.all([\n tx<DesiresRow[]>`\n SELECT * FROM ${this.#cvr('desires')}\n WHERE \"clientGroupID\" = ${this.#id}\n AND \"patchVersion\" > ${start}\n AND \"patchVersion\" <= ${end}`,\n tx<Pick<QueriesRow, 'deleted' | 'queryHash' | 'patchVersion'>[]>`\n SELECT deleted, \"queryHash\", \"patchVersion\" FROM ${this.#cvr('queries')}\n WHERE \"clientGroupID\" = ${this.#id}\n AND \"patchVersion\" > ${start}\n AND \"patchVersion\" <= ${end}`,\n ]),\n );\n\n const patches: PatchToVersion[] = [];\n for (const row of queryRows) {\n const {queryHash: id} = row;\n const patch: Patch = row.deleted\n ? {type: 'query', op: 'del', id}\n : {type: 'query', op: 'put', id};\n const v = row.patchVersion;\n assert(v, 'patchVersion must be set for query patches');\n patches.push({patch, toVersion: versionFromString(v)});\n }\n for (const row of allDesires) {\n const {clientID, queryHash: id} = row;\n const patch: Patch = row.deleted\n ? {type: 'query', op: 'del', id, clientID}\n : {type: 'query', op: 'put', id, clientID};\n patches.push({patch, toVersion: versionFromString(row.patchVersion)});\n }\n\n lc.debug?.(\n `${patches.length} config patches (${Date.now() - startMs} ms)`,\n );\n return patches;\n } finally {\n reader.setDone();\n }\n }\n\n #flushQueries(\n tx: PostgresTransaction,\n lc: LogContext,\n ): PendingQuery<Row[]>[] {\n // Merge partial updates into full updates\n const partialOnly = new Map<string, Partial<QueriesRow>>();\n for (const [queryHash, partial] of this.#pendingQueryPartialUpdates) {\n const existing = this.#pendingQueryUpdates.get(queryHash);\n if (existing) {\n // Merge partial into full update\n Object.assign(existing, partial);\n } else {\n // Track partial-only updates to batch separately\n partialOnly.set(queryHash, partial);\n }\n }\n\n const queries: PendingQuery<Row[]>[] = [];\n\n // Batch full updates\n if (this.#pendingQueryUpdates.size > 0) {\n const rows = [...this.#pendingQueryUpdates.values()];\n lc.debug?.(`Batch flushing ${rows.length} full query updates`);\n\n queries.push(tx`\n INSERT INTO ${this.#cvr('queries')} (\n \"clientGroupID\",\n \"queryHash\",\n \"clientAST\",\n \"queryName\",\n \"queryArgs\",\n \"patchVersion\",\n \"transformationHash\",\n \"transformationVersion\",\n \"internal\",\n \"deleted\"\n )\n SELECT\n \"clientGroupID\",\n \"queryHash\",\n \"clientAST\",\n \"queryName\",\n CASE\n WHEN \"queryArgs\" IS NULL THEN NULL\n ELSE \"queryArgs\"::json\n END,\n \"patchVersion\",\n \"transformationHash\",\n \"transformationVersion\",\n \"internal\",\n \"deleted\"\n FROM json_to_recordset(${rows}) AS x(\n \"clientGroupID\" TEXT,\n \"queryHash\" TEXT,\n \"clientAST\" JSONB,\n \"queryName\" TEXT,\n \"queryArgs\" TEXT,\n \"patchVersion\" TEXT,\n \"transformationHash\" TEXT,\n \"transformationVersion\" TEXT,\n \"internal\" BOOLEAN,\n \"deleted\" BOOLEAN\n )\n ON CONFLICT (\"clientGroupID\", \"queryHash\") DO UPDATE SET\n \"clientAST\" = excluded.\"clientAST\",\n \"queryName\" = excluded.\"queryName\",\n \"queryArgs\" = CASE \n WHEN excluded.\"queryArgs\" IS NULL THEN NULL\n ELSE excluded.\"queryArgs\"::json\n END,\n \"patchVersion\" = excluded.\"patchVersion\",\n \"transformationHash\" = excluded.\"transformationHash\",\n \"transformationVersion\" = excluded.\"transformationVersion\",\n \"internal\" = excluded.\"internal\",\n \"deleted\" = excluded.\"deleted\"\n `);\n }\n\n // Batch partial-only updates\n if (partialOnly.size > 0) {\n lc.debug?.(`Batch flushing ${partialOnly.size} partial query updates`);\n const rows = Array.from(\n partialOnly.entries(),\n ([queryHash, partial]) => ({\n clientGroupID: this.#id,\n queryHash,\n patchVersionSet: partial.patchVersion !== undefined,\n patchVersion: partial.patchVersion ?? null,\n deletedSet: partial.deleted !== undefined,\n deleted: partial.deleted ?? null,\n transformationHashSet: partial.transformationHash !== undefined,\n transformationHash: partial.transformationHash ?? null,\n transformationVersionSet: partial.transformationVersion !== undefined,\n transformationVersion: partial.transformationVersion ?? null,\n }),\n );\n queries.push(tx`\n UPDATE ${this.#cvr('queries')} AS q\n SET\n \"patchVersion\" = CASE\n WHEN u.\"patchVersionSet\" THEN u.\"patchVersion\"\n ELSE q.\"patchVersion\"\n END,\n \"deleted\" = CASE\n WHEN u.\"deletedSet\" THEN u.\"deleted\"\n ELSE q.\"deleted\"\n END,\n \"transformationHash\" = CASE\n WHEN u.\"transformationHashSet\" THEN u.\"transformationHash\"\n ELSE q.\"transformationHash\"\n END,\n \"transformationVersion\" = CASE\n WHEN u.\"transformationVersionSet\" THEN u.\"transformationVersion\"\n ELSE q.\"transformationVersion\"\n END\n FROM json_to_recordset(${rows}) AS u(\n \"clientGroupID\" TEXT,\n \"queryHash\" TEXT,\n \"patchVersionSet\" BOOLEAN,\n \"patchVersion\" TEXT,\n \"deletedSet\" BOOLEAN,\n \"deleted\" BOOLEAN,\n \"transformationHashSet\" BOOLEAN,\n \"transformationHash\" TEXT,\n \"transformationVersionSet\" BOOLEAN,\n \"transformationVersion\" TEXT\n )\n WHERE q.\"clientGroupID\" = u.\"clientGroupID\"\n AND q.\"queryHash\" = u.\"queryHash\"\n `);\n }\n\n return queries;\n }\n\n #flushDesires(\n tx: PostgresTransaction,\n lc: LogContext,\n ): PendingQuery<Row[]> | null {\n if (this.#pendingDesireUpdates.size === 0) {\n return null;\n }\n\n const rows = Array.from(this.#pendingDesireUpdates.values(), row => {\n const {ttlInterval, ttlMs, inactivatedAtTimestamp, inactivatedAtMs} =\n convertTTLValues(row.inactivatedAt ?? undefined, row.ttl ?? -1);\n return {\n clientGroupID: row.clientGroupID,\n clientID: row.clientID,\n queryHash: row.queryHash,\n patchVersion: row.patchVersion,\n deleted: row.deleted,\n ttl: ttlInterval,\n ttlMs,\n inactivatedAt: inactivatedAtTimestamp,\n inactivatedAtMs,\n };\n });\n\n lc.debug?.(`Batch flushing ${rows.length} desire updates`);\n\n return tx`\n INSERT INTO ${this.#cvr('desires')} (\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttl\",\n \"ttlMs\",\n \"inactivatedAt\",\n \"inactivatedAtMs\"\n )\n SELECT\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttl\",\n \"ttlMs\",\n CASE\n WHEN \"inactivatedAt\" IS NULL THEN NULL\n -- Divide by 1000 because postgres.js serializeTimestamp treats numbers as ms\n -- and to_timestamp expects seconds. This matches non-batched behavior.\n ELSE to_timestamp(\"inactivatedAt\" / 1000.0)\n END,\n \"inactivatedAtMs\"\n FROM json_to_recordset(${rows}) AS x(\n \"clientGroupID\" TEXT,\n \"clientID\" TEXT,\n \"queryHash\" TEXT,\n \"patchVersion\" TEXT,\n \"deleted\" BOOLEAN,\n \"ttl\" INTERVAL,\n \"ttlMs\" DOUBLE PRECISION,\n \"inactivatedAt\" DOUBLE PRECISION,\n \"inactivatedAtMs\" DOUBLE PRECISION\n )\n ON CONFLICT (\"clientGroupID\", \"clientID\", \"queryHash\") DO UPDATE SET\n \"patchVersion\" = excluded.\"patchVersion\",\n \"deleted\" = excluded.\"deleted\",\n \"ttl\" = excluded.\"ttl\",\n \"ttlMs\" = excluded.\"ttlMs\",\n \"inactivatedAt\" = excluded.\"inactivatedAt\",\n \"inactivatedAtMs\" = excluded.\"inactivatedAtMs\"\n `;\n }\n\n async #checkVersionAndOwnership(\n lc: LogContext,\n tx: PostgresTransaction,\n expectedCurrentVersion: CVRVersion,\n lastConnectTime: number,\n ): Promise<void> {\n const start = Date.now();\n lc.debug?.('checking cvr version and ownership');\n const result = await tx<\n Pick<InstancesRow, 'version' | 'owner' | 'grantedAt'>[]\n >`SELECT \"version\", \"owner\", \"grantedAt\" FROM ${this.#cvr('instances')}\n WHERE \"clientGroupID\" = ${this.#id}\n FOR UPDATE`;\n const expected = versionString(expectedCurrentVersion);\n const {version, owner, grantedAt} =\n result.length > 0\n ? result[0]\n : {\n version: EMPTY_CVR_VERSION.stateVersion,\n owner: null,\n grantedAt: null,\n };\n lc.debug?.(\n 'checked cvr version and ownership in ' + (Date.now() - start) + ' ms',\n );\n if (owner !== this.#taskID && (grantedAt ?? 0) > lastConnectTime) {\n throw new OwnershipError(owner, grantedAt, lastConnectTime);\n }\n if (version !== expected) {\n throw new ConcurrentModificationException(expected, version);\n }\n }\n\n async #flush(\n lc: LogContext,\n expectedCurrentVersion: CVRVersion,\n cvr: CVRSnapshot,\n lastConnectTime: number,\n ): Promise<CVRFlushStats | null> {\n const stats: CVRFlushStats = {\n instances: 0,\n queries: 0,\n desires: 0,\n clients: 0,\n rows: 0,\n rowsDeferred: 0,\n statements: 0,\n };\n if (this.#pendingRowRecordUpdates.size) {\n const existingRowRecords = await this.getRowRecords();\n this.#rowCount = existingRowRecords.size;\n for (const [id, row] of this.#pendingRowRecordUpdates.entries()) {\n if (this.#forceUpdates.has(id)) {\n continue;\n }\n const existing = existingRowRecords.get(id);\n if (\n // Don't delete or add an unreferenced row if it's not in the CVR.\n (existing === undefined && !row?.refCounts) ||\n // Don't write a row record that exactly matches what's in the CVR.\n deepEqual(\n (row ?? undefined) as ReadonlyJSONValue | undefined,\n existing as ReadonlyJSONValue | undefined,\n )\n ) {\n this.#pendingRowRecordUpdates.delete(id);\n }\n }\n }\n if (\n this.#pendingRowRecordUpdates.size === 0 &&\n this.#writes.size === 0 &&\n this.#pendingInstanceWrite === undefined &&\n this.#pendingQueryUpdates.size === 0 &&\n this.#pendingQueryPartialUpdates.size === 0 &&\n this.#pendingDesireUpdates.size === 0\n ) {\n return null;\n }\n // Note: The CVR instance itself is only updated if there are material\n // changes (i.e. changes to the CVR contents) to flush.\n this.putInstance(cvr);\n const start = Date.now();\n lc.debug?.('flush tx beginning');\n\n // Use an async callback so we can await the version/ownership check and\n // validate it INSIDE the transaction. If validation fails, the exception\n // causes postgres.js to ROLLBACK, ensuring no writes are committed on error.\n const results = await runTx(\n this.#db,\n async tx => {\n lc.debug?.(`flush tx begun after ${Date.now() - start} ms`);\n\n // Acquire row-level lock and validate version/ownership before queuing writes.\n // Throwing here (inside the begin callback) rolls back the transaction so that\n // no writes are committed when concurrent modification or ownership errors occur.\n await this.#checkVersionAndOwnership(\n lc,\n tx,\n expectedCurrentVersion,\n lastConnectTime,\n );\n\n const writeQueries = [];\n if (this.#pendingInstanceWrite) {\n writeQueries.push(this.#pendingInstanceWrite(tx, lastConnectTime));\n stats.instances++;\n stats.statements++;\n }\n for (const write of this.#writes) {\n stats.clients += write.stats.clients ?? 0;\n stats.rows += write.stats.rows ?? 0;\n\n writeQueries.push(write.write(tx, lastConnectTime));\n stats.statements++;\n }\n\n // Batch flush config writes\n // Flush queries first (desires depend on queries via foreign key)\n const hasQueryUpdates =\n this.#pendingQueryUpdates.size > 0 ||\n this.#pendingQueryPartialUpdates.size > 0;\n\n const desireFlush = this.#flushDesires(tx, lc);\n\n let queryFlushes: PendingQuery<Row[]>[] = [];\n if (hasQueryUpdates) {\n queryFlushes = this.#flushQueries(tx, lc);\n\n // Count both full updates and partial-only updates\n const partialOnlyCount = [\n ...this.#pendingQueryPartialUpdates.keys(),\n ].filter(key => !this.#pendingQueryUpdates.has(key)).length;\n\n stats.queries = this.#pendingQueryUpdates.size + partialOnlyCount;\n stats.statements +=\n (this.#pendingQueryUpdates.size > 0 ? 1 : 0) +\n (partialOnlyCount > 0 ? 1 : 0);\n\n if (desireFlush) {\n stats.desires = this.#pendingDesireUpdates.size;\n stats.statements++;\n }\n } else if (desireFlush) {\n stats.desires = this.#pendingDesireUpdates.size;\n stats.statements++;\n }\n\n const rowUpdates = this.#rowCache.executeRowUpdates(\n tx,\n cvr.version,\n this.#pendingRowRecordUpdates,\n 'allow-defer',\n lc,\n );\n stats.statements += rowUpdates.length;\n\n // Pipeline writes now that the version check has passed.\n const pipelined = [\n ...writeQueries,\n ...queryFlushes,\n ...(desireFlush ? [desireFlush] : []),\n ...rowUpdates,\n ];\n\n lc.debug?.(`returning ${pipelined.length} queries for pipelining`);\n\n // Explicitly await all pipelined queries. When the begin callback is async,\n // postgres.js does not call Promise.all() on the return value the way it does\n // for sync callbacks, so we must do it ourselves.\n return Promise.all(pipelined);\n },\n {mode: Mode.READ_COMMITTED},\n );\n\n lc.debug?.(`flush tx completed after ${Date.now() - start} ms`);\n\n // Calculate how many row update queries were in the pipeline.\n // Note: the version check was awaited separately and is not in the results array.\n const baseQueries =\n (this.#pendingInstanceWrite ? 1 : 0) +\n this.#writes.size +\n (this.#pendingQueryUpdates.size > 0 ? 1 : 0) +\n ([...this.#pendingQueryPartialUpdates.keys()].filter(\n key => !this.#pendingQueryUpdates.has(key),\n ).length > 0\n ? 1\n : 0) +\n (this.#pendingDesireUpdates.size > 0 ? 1 : 0);\n const rowUpdateCount = results.length - baseQueries;\n\n const rowsFlushed = rowUpdateCount > 0;\n if (!rowsFlushed) {\n stats.rowsDeferred = this.#pendingRowRecordUpdates.size;\n } else {\n stats.rows += this.#pendingRowRecordUpdates.size;\n }\n\n this.#rowCount = await this.#rowCache.apply(\n this.#pendingRowRecordUpdates,\n cvr.version,\n rowsFlushed,\n );\n recordRowsSynced(this.#rowCount);\n\n return stats;\n }\n\n get rowCount(): number {\n return this.#rowCount;\n }\n\n async flush(\n lc: LogContext,\n expectedCurrentVersion: CVRVersion,\n cvr: CVRSnapshot,\n lastConnectTime: number,\n ): Promise<CVRFlushStats | null> {\n const start = performance.now();\n lc = lc.withContext('cvrFlushID', flushCounter++);\n try {\n const stats = await this.#flush(\n lc,\n expectedCurrentVersion,\n cvr,\n lastConnectTime,\n );\n if (stats) {\n const elapsed = performance.now() - start;\n lc.info?.(\n `flushed cvr@${versionString(cvr.version)} ` +\n `${JSON.stringify(stats)} in (${elapsed} ms)`,\n );\n this.#rowCache.recordSyncFlushStats(stats, elapsed);\n }\n return stats;\n } catch (e) {\n // Clear cached state if an error (e.g. ConcurrentModificationException) is encountered.\n this.#rowCache.clear();\n throw e;\n } finally {\n this.#writes.clear();\n this.#pendingInstanceWrite = undefined;\n this.#pendingRowRecordUpdates.clear();\n this.#forceUpdates.clear();\n this.#pendingQueryUpdates.clear();\n this.#pendingDesireUpdates.clear();\n this.#pendingQueryPartialUpdates.clear();\n }\n }\n\n hasPendingUpdates(): boolean {\n return this.#rowCache.hasPendingUpdates();\n }\n\n /** Resolves when all pending updates are flushed. */\n flushed(lc: LogContext): Promise<void> {\n return this.#rowCache.flushed(lc);\n }\n\n async inspectQueries(\n lc: LogContext,\n ttlClock: TTLClock,\n clientID?: string,\n ): Promise<InspectQueryRow[]> {\n const db = this.#db;\n const clientGroupID = this.#id;\n\n const reader = new TransactionPool(lc, {mode: Mode.READONLY}).run(db);\n try {\n return await reader.processReadTask(\n tx => tx<InspectQueryRow[]>`\n SELECT DISTINCT ON (d.\"clientID\", d.\"queryHash\")\n d.\"clientID\",\n d.\"queryHash\" AS \"queryID\",\n COALESCE(d.\"ttlMs\", ${DEFAULT_TTL_MS}) AS \"ttl\",\n d.\"inactivatedAtMs\" AS \"inactivatedAt\",\n (SELECT COUNT(*)::INT FROM ${this.#cvr('rows')} r \n WHERE r.\"clientGroupID\" = d.\"clientGroupID\" \n AND r.\"refCounts\" ? d.\"queryHash\") AS \"rowCount\",\n q.\"clientAST\" AS \"ast\",\n (q.\"patchVersion\" IS NOT NULL) AS \"got\",\n COALESCE(d.\"deleted\", FALSE) AS \"deleted\",\n q.\"queryName\" AS \"name\",\n q.\"queryArgs\" AS \"args\"\n FROM ${this.#cvr('desires')} d\n LEFT JOIN ${this.#cvr('queries')} q\n ON q.\"clientGroupID\" = d.\"clientGroupID\"\n AND q.\"queryHash\" = d.\"queryHash\"\n WHERE d.\"clientGroupID\" = ${clientGroupID}\n ${clientID ? tx`AND d.\"clientID\" = ${clientID}` : tx``}\n AND NOT (\n d.\"inactivatedAtMs\" IS NOT NULL \n AND d.\"ttlMs\" IS NOT NULL \n AND (d.\"inactivatedAtMs\" + d.\"ttlMs\") <= ${ttlClockAsNumber(ttlClock)}\n )\n ORDER BY d.\"clientID\", d.\"queryHash\"`,\n );\n } finally {\n reader.setDone();\n }\n }\n}\n\n/**\n * This is similar to {@link CVRStore.#checkVersionAndOwnership} except\n * that it only checks the version and is suitable for snapshot reads\n * (i.e. by doing a plain `SELECT` rather than a `SELECT ... FOR UPDATE`).\n */\nexport async function checkVersion(\n tx: PostgresTransaction,\n schema: string,\n clientGroupID: string,\n expectedCurrentVersion: CVRVersion,\n): Promise<void> {\n const expected = versionString(expectedCurrentVersion);\n const result = await tx<Pick<InstancesRow, 'version'>[]>`\n SELECT version FROM ${tx(schema)}.instances \n WHERE \"clientGroupID\" = ${clientGroupID}`;\n const {version} =\n result.length > 0 ? result[0] : {version: EMPTY_CVR_VERSION.stateVersion};\n if (version !== expected) {\n throw new ConcurrentModificationException(expected, version);\n }\n}\n\nexport class ClientNotFoundError extends ProtocolErrorWithLevel {\n constructor(message: string) {\n super(\n {\n kind: ErrorKind.ClientNotFound,\n message,\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n}\n\nexport class ConcurrentModificationException extends ProtocolErrorWithLevel {\n readonly name = 'ConcurrentModificationException';\n\n constructor(expectedVersion: string, actualVersion: string) {\n super(\n {\n kind: ErrorKind.Internal,\n message: `CVR has been concurrently modified. Expected ${expectedVersion}, got ${actualVersion}`,\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n}\n\nexport class OwnershipError extends ProtocolErrorWithLevel {\n readonly name = 'OwnershipError';\n\n constructor(\n owner: string | null,\n grantedAt: number | null,\n lastConnectTime: number,\n ) {\n super(\n {\n kind: ErrorKind.Rehome,\n message:\n `CVR ownership was transferred to ${owner} at ` +\n `${new Date(grantedAt ?? 0).toISOString()} ` +\n `(last connect time: ${new Date(lastConnectTime).toISOString()})`,\n maxBackoffMs: 0,\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n );\n }\n}\n\nexport class InvalidClientSchemaError extends ProtocolErrorWithLevel {\n readonly name = 'InvalidClientSchemaError';\n\n constructor(cause: unknown) {\n super(\n {\n kind: ErrorKind.SchemaVersionNotSupported,\n message: `Could not parse clientSchema stored in CVR: ${String(cause)}`,\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n {cause},\n );\n }\n}\n\nexport class RowsVersionBehindError extends Error {\n readonly name = 'RowsVersionBehindError';\n readonly cvrVersion: string;\n readonly rowsVersion: string | null;\n\n constructor(cvrVersion: string, rowsVersion: string | null) {\n super(`rowsVersion (${rowsVersion}) is behind CVR ${cvrVersion}`);\n this.cvrVersion = cvrVersion;\n this.rowsVersion = rowsVersion;\n }\n}\n"],"mappings":"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"}
|
|
@@ -2,8 +2,8 @@ import { unreachable } from "../../../../shared/src/asserts.js";
|
|
|
2
2
|
import { must } from "../../../../shared/src/must.js";
|
|
3
3
|
import { Database } from "../../../../zqlite/src/db.js";
|
|
4
4
|
import { getServerVersion, isAdminPasswordValid } from "../../config/zero-config.js";
|
|
5
|
-
import { StatementRunner } from "../../db/statements.js";
|
|
6
5
|
import { loadPermissions } from "../../auth/load-permissions.js";
|
|
6
|
+
import { StatementRunner } from "../../db/statements.js";
|
|
7
7
|
import { _usingCtx } from "../../../../_virtual/_@oxc-project_runtime@0.122.0/helpers/usingCtx.js";
|
|
8
8
|
import { analyzeQuery } from "../analyze.js";
|
|
9
9
|
//#region ../zero-cache/src/services/view-syncer/inspect-handler.ts
|
|
@@ -3,6 +3,7 @@ import type { AST } from '../../../../zero-protocol/src/ast.ts';
|
|
|
3
3
|
import type { ClientSchema } from '../../../../zero-protocol/src/client-schema.ts';
|
|
4
4
|
import type { Row } from '../../../../zero-protocol/src/data.ts';
|
|
5
5
|
import type { PrimaryKey } from '../../../../zero-protocol/src/primary-key.ts';
|
|
6
|
+
import { ChangeType } from '../../../../zql/src/ivm/change-type.ts';
|
|
6
7
|
import { type Input } from '../../../../zql/src/ivm/operator.ts';
|
|
7
8
|
import type { ClientGroupStorage } from '../../../../zqlite/src/database-storage.ts';
|
|
8
9
|
import { type LoadedPermissions } from '../../auth/load-permissions.ts';
|
|
@@ -12,27 +13,16 @@ import type { InspectorDelegate } from '../../server/inspector-delegate.ts';
|
|
|
12
13
|
import { type RowKey } from '../../types/row-key.ts';
|
|
13
14
|
import { type ShardID } from '../../types/shards.ts';
|
|
14
15
|
import type { Snapshotter } from './snapshotter.ts';
|
|
15
|
-
|
|
16
|
-
readonly type:
|
|
17
|
-
readonly queryID: string;
|
|
18
|
-
readonly table: string;
|
|
19
|
-
readonly rowKey: Row;
|
|
20
|
-
readonly row: Row;
|
|
21
|
-
};
|
|
22
|
-
export type RowRemove = {
|
|
23
|
-
readonly type: 'remove';
|
|
24
|
-
readonly queryID: string;
|
|
25
|
-
readonly table: string;
|
|
26
|
-
readonly rowKey: Row;
|
|
27
|
-
readonly row: undefined;
|
|
28
|
-
};
|
|
29
|
-
export type RowEdit = {
|
|
30
|
-
readonly type: 'edit';
|
|
16
|
+
type RowOp<Op extends Omit<ChangeType, ChangeType.CHILD>> = {
|
|
17
|
+
readonly type: Op;
|
|
31
18
|
readonly queryID: string;
|
|
32
19
|
readonly table: string;
|
|
33
20
|
readonly rowKey: Row;
|
|
34
21
|
readonly row: Row;
|
|
35
22
|
};
|
|
23
|
+
export type RowAdd = RowOp<ChangeType.ADD>;
|
|
24
|
+
export type RowRemove = RowOp<ChangeType.REMOVE>;
|
|
25
|
+
export type RowEdit = RowOp<ChangeType.EDIT>;
|
|
36
26
|
export type RowChange = RowAdd | RowRemove | RowEdit;
|
|
37
27
|
type QueryInfo = {
|
|
38
28
|
readonly transformedAst: AST;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline-driver.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/pipeline-driver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAIjD,OAAO,KAAK,EAAC,GAAG,EAAe,MAAM,sCAAsC,CAAC;AAC5E,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gDAAgD,CAAC;AACjF,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,uCAAuC,CAAC;AAC/D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,8CAA8C,CAAC;
|
|
1
|
+
{"version":3,"file":"pipeline-driver.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/pipeline-driver.ts"],"names":[],"mappings":"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"}
|