@rocicorp/zero 1.11.0-canary.32 → 1.11.0-canary.34
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/analyze-cli.js +2 -2
- package/out/analyze-query/src/analyze-cli.js.map +1 -1
- package/out/zero/package.js +2 -2
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +6 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +14 -1
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/db/migration-lite.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +1 -5
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/slot-keeper.d.ts +1 -1
- package/out/zero-cache/src/server/slot-keeper.d.ts.map +1 -1
- package/out/zero-cache/src/server/slot-keeper.js +8 -2
- package/out/zero-cache/src/server/slot-keeper.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts +3 -13
- package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-restore.js +4 -10
- package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js +4 -5
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +2 -2
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts +4 -11
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +4 -9
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +0 -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 +0 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts +0 -9
- package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js +1 -15
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +2 -2
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/workers/connect-params.d.ts +0 -7
- package/out/zero-cache/src/workers/connect-params.d.ts.map +1 -1
- package/out/zero-cache/src/workers/connect-params.js +1 -24
- package/out/zero-cache/src/workers/connect-params.js.map +1 -1
- package/out/zero-cache/src/workers/connection.d.ts +4 -4
- package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
- package/out/zero-cache/src/workers/connection.js +11 -11
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/workers/replicator.js +10 -1
- package/out/zero-cache/src/workers/replicator.js.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/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +3 -4
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-protocol/src/analyze-query-result.d.ts +0 -5
- package/out/zero-protocol/src/analyze-query-result.d.ts.map +1 -1
- package/out/zero-protocol/src/analyze-query-result.js.map +1 -1
- package/out/zero-protocol/src/connect.d.ts +0 -13
- package/out/zero-protocol/src/connect.d.ts.map +1 -1
- package/out/zero-protocol/src/connect.js +1 -30
- package/out/zero-protocol/src/connect.js.map +1 -1
- package/out/zero-protocol/src/down.d.ts +2 -2
- package/out/zero-protocol/src/poke.d.ts +10 -17
- package/out/zero-protocol/src/poke.d.ts.map +1 -1
- package/out/zero-protocol/src/poke.js +2 -3
- package/out/zero-protocol/src/poke.js.map +1 -1
- package/out/zero-protocol/src/protocol-version.d.ts +6 -30
- package/out/zero-protocol/src/protocol-version.d.ts.map +1 -1
- package/out/zero-protocol/src/protocol-version.js +1 -1
- package/out/zero-protocol/src/protocol-version.js.map +1 -1
- package/out/zql/src/query/expression.d.ts +2 -2
- package/out/zql/src/query/expression.d.ts.map +1 -1
- package/out/zql/src/query/expression.js.map +1 -1
- package/out/zql/src/query/query.d.ts +5 -6
- package/out/zql/src/query/query.d.ts.map +1 -1
- package/out/zqlite/src/db.d.ts +8 -0
- package/out/zqlite/src/db.d.ts.map +1 -1
- package/out/zqlite/src/db.js +11 -0
- package/out/zqlite/src/db.js.map +1 -1
- package/package.json +2 -2
- package/out/shared/src/base64.d.ts +0 -19
- package/out/shared/src/base64.d.ts.map +0 -1
- package/out/shared/src/base64.js +0 -40
- package/out/shared/src/base64.js.map +0 -1
- package/out/zero-cache/src/services/litestream/wal-keeper.d.ts +0 -33
- package/out/zero-cache/src/services/litestream/wal-keeper.d.ts.map +0 -1
- package/out/zero-cache/src/services/litestream/wal-keeper.js +0 -49
- package/out/zero-cache/src/services/litestream/wal-keeper.js.map +0 -1
- package/out/zero-protocol/src/protocol-flag-enum.d.ts +0 -14
- package/out/zero-protocol/src/protocol-flag-enum.d.ts.map +0 -1
- package/out/zero-protocol/src/protocol-flag-enum.js +0 -10
- package/out/zero-protocol/src/protocol-flag-enum.js.map +0 -1
- package/out/zero-protocol/src/protocol-flag.d.ts +0 -14
- package/out/zero-protocol/src/protocol-flag.d.ts.map +0 -1
- package/out/zero-protocol/src/protocol-flag.js +0 -8
- package/out/zero-protocol/src/protocol-flag.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection-context-manager.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/connection-context-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,0CAA0C,CAAC;AAGjF,OAAO,KAAK,EAAC,
|
|
1
|
+
{"version":3,"file":"connection-context-manager.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/connection-context-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -70,7 +70,6 @@ var ConnectionContextManagerImpl = class {
|
|
|
70
70
|
profileID: connectParams.profileID,
|
|
71
71
|
baseCookie: connectParams.baseCookie,
|
|
72
72
|
protocolVersion: connectParams.protocolVersion,
|
|
73
|
-
protocolFlags: connectParams.protocolFlags,
|
|
74
73
|
revalidateAt: void 0,
|
|
75
74
|
queryContext: getContext("query"),
|
|
76
75
|
mutateContext: getContext("mutate"),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection-context-manager.js","names":["#lc","#connections","#validateLegacyJWT","#now","#revalidateIntervalMs","#retransformIntervalMs","#queryConfig","#pushConfig","#removeConnection","#nextInsertionOrder","#storeConnection","#refreshBackgroundConnectionContext","#updateBackgroundRetransformDeadline","#mustGetConnectionContext","#demoteConnection","#getConnectionContext","#group","#setGroup","#nextRevalidateAt","#getBackgroundConnectionContext","#sharedRetransformReady","#setBackgroundConnection"],"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 {ProtocolFlags} from '../../../../zero-protocol/src/protocol-flag.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 * Normalized user identity shared by live connection state and group auth state.\n * `id: null` means logged out.\n */\nexport type UserState = {readonly id: string | null};\n\n/**\n * Delineates the two paths for validating a connection: either server can validate\n * the user's identity and return a definitive userID to trust, or we fall back to\n * trusting the one provided by the client in the incoming query params.\n */\nexport type ConnectionValidation =\n | {kind: 'client-fallback'}\n | {kind: 'server-validated'; validatedUserID: string | null};\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 readonly apiKey?: string | undefined;\n /** Allowlisted headers provided in the client options. */\n readonly customHeaders?: Readonly<Record<string, string>> | undefined;\n /** Allowlisted headers from the incoming HTTP request. */\n readonly requestHeaders?: Readonly<Record<string, string>> | undefined;\n readonly cookie?: string | undefined;\n readonly origin?: string | undefined;\n};\n\nexport type ConnectionFetchContext = {\n readonly url: string | undefined;\n readonly allowedUrlPatterns: readonly URLPattern[] | undefined;\n readonly 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 readonly state: ConnectionState;\n\n readonly clientID: string;\n readonly wsID: string;\n readonly user: UserState;\n\n readonly auth: Auth | undefined;\n\n readonly profileID: string | null;\n readonly baseCookie: string | null;\n readonly protocolVersion: number;\n readonly protocolFlags: ProtocolFlags;\n\n readonly revision: number;\n\n readonly revalidateAt: number | undefined;\n\n readonly insertionOrder: number;\n\n readonly queryContext: ConnectionFetchContext;\n readonly mutateContext: 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 */\nexport type GroupAuthState = {\n readonly pinnedUser: UserState | undefined;\n\n readonly backgroundConnection: ConnectionSelector | undefined;\n readonly retransformAt: number | undefined;\n // Defer all maintenance in case a transient failure occurs.\n readonly 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 validation: ConnectionValidation,\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 setSharedRetransformReady(ready: boolean): 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 * effective `userID` is confirmed as valid. The manager also tracks which\n * validated connection currently serves as the group's background connection.\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 #group: GroupAuthState = {\n pinnedUser: undefined,\n backgroundConnection: undefined,\n retransformAt: undefined,\n maintenanceNotBeforeAt: undefined,\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 #sharedRetransformReady = false;\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 getContext = (type: 'query' | 'mutate'): ConnectionFetchContext => {\n const config = type === 'query' ? this.#queryConfig : this.#pushConfig;\n\n return {\n url: config?.url?.[0],\n allowedUrlPatterns: config?.url?.map(compileUrlPattern),\n headerOptions: {\n customHeaders: undefined,\n requestHeaders: filterHeaders(\n connectParams.requestHeaders,\n config?.allowedRequestHeaders,\n ),\n origin: connectParams.origin,\n apiKey: config?.apiKey,\n cookie: config?.forwardCookies ? connectParams.httpCookie : undefined,\n },\n };\n };\n\n const connection: ConnectionContext = {\n state: 'provisional',\n\n clientID: connectParams.clientID,\n wsID: connectParams.wsID,\n revision: 0,\n user: {id: connectParams.userID ?? null},\n auth,\n\n profileID: connectParams.profileID,\n baseCookie: connectParams.baseCookie,\n protocolVersion: connectParams.protocolVersion,\n protocolFlags: connectParams.protocolFlags,\n\n revalidateAt: undefined,\n\n queryContext: getContext('query'),\n mutateContext: getContext('mutate'),\n\n insertionOrder: ++this.#nextInsertionOrder,\n };\n this.#storeConnection(connection);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return 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 let queryContext = connection.queryContext;\n let mutateContext = connection.mutateContext;\n\n if (body.userQueryURL) {\n queryContext = {\n ...queryContext,\n url: body.userQueryURL,\n };\n }\n if (body.userQueryHeaders) {\n queryContext = {\n ...queryContext,\n headerOptions: {\n ...queryContext.headerOptions,\n customHeaders: filterHeaders(\n body.userQueryHeaders,\n this.#queryConfig?.allowedClientHeaders,\n ),\n },\n };\n }\n if (body.userPushURL) {\n mutateContext = {\n ...mutateContext,\n url: body.userPushURL,\n };\n }\n if (body.userPushHeaders) {\n mutateContext = {\n ...mutateContext,\n headerOptions: {\n ...mutateContext.headerOptions,\n customHeaders: filterHeaders(\n body.userPushHeaders,\n this.#pushConfig?.allowedClientHeaders,\n ),\n },\n };\n }\n\n return this.#demoteConnection({\n ...connection,\n revision: connection.revision + 1,\n queryContext,\n mutateContext,\n });\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.user.id,\n body.auth,\n this.#validateLegacyJWT,\n );\n\n const authChanged = !authEquals(connection.auth, nextAuth);\n if (authChanged) {\n return this.#demoteConnection({\n ...connection,\n auth: nextAuth,\n revision: connection.revision + 1,\n });\n }\n\n if (nextAuth === connection.auth) {\n return connection;\n }\n\n return this.#storeConnection({\n ...connection,\n auth: nextAuth,\n });\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 validation: ConnectionValidation,\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 let validatedUserState: UserState | undefined;\n\n // If the API server has validated the user's identity, we ensure that\n // the connection's claimed userID matches it.\n if (validation.kind === 'server-validated') {\n validatedUserState = {id: validation.validatedUserID};\n\n // Check that the ws connection userID provided by the client\n // matches the validated userID from the API server.\n if (connection.user.id !== validatedUserState.id) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Unauthorized,\n message:\n 'Connection userID does not match validated server userID.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n }\n\n // The incoming user state is either the validated user state from the server\n // or the WS client's claimed user state if no server validation occurred.\n const incomingUserState = validatedUserState ?? connection.user;\n\n // Once a client group is validated, every later validated connection must\n // agree with that pinned identity.\n if (\n this.#group.pinnedUser !== undefined &&\n this.#group.pinnedUser.id !== incomingUserState.id\n ) {\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.pinnedUser === undefined) {\n this.#setGroup({\n ...this.#group,\n pinnedUser: incomingUserState,\n });\n }\n\n const validatedConnection = this.#storeConnection({\n ...connection,\n state: 'validated',\n revalidateAt: this.#nextRevalidateAt(),\n });\n this.#refreshBackgroundConnectionContext(validatedConnection);\n this.#updateBackgroundRetransformDeadline(false);\n\n return {\n connection: validatedConnection,\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 backgroundConnection.clientID !== selector.clientID ||\n backgroundConnection.wsID !== selector.wsID ||\n backgroundConnection.revision !== revision\n ) {\n return;\n }\n this.#updateBackgroundRetransformDeadline(true);\n }\n\n setSharedRetransformReady(ready: boolean): void {\n if (this.#sharedRetransformReady === ready) {\n return;\n }\n this.#sharedRetransformReady = ready;\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.#setGroup({\n ...this.#group,\n maintenanceNotBeforeAt: Math.max(\n this.#group.maintenanceNotBeforeAt ?? 0,\n this.#now() + intervalMs,\n ),\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 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 this.#mustGetConnectionContext(selector);\n }\n\n /** Returns the current background connection, if one exists. */\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined {\n return 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 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(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 this.#connections.delete(connection.clientID);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n\n return connection;\n }\n\n #demoteConnection(connection: ConnectionContext): ConnectionContext {\n const demotedConnection = this.#storeConnection({\n ...connection,\n state: 'provisional',\n revalidateAt: undefined,\n });\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return demotedConnection;\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.#setBackgroundConnection({\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.#setBackgroundConnection(\n nextBackgroundConnection\n ? {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n }\n : undefined,\n );\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 #storeConnection(connection: ConnectionContext): ConnectionContext {\n this.#connections.set(connection.clientID, connection);\n return connection;\n }\n\n #setGroup(group: GroupAuthState): GroupAuthState {\n this.#group = group;\n return group;\n }\n\n #setBackgroundConnection(\n backgroundConnection: ConnectionSelector | undefined,\n ) {\n if (\n sameConnectionSelector(\n this.#group.backgroundConnection,\n backgroundConnection,\n )\n ) {\n return;\n }\n this.#setGroup({\n ...this.#group,\n backgroundConnection: backgroundConnection\n ? {...backgroundConnection}\n : undefined,\n });\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 for the current ready ViewSyncer instance, or\n * clears the deadline if it is not.\n */\n #updateBackgroundRetransformDeadline(reset: boolean) {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (\n !backgroundConnection ||\n this.#retransformIntervalMs === undefined ||\n !this.#sharedRetransformReady\n ) {\n if (this.#group.retransformAt !== undefined) {\n this.#setGroup({\n ...this.#group,\n retransformAt: undefined,\n });\n }\n return;\n }\n\n if (reset || this.#group.retransformAt === undefined) {\n this.#setGroup({\n ...this.#group,\n retransformAt: this.#now() + this.#retransformIntervalMs,\n });\n }\n }\n\n #nextRevalidateAt() {\n return this.#revalidateIntervalMs === undefined\n ? undefined\n : this.#now() + this.#revalidateIntervalMs;\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\nfunction sameConnectionSelector(\n a: ConnectionSelector | undefined,\n b: ConnectionSelector | undefined,\n) {\n return a?.clientID === b?.clientID && a?.wsID === b?.wsID;\n}\n\nfunction filterHeaders(\n headers: Readonly<Record<string, string>> | undefined,\n allowedHeaders: readonly string[] | undefined,\n) {\n if (!headers || !allowedHeaders || allowedHeaders.length === 0) {\n return undefined;\n }\n\n const allowed = new Set(allowedHeaders.map(header => header.toLowerCase()));\n let filtered: Record<string, string> | undefined;\n for (const [key, value] of Object.entries(headers)) {\n if (allowed.has(key.toLowerCase())) {\n filtered ??= {};\n filtered[key] = value;\n }\n }\n return filtered;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"connection-context-manager.js","names":["#lc","#connections","#validateLegacyJWT","#now","#revalidateIntervalMs","#retransformIntervalMs","#queryConfig","#pushConfig","#removeConnection","#nextInsertionOrder","#storeConnection","#refreshBackgroundConnectionContext","#updateBackgroundRetransformDeadline","#mustGetConnectionContext","#demoteConnection","#getConnectionContext","#group","#setGroup","#nextRevalidateAt","#getBackgroundConnectionContext","#sharedRetransformReady","#setBackgroundConnection"],"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 * Normalized user identity shared by live connection state and group auth state.\n * `id: null` means logged out.\n */\nexport type UserState = {readonly id: string | null};\n\n/**\n * Delineates the two paths for validating a connection: either server can validate\n * the user's identity and return a definitive userID to trust, or we fall back to\n * trusting the one provided by the client in the incoming query params.\n */\nexport type ConnectionValidation =\n | {kind: 'client-fallback'}\n | {kind: 'server-validated'; validatedUserID: string | null};\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 readonly apiKey?: string | undefined;\n /** Allowlisted headers provided in the client options. */\n readonly customHeaders?: Readonly<Record<string, string>> | undefined;\n /** Allowlisted headers from the incoming HTTP request. */\n readonly requestHeaders?: Readonly<Record<string, string>> | undefined;\n readonly cookie?: string | undefined;\n readonly origin?: string | undefined;\n};\n\nexport type ConnectionFetchContext = {\n readonly url: string | undefined;\n readonly allowedUrlPatterns: readonly URLPattern[] | undefined;\n readonly 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 readonly state: ConnectionState;\n\n readonly clientID: string;\n readonly wsID: string;\n readonly user: UserState;\n\n readonly auth: Auth | undefined;\n\n readonly profileID: string | null;\n readonly baseCookie: string | null;\n readonly protocolVersion: number;\n\n readonly revision: number;\n\n readonly revalidateAt: number | undefined;\n\n readonly insertionOrder: number;\n\n readonly queryContext: ConnectionFetchContext;\n readonly mutateContext: 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 */\nexport type GroupAuthState = {\n readonly pinnedUser: UserState | undefined;\n\n readonly backgroundConnection: ConnectionSelector | undefined;\n readonly retransformAt: number | undefined;\n // Defer all maintenance in case a transient failure occurs.\n readonly 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 validation: ConnectionValidation,\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 setSharedRetransformReady(ready: boolean): 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 * effective `userID` is confirmed as valid. The manager also tracks which\n * validated connection currently serves as the group's background connection.\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 #group: GroupAuthState = {\n pinnedUser: undefined,\n backgroundConnection: undefined,\n retransformAt: undefined,\n maintenanceNotBeforeAt: undefined,\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 #sharedRetransformReady = false;\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 getContext = (type: 'query' | 'mutate'): ConnectionFetchContext => {\n const config = type === 'query' ? this.#queryConfig : this.#pushConfig;\n\n return {\n url: config?.url?.[0],\n allowedUrlPatterns: config?.url?.map(compileUrlPattern),\n headerOptions: {\n customHeaders: undefined,\n requestHeaders: filterHeaders(\n connectParams.requestHeaders,\n config?.allowedRequestHeaders,\n ),\n origin: connectParams.origin,\n apiKey: config?.apiKey,\n cookie: config?.forwardCookies ? connectParams.httpCookie : undefined,\n },\n };\n };\n\n const connection: ConnectionContext = {\n state: 'provisional',\n\n clientID: connectParams.clientID,\n wsID: connectParams.wsID,\n revision: 0,\n user: {id: connectParams.userID ?? null},\n auth,\n\n profileID: connectParams.profileID,\n baseCookie: connectParams.baseCookie,\n protocolVersion: connectParams.protocolVersion,\n\n revalidateAt: undefined,\n\n queryContext: getContext('query'),\n mutateContext: getContext('mutate'),\n\n insertionOrder: ++this.#nextInsertionOrder,\n };\n this.#storeConnection(connection);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return 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 let queryContext = connection.queryContext;\n let mutateContext = connection.mutateContext;\n\n if (body.userQueryURL) {\n queryContext = {\n ...queryContext,\n url: body.userQueryURL,\n };\n }\n if (body.userQueryHeaders) {\n queryContext = {\n ...queryContext,\n headerOptions: {\n ...queryContext.headerOptions,\n customHeaders: filterHeaders(\n body.userQueryHeaders,\n this.#queryConfig?.allowedClientHeaders,\n ),\n },\n };\n }\n if (body.userPushURL) {\n mutateContext = {\n ...mutateContext,\n url: body.userPushURL,\n };\n }\n if (body.userPushHeaders) {\n mutateContext = {\n ...mutateContext,\n headerOptions: {\n ...mutateContext.headerOptions,\n customHeaders: filterHeaders(\n body.userPushHeaders,\n this.#pushConfig?.allowedClientHeaders,\n ),\n },\n };\n }\n\n return this.#demoteConnection({\n ...connection,\n revision: connection.revision + 1,\n queryContext,\n mutateContext,\n });\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.user.id,\n body.auth,\n this.#validateLegacyJWT,\n );\n\n const authChanged = !authEquals(connection.auth, nextAuth);\n if (authChanged) {\n return this.#demoteConnection({\n ...connection,\n auth: nextAuth,\n revision: connection.revision + 1,\n });\n }\n\n if (nextAuth === connection.auth) {\n return connection;\n }\n\n return this.#storeConnection({\n ...connection,\n auth: nextAuth,\n });\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 validation: ConnectionValidation,\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 let validatedUserState: UserState | undefined;\n\n // If the API server has validated the user's identity, we ensure that\n // the connection's claimed userID matches it.\n if (validation.kind === 'server-validated') {\n validatedUserState = {id: validation.validatedUserID};\n\n // Check that the ws connection userID provided by the client\n // matches the validated userID from the API server.\n if (connection.user.id !== validatedUserState.id) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Unauthorized,\n message:\n 'Connection userID does not match validated server userID.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n }\n\n // The incoming user state is either the validated user state from the server\n // or the WS client's claimed user state if no server validation occurred.\n const incomingUserState = validatedUserState ?? connection.user;\n\n // Once a client group is validated, every later validated connection must\n // agree with that pinned identity.\n if (\n this.#group.pinnedUser !== undefined &&\n this.#group.pinnedUser.id !== incomingUserState.id\n ) {\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.pinnedUser === undefined) {\n this.#setGroup({\n ...this.#group,\n pinnedUser: incomingUserState,\n });\n }\n\n const validatedConnection = this.#storeConnection({\n ...connection,\n state: 'validated',\n revalidateAt: this.#nextRevalidateAt(),\n });\n this.#refreshBackgroundConnectionContext(validatedConnection);\n this.#updateBackgroundRetransformDeadline(false);\n\n return {\n connection: validatedConnection,\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 backgroundConnection.clientID !== selector.clientID ||\n backgroundConnection.wsID !== selector.wsID ||\n backgroundConnection.revision !== revision\n ) {\n return;\n }\n this.#updateBackgroundRetransformDeadline(true);\n }\n\n setSharedRetransformReady(ready: boolean): void {\n if (this.#sharedRetransformReady === ready) {\n return;\n }\n this.#sharedRetransformReady = ready;\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.#setGroup({\n ...this.#group,\n maintenanceNotBeforeAt: Math.max(\n this.#group.maintenanceNotBeforeAt ?? 0,\n this.#now() + intervalMs,\n ),\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 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 this.#mustGetConnectionContext(selector);\n }\n\n /** Returns the current background connection, if one exists. */\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined {\n return 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 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(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 this.#connections.delete(connection.clientID);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n\n return connection;\n }\n\n #demoteConnection(connection: ConnectionContext): ConnectionContext {\n const demotedConnection = this.#storeConnection({\n ...connection,\n state: 'provisional',\n revalidateAt: undefined,\n });\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return demotedConnection;\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.#setBackgroundConnection({\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.#setBackgroundConnection(\n nextBackgroundConnection\n ? {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n }\n : undefined,\n );\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 #storeConnection(connection: ConnectionContext): ConnectionContext {\n this.#connections.set(connection.clientID, connection);\n return connection;\n }\n\n #setGroup(group: GroupAuthState): GroupAuthState {\n this.#group = group;\n return group;\n }\n\n #setBackgroundConnection(\n backgroundConnection: ConnectionSelector | undefined,\n ) {\n if (\n sameConnectionSelector(\n this.#group.backgroundConnection,\n backgroundConnection,\n )\n ) {\n return;\n }\n this.#setGroup({\n ...this.#group,\n backgroundConnection: backgroundConnection\n ? {...backgroundConnection}\n : undefined,\n });\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 for the current ready ViewSyncer instance, or\n * clears the deadline if it is not.\n */\n #updateBackgroundRetransformDeadline(reset: boolean) {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (\n !backgroundConnection ||\n this.#retransformIntervalMs === undefined ||\n !this.#sharedRetransformReady\n ) {\n if (this.#group.retransformAt !== undefined) {\n this.#setGroup({\n ...this.#group,\n retransformAt: undefined,\n });\n }\n return;\n }\n\n if (reset || this.#group.retransformAt === undefined) {\n this.#setGroup({\n ...this.#group,\n retransformAt: this.#now() + this.#retransformIntervalMs,\n });\n }\n }\n\n #nextRevalidateAt() {\n return this.#revalidateIntervalMs === undefined\n ? undefined\n : this.#now() + this.#revalidateIntervalMs;\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\nfunction sameConnectionSelector(\n a: ConnectionSelector | undefined,\n b: ConnectionSelector | undefined,\n) {\n return a?.clientID === b?.clientID && a?.wsID === b?.wsID;\n}\n\nfunction filterHeaders(\n headers: Readonly<Record<string, string>> | undefined,\n allowedHeaders: readonly string[] | undefined,\n) {\n if (!headers || !allowedHeaders || allowedHeaders.length === 0) {\n return undefined;\n }\n\n const allowed = new Set(allowedHeaders.map(header => header.toLowerCase()));\n let filtered: Record<string, string> | undefined;\n for (const [key, value] of Object.entries(headers)) {\n if (allowed.has(key.toLowerCase())) {\n filtered ??= {};\n filtered[key] = value;\n }\n }\n return filtered;\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { LogContext } from '@rocicorp/logger';
|
|
2
|
-
import type { AnalyzeQueryResult } from '../../../../zero-protocol/src/analyze-query-result.ts';
|
|
3
2
|
import type { QueryServerMetrics } from '../../../../zero-protocol/src/inspect-down.ts';
|
|
4
3
|
import { type UnparsedInspectUpBody } from '../../../../zero-protocol/src/inspect-up.ts';
|
|
5
4
|
import type { NormalizedZeroConfig } from '../../config/normalize.ts';
|
|
@@ -9,14 +8,6 @@ import type { ConnectionContext } from './connection-context-manager.ts';
|
|
|
9
8
|
import type { CVRStore } from './cvr-store.ts';
|
|
10
9
|
import type { CVRSnapshot } from './cvr.ts';
|
|
11
10
|
export declare function handleInspect(lc: LogContext, body: UnparsedInspectUpBody, cvr: CVRSnapshot, client: ClientHandler, inspectorDelegate: InspectorDelegate, clientGroupID: string, authOwner: object, cvrStore: CVRStore, config: NormalizedZeroConfig, ctx: ConnectionContext): Promise<void>;
|
|
12
|
-
/**
|
|
13
|
-
* Removes the planner's `filter` nodes from `joinPlans` for clients that
|
|
14
|
-
* can't parse them: a `filter` node fails an older client's parse of the
|
|
15
|
-
* whole message, which disconnects it.
|
|
16
|
-
*
|
|
17
|
-
* @visibleForTesting
|
|
18
|
-
*/
|
|
19
|
-
export declare function withoutFilterNodes(result: AnalyzeQueryResult): AnalyzeQueryResult;
|
|
20
11
|
/**
|
|
21
12
|
* Converts per-query server metrics to the appropriate wire format based on the
|
|
22
13
|
* client's protocol version.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inspect-handler.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/inspect-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAKjD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"inspect-handler.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/inspect-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAKjD,OAAO,KAAK,EACV,kBAAkB,EAEnB,MAAM,+CAA+C,CAAC;AACvD,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,6CAA6C,CAAC;AAGrD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,2BAA2B,CAAC;AAMpE,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,oCAAoC,CAAC;AAE1E,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,UAAU,CAAC;AAE1C,wBAAsB,aAAa,CACjC,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,qBAAqB,EAC3B,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,aAAa,EACrB,iBAAiB,EAAE,iBAAiB,EACpC,aAAa,EAAE,MAAM,EAGrB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,oBAAoB,EAC5B,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,IAAI,CAAC,CAiJf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,kBAAkB,GAAG,IAAI,EAClC,eAAe,EAAE,MAAM,GACtB,kBAAkB,GAAG,IAAI,CAmB3B"}
|
|
@@ -85,7 +85,7 @@ async function handleInspect(lc, body, cvr, client, inspectorDelegate, clientGro
|
|
|
85
85
|
client.sendInspectResponse(lc, {
|
|
86
86
|
op: "analyze-query",
|
|
87
87
|
id: body.id,
|
|
88
|
-
value:
|
|
88
|
+
value: result
|
|
89
89
|
});
|
|
90
90
|
break;
|
|
91
91
|
}
|
|
@@ -101,20 +101,6 @@ async function handleInspect(lc, body, cvr, client, inspectorDelegate, clientGro
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
|
-
* Removes the planner's `filter` nodes from `joinPlans` for clients that
|
|
105
|
-
* can't parse them: a `filter` node fails an older client's parse of the
|
|
106
|
-
* whole message, which disconnects it.
|
|
107
|
-
*
|
|
108
|
-
* @visibleForTesting
|
|
109
|
-
*/
|
|
110
|
-
function withoutFilterNodes(result) {
|
|
111
|
-
if (result.joinPlans === void 0) return result;
|
|
112
|
-
return {
|
|
113
|
-
...result,
|
|
114
|
-
joinPlans: result.joinPlans.filter((event) => !("nodeType" in event && event.nodeType === "filter"))
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
104
|
* Converts per-query server metrics to the appropriate wire format based on the
|
|
119
105
|
* client's protocol version.
|
|
120
106
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inspect-handler.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/inspect-handler.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {unreachable} from '../../../../shared/src/asserts.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {TDigest} from '../../../../shared/src/tdigest.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {AnalyzeQueryResult} from '../../../../zero-protocol/src/analyze-query-result.ts';\nimport type {\n QueryServerMetrics,\n ServerMetrics,\n} from '../../../../zero-protocol/src/inspect-down.ts';\nimport {\n inspectAnalyzeQueryUpSchema,\n type UnparsedInspectUpBody,\n} from '../../../../zero-protocol/src/inspect-up.ts';\nimport {ProtocolFlag} from '../../../../zero-protocol/src/protocol-flag.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {loadPermissions} from '../../auth/load-permissions.ts';\nimport type {NormalizedZeroConfig} from '../../config/normalize.ts';\nimport {\n getServerVersion,\n isAdminPasswordValid,\n} from '../../config/zero-config.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport type {InspectorDelegate} from '../../server/inspector-delegate.ts';\nimport {analyzeQuery} from '../analyze.ts';\nimport type {ClientHandler} from './client-handler.ts';\nimport type {ConnectionContext} from './connection-context-manager.ts';\nimport type {CVRStore} from './cvr-store.ts';\nimport type {CVRSnapshot} from './cvr.ts';\n\nexport async function handleInspect(\n lc: LogContext,\n body: UnparsedInspectUpBody,\n cvr: CVRSnapshot,\n client: ClientHandler,\n inspectorDelegate: InspectorDelegate,\n clientGroupID: string,\n // The service handling the request, on whose behalf authentication is\n // recorded (see InspectorDelegate.clearAuthenticated).\n authOwner: object,\n cvrStore: CVRStore,\n config: NormalizedZeroConfig,\n ctx: ConnectionContext,\n): Promise<void> {\n // Check if the client is already authenticated. We only authenticate the clientGroup\n // once per \"worker\".\n if (\n body.op !== 'authenticate' &&\n !inspectorDelegate.isAuthenticated(clientGroupID)\n ) {\n lc.info?.(\n 'Client not authenticated to access the inspector protocol. Sending authentication challenge',\n );\n client.sendInspectResponse(lc, {\n op: 'authenticated',\n id: body.id,\n value: false,\n });\n return;\n }\n\n try {\n switch (body.op) {\n case 'queries': {\n const queryRows = await cvrStore.inspectQueries(\n lc,\n cvr.ttlClock,\n body.clientID,\n );\n\n // Enhance query rows with server-side materialization metrics\n const enhancedRows = queryRows.map(row => ({\n ...row,\n ast: row.ast ?? inspectorDelegate.getASTForQuery(row.queryID) ?? null,\n metrics: metricsForProtocol(\n inspectorDelegate.getMetricsJSONForQuery(row.queryID),\n ctx.protocolVersion,\n ),\n }));\n\n client.sendInspectResponse(lc, {\n op: 'queries',\n id: body.id,\n value: enhancedRows,\n });\n break;\n }\n\n case 'metrics': {\n client.sendInspectResponse(lc, {\n op: 'metrics',\n id: body.id,\n value: inspectorDelegate.getMetricsJSON(),\n });\n break;\n }\n\n case 'version':\n client.sendInspectResponse(lc, {\n op: 'version',\n id: body.id,\n value: getServerVersion(config),\n });\n break;\n\n case 'authenticate': {\n const password = body.value;\n const ok = isAdminPasswordValid(lc, config, password);\n if (ok) {\n inspectorDelegate.setAuthenticated(clientGroupID, authOwner);\n } else {\n inspectorDelegate.clearAuthenticated(clientGroupID);\n }\n\n client.sendInspectResponse(lc, {\n op: 'authenticated',\n id: body.id,\n value: ok,\n });\n\n break;\n }\n\n case 'analyze-query': {\n const parsedBody = v.parse(body, inspectAnalyzeQueryUpSchema);\n let ast = parsedBody.ast ?? parsedBody.value;\n let legacyQuery = true;\n\n if (parsedBody.name && parsedBody.args) {\n // Get the AST from the API server by transforming the named query\n ast = await inspectorDelegate.transformCustomQuery(\n parsedBody.name,\n parsedBody.args,\n ctx,\n );\n legacyQuery = false;\n }\n\n if (ast === undefined) {\n throw new Error(\n 'AST is required for analyze-query operation. Either provide an AST directly or ensure custom query transformation is configured.',\n );\n }\n\n let permissions;\n if (legacyQuery) {\n using db = new Database(lc, config.replica.file);\n const dbRunner = new StatementRunner(db);\n const loaded = loadPermissions(lc, dbRunner, config.app.id, config);\n if (loaded.permissions) {\n permissions = loaded.permissions;\n } else {\n lc.info?.(\n 'No permissions loaded; analyze-query will run without applying permissions.',\n );\n }\n }\n\n const result = await analyzeQuery(\n lc,\n config,\n must(cvr.clientSchema),\n ast,\n parsedBody.options?.syncedRows,\n parsedBody.options?.vendedRows,\n permissions,\n ctx.auth?.type === 'jwt' ? ctx.auth : undefined,\n parsedBody.options?.joinPlans,\n );\n client.sendInspectResponse(lc, {\n op: 'analyze-query',\n id: body.id,\n value: ctx.protocolFlags.has(ProtocolFlag.AnalyzeFilterNode)\n ? result\n : withoutFilterNodes(result),\n });\n break;\n }\n\n default:\n unreachable(body);\n }\n } catch (e) {\n lc.warn?.('Error handling inspect message', e);\n client.sendInspectResponse(lc, {\n op: 'error',\n id: body.id,\n value: (e as Error).message,\n });\n }\n}\n\n/**\n * Removes the planner's `filter` nodes from `joinPlans` for clients that\n * can't parse them: a `filter` node fails an older client's parse of the\n * whole message, which disconnects it.\n *\n * @visibleForTesting\n */\nexport function withoutFilterNodes(\n result: AnalyzeQueryResult,\n): AnalyzeQueryResult {\n if (result.joinPlans === undefined) {\n return result;\n }\n return {\n ...result,\n joinPlans: result.joinPlans.filter(\n event => !('nodeType' in event && event.nodeType === 'filter'),\n ),\n };\n}\n\n/**\n * Converts per-query server metrics to the appropriate wire format based on the\n * client's protocol version.\n *\n * Protocol >= 51: new format — `query-hydration-server-ms` (plain number) +\n * `query-update-server` (TDigest).\n * Protocol < 51: old format — `query-materialization-server` (TDigest) +\n * `query-update-server` (TDigest). The scalar hydration time is wrapped into a\n * one-point TDigest for backward compatibility.\n *\n * @visibleForTesting\n */\nexport function metricsForProtocol(\n metrics: QueryServerMetrics | null,\n protocolVersion: number,\n): QueryServerMetrics | null {\n if (protocolVersion >= 51 || metrics === null) {\n return metrics;\n }\n // Backward compat: wrap the scalar hydration ms into a one-point TDigest\n // under the old field name so that 1.5 clients can parse the response.\n const hydrateDigest = new TDigest();\n const hydrateMs = metrics['query-hydration-server-ms'];\n if (hydrateMs !== undefined) {\n hydrateDigest.add(hydrateMs);\n }\n const legacyMetrics: ServerMetrics = {\n 'query-materialization-server': hydrateDigest.toJSON(),\n 'query-update-server': metrics['query-update-server'],\n };\n // Cast to QueryServerMetrics: this is intentional — for old-protocol clients\n // we send the legacy ServerMetrics wire shape under the QueryServerMetrics type\n // to satisfy TypeScript while preserving backward compatibility.\n return legacyMetrics as unknown as QueryServerMetrics;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"inspect-handler.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/inspect-handler.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {unreachable} from '../../../../shared/src/asserts.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {TDigest} from '../../../../shared/src/tdigest.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {\n QueryServerMetrics,\n ServerMetrics,\n} from '../../../../zero-protocol/src/inspect-down.ts';\nimport {\n inspectAnalyzeQueryUpSchema,\n type UnparsedInspectUpBody,\n} from '../../../../zero-protocol/src/inspect-up.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {loadPermissions} from '../../auth/load-permissions.ts';\nimport type {NormalizedZeroConfig} from '../../config/normalize.ts';\nimport {\n getServerVersion,\n isAdminPasswordValid,\n} from '../../config/zero-config.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport type {InspectorDelegate} from '../../server/inspector-delegate.ts';\nimport {analyzeQuery} from '../analyze.ts';\nimport type {ClientHandler} from './client-handler.ts';\nimport type {ConnectionContext} from './connection-context-manager.ts';\nimport type {CVRStore} from './cvr-store.ts';\nimport type {CVRSnapshot} from './cvr.ts';\n\nexport async function handleInspect(\n lc: LogContext,\n body: UnparsedInspectUpBody,\n cvr: CVRSnapshot,\n client: ClientHandler,\n inspectorDelegate: InspectorDelegate,\n clientGroupID: string,\n // The service handling the request, on whose behalf authentication is\n // recorded (see InspectorDelegate.clearAuthenticated).\n authOwner: object,\n cvrStore: CVRStore,\n config: NormalizedZeroConfig,\n ctx: ConnectionContext,\n): Promise<void> {\n // Check if the client is already authenticated. We only authenticate the clientGroup\n // once per \"worker\".\n if (\n body.op !== 'authenticate' &&\n !inspectorDelegate.isAuthenticated(clientGroupID)\n ) {\n lc.info?.(\n 'Client not authenticated to access the inspector protocol. Sending authentication challenge',\n );\n client.sendInspectResponse(lc, {\n op: 'authenticated',\n id: body.id,\n value: false,\n });\n return;\n }\n\n try {\n switch (body.op) {\n case 'queries': {\n const queryRows = await cvrStore.inspectQueries(\n lc,\n cvr.ttlClock,\n body.clientID,\n );\n\n // Enhance query rows with server-side materialization metrics\n const enhancedRows = queryRows.map(row => ({\n ...row,\n ast: row.ast ?? inspectorDelegate.getASTForQuery(row.queryID) ?? null,\n metrics: metricsForProtocol(\n inspectorDelegate.getMetricsJSONForQuery(row.queryID),\n ctx.protocolVersion,\n ),\n }));\n\n client.sendInspectResponse(lc, {\n op: 'queries',\n id: body.id,\n value: enhancedRows,\n });\n break;\n }\n\n case 'metrics': {\n client.sendInspectResponse(lc, {\n op: 'metrics',\n id: body.id,\n value: inspectorDelegate.getMetricsJSON(),\n });\n break;\n }\n\n case 'version':\n client.sendInspectResponse(lc, {\n op: 'version',\n id: body.id,\n value: getServerVersion(config),\n });\n break;\n\n case 'authenticate': {\n const password = body.value;\n const ok = isAdminPasswordValid(lc, config, password);\n if (ok) {\n inspectorDelegate.setAuthenticated(clientGroupID, authOwner);\n } else {\n inspectorDelegate.clearAuthenticated(clientGroupID);\n }\n\n client.sendInspectResponse(lc, {\n op: 'authenticated',\n id: body.id,\n value: ok,\n });\n\n break;\n }\n\n case 'analyze-query': {\n const parsedBody = v.parse(body, inspectAnalyzeQueryUpSchema);\n let ast = parsedBody.ast ?? parsedBody.value;\n let legacyQuery = true;\n\n if (parsedBody.name && parsedBody.args) {\n // Get the AST from the API server by transforming the named query\n ast = await inspectorDelegate.transformCustomQuery(\n parsedBody.name,\n parsedBody.args,\n ctx,\n );\n legacyQuery = false;\n }\n\n if (ast === undefined) {\n throw new Error(\n 'AST is required for analyze-query operation. Either provide an AST directly or ensure custom query transformation is configured.',\n );\n }\n\n let permissions;\n if (legacyQuery) {\n using db = new Database(lc, config.replica.file);\n const dbRunner = new StatementRunner(db);\n const loaded = loadPermissions(lc, dbRunner, config.app.id, config);\n if (loaded.permissions) {\n permissions = loaded.permissions;\n } else {\n lc.info?.(\n 'No permissions loaded; analyze-query will run without applying permissions.',\n );\n }\n }\n\n const result = await analyzeQuery(\n lc,\n config,\n must(cvr.clientSchema),\n ast,\n parsedBody.options?.syncedRows,\n parsedBody.options?.vendedRows,\n permissions,\n ctx.auth?.type === 'jwt' ? ctx.auth : undefined,\n parsedBody.options?.joinPlans,\n );\n client.sendInspectResponse(lc, {\n op: 'analyze-query',\n id: body.id,\n value: result,\n });\n break;\n }\n\n default:\n unreachable(body);\n }\n } catch (e) {\n lc.warn?.('Error handling inspect message', e);\n client.sendInspectResponse(lc, {\n op: 'error',\n id: body.id,\n value: (e as Error).message,\n });\n }\n}\n\n/**\n * Converts per-query server metrics to the appropriate wire format based on the\n * client's protocol version.\n *\n * Protocol >= 51: new format — `query-hydration-server-ms` (plain number) +\n * `query-update-server` (TDigest).\n * Protocol < 51: old format — `query-materialization-server` (TDigest) +\n * `query-update-server` (TDigest). The scalar hydration time is wrapped into a\n * one-point TDigest for backward compatibility.\n *\n * @visibleForTesting\n */\nexport function metricsForProtocol(\n metrics: QueryServerMetrics | null,\n protocolVersion: number,\n): QueryServerMetrics | null {\n if (protocolVersion >= 51 || metrics === null) {\n return metrics;\n }\n // Backward compat: wrap the scalar hydration ms into a one-point TDigest\n // under the old field name so that 1.5 clients can parse the response.\n const hydrateDigest = new TDigest();\n const hydrateMs = metrics['query-hydration-server-ms'];\n if (hydrateMs !== undefined) {\n hydrateDigest.add(hydrateMs);\n }\n const legacyMetrics: ServerMetrics = {\n 'query-materialization-server': hydrateDigest.toJSON(),\n 'query-update-server': metrics['query-update-server'],\n };\n // Cast to QueryServerMetrics: this is intentional — for old-protocol clients\n // we send the legacy ServerMetrics wire shape under the QueryServerMetrics type\n // to satisfy TypeScript while preserving backward compatibility.\n return legacyMetrics as unknown as QueryServerMetrics;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshotter.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/snapshotter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAY,KAAK,SAAS,EAAC,MAAM,uCAAuC,CAAC;AAGhF,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,uCAAuC,CAAC;AAC/D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,sCAAsC,CAAC;AAIrE,OAAO,KAAK,EACV,cAAc,EACd,+BAA+B,EAChC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAC,eAAe,EAAC,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,QAAQ,EACd,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAC,KAAK,EAAC,MAAM,uBAAuB,CAAC;AAYjD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,qBAAa,WAAW;;IAStB;;;;;OAKG;IACH,YACE,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,MAAM,EACd,EAAC,KAAK,EAAC,EAAE,KAAK,EACd,gBAAgB,CAAC,EAAE,MAAM,EACzB,QAAQ,CAAC,EAAE,gBAAgB,EAO5B;IAED;;;;OAIG;IACH,IAAI,IAAI,IAAI,CAUX;IAED,WAAW,IAAI,OAAO,CAErB;IAED,+EAA+E;IAC/E,OAAO,IAAI,QAAQ,CAGlB;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAwCG;IACH,OAAO,CACL,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,EAC3C,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,EAC1B,cAAc,CAAC,EAAE,WAAW,GAAG,SAAS,EACxC,UAAU,GAAE,UAAsB,GACjC,YAAY,CAYd;IAED,kBAAkB;QAYR,IAAI;QAAc,IAAI;MAC/B;IAED;;;OAGG;IACH,OAAO,
|
|
1
|
+
{"version":3,"file":"snapshotter.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/snapshotter.ts"],"names":[],"mappings":"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"}
|
|
@@ -171,8 +171,8 @@ var Snapshotter = class {
|
|
|
171
171
|
* no longer needed.
|
|
172
172
|
*/
|
|
173
173
|
destroy() {
|
|
174
|
-
this.#curr?.db.db.close();
|
|
175
|
-
this.#prev?.db.db.close();
|
|
174
|
+
this.#curr?.db.db.close({ optimize: false });
|
|
175
|
+
this.#prev?.db.db.close({ optimize: false });
|
|
176
176
|
this.#lc.debug?.("closed database connections");
|
|
177
177
|
}
|
|
178
178
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshotter.js","names":["#lc","#dbFile","#appID","#pageCacheSizeKib","#rowCache","#curr","#prev","#assertNotAdvanced","#reset","#permissionsTable","#syncableTables","#allTableNames","#observedTables","#prevTagUniform","#prevTagDivergent","#prevWrites","#iterated"],"sources":["../../../../../../zero-cache/src/services/view-syncer/snapshotter.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {stringify, type JSONValue} from '../../../../shared/src/bigint-json.ts';\nimport {getOrInsertComputed} from '../../../../shared/src/map.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {Row} from '../../../../zero-protocol/src/data.ts';\nimport type {PrimaryKey} from '../../../../zero-types/src/schema.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {recordIndexUsage} from '../../../../zqlite/src/index-usage.ts';\nimport {fromSQLiteTypes} from '../../../../zqlite/src/table-source.ts';\nimport type {\n LiteAndZqlSpec,\n LiteTableSpecWithKeysAndVersion,\n} from '../../db/specs.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport {\n normalizedKeyOrder,\n type RowKey,\n type RowValue,\n} from '../../types/row-key.ts';\nimport type {AppID} from '../../types/shards.ts';\nimport {id} from '../../types/sql.ts';\nimport {\n RESET_OP,\n changeLogEntrySchema as schema,\n SET_OP,\n TRUNCATE_OP,\n} from '../replicator/schema/change-log.ts';\nimport {\n getReplicationState,\n ZERO_VERSION_COLUMN_NAME as ROW_VERSION,\n} from '../replicator/schema/replication-state.ts';\nimport type {SnapshotRowCache} from './snapshot-row-cache.ts';\n\n/**\n * A `Snapshotter` manages the progression of database snapshots for a\n * ViewSyncer.\n *\n * The Replicator and ViewSyncers operate on the same SQLite file, with the\n * Replicator being the sole writer to the database. The IVM logic in\n * ViewSyncers, however, rely on incrementally applying changes to the DB to\n * update the state of its pipelines.\n *\n * To avoid coupling the progress of the Replicator and all IVM pipelines on\n * each other, ViewSyncers operate on ephemeral forks of the database by holding\n * [concurrent](https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md)\n * snapshots of the database and simulating (but ultimately rolling back)\n * mutations on these snapshots.\n *\n * Example:\n * 1. ViewSyncer takes `snapshot_a` at version `t1` of the database and\n * hydrates its pipeline(s).\n * 2. Replicator applies a new transaction to the database and notifies\n * subscribers.\n * 3. ViewSyncer takes `snapshot_b` at `t2`, and queries the `ChangeLog` at\n * that snapshot for changes since `t1`.\n * 4. ViewSyncer applies those changes to `snapshot_a` for IVM, but does not\n * commit them. (Recall that the Replicator is the sole writer to the db, so\n * the ViewSyncer never commits any writes.)\n * 5. Replicator applies the next transaction and advances the database to `t3`.\n * 6. ViewSyncer rolls back `snapshot_a` and opens `snapshot_c` at `t3`, using\n * `snapshot_b` to simulate changes from `t2` to `t3`.\n *\n * ```\n * Replicator: t1 --------------> t2 --------------> t3 --------------->\n * ViewSyncer: [snapshot_a] ----> [snapshot_b] ----> [snapshot_c]\n * ```\n *\n * Note that the Replicator (and ViewSyncers) do not wait on the progress of\n * other ViewSyncers. If a ViewSyncer is busy hydrating at `t1`, the Replicator\n * and other ViewSyncers can progress through `t2`, `t3`, etc. independently,\n * as the busy ViewSyncer simply takes its own snapshot when it is ready.\n *\n * ```\n * Replicator: t1 --------------> t2 --------------> t3 --------------->\n * ViewSyncer1: [snapshot_a] ----> [snapshot_b] ----> [snapshot_c]\n * ViewSyncer2: [.......... snapshot_a ..........] ----> [snapshot_b]\n * ```\n *\n * To minimize Database connections (and statement preparation, etc.), the\n * Snapshotter reuses the connection from the previous (rolled back)\n * snapshot when opening the new one.\n *\n * ```\n * Replicator: t1 --------------> t2 --------------> t3 --------------->\n * ViewSyncer: [snapshot_a] ----> [snapshot_b] ----> [snapshot_c]\n * (conn_1) (conn_2) (conn_1)\n * ```\n *\n * In this manner, each ViewSyncer uses two connections that continually\n * \"leapfrog\" each other to replay the timeline of changes in isolation from\n * the Replicator and other ViewSyncers.\n */\nexport class Snapshotter {\n readonly #lc: LogContext;\n readonly #dbFile: string;\n readonly #appID: string;\n readonly #pageCacheSizeKib: number | undefined;\n readonly #rowCache: SnapshotRowCache | undefined;\n #curr: Snapshot | undefined;\n #prev: Snapshot | undefined;\n\n /**\n * @param rowCache An optional worker-wide {@link SnapshotRowCache} through\n * which the row reads performed when iterating over a\n * {@link SnapshotDiff} are shared with the other Snapshotters\n * (i.e. client groups) on the worker.\n */\n constructor(\n lc: LogContext,\n dbFile: string,\n {appID}: AppID,\n pageCacheSizeKib?: number,\n rowCache?: SnapshotRowCache,\n ) {\n this.#lc = lc;\n this.#dbFile = dbFile;\n this.#appID = appID;\n this.#pageCacheSizeKib = pageCacheSizeKib;\n this.#rowCache = rowCache;\n }\n\n /**\n * Initializes the snapshot to the current head of the database. This must be\n * only be called once. The state of whether a Snapshotter has been initialized\n * can be determined by calling {@link initialized()}.\n */\n init(): this {\n assert(this.#curr === undefined, 'Already initialized');\n this.#curr = Snapshot.create(\n this.#lc,\n this.#dbFile,\n this.#appID,\n this.#pageCacheSizeKib,\n );\n this.#lc.debug?.(`Initial snapshot at version ${this.#curr.version}`);\n return this;\n }\n\n initialized(): boolean {\n return this.#curr !== undefined;\n }\n\n /** Returns the current snapshot. Asserts if {@link initialized()} is false. */\n current(): Snapshot {\n assert(this.#curr !== undefined, 'Snapshotter has not been initialized');\n return this.#curr;\n }\n\n /**\n * Advances to the head of the Database, returning a diff between the\n * previously current Snapshot and a new Snapshot at head. This is called\n * in response to a notification from a Replicator subscription. Subsequent\n * calls to {@link current()} return the new Snapshot. Note that the Snapshotter\n * must be initialized before advancing.\n *\n * The returned {@link SnapshotDiff} contains snapshots at the endpoints\n * of the database timeline. Iterating over the diff generates a sequence\n * of {@link Change}s between the two snapshots.\n *\n * Note that this sequence is not chronological; rather, the sequence is\n * ordered by `<table, row-key>`, such that a row can appear at most once\n * in the common case, or twice if its table is `TRUNCATE`'d and a new value\n * is subsequently `INSERT`'ed. This results in dropping most intermediate\n * changes to a row and bounds the amount of work needed to catch up;\n * however, as a consequence, a consistent database state is only guaranteed\n * when the sequence has been fully consumed.\n *\n * Note that Change generation relies on the state of the underlying\n * database connections, and because the connection for the previous snapshot\n * is reused to produce the next snapshot, the diff object is only valid\n * until the next call to `advance()`.\n *\n * It is okay for the caller to apply `Change`s to the `prev` snapshot\n * during the iteration (e.g. this is necessary for IVM); the remainder\n * of the iteration is not affected because a given row can appear at most\n * once in the sequence (with the exception being TRUNCATE, after which the\n * deleted rows can be re-inserted, but this will also behave correctly if\n * the changes are applied).\n *\n * Once the changes have been applied, however, a _subsequent_ iteration\n * will not produce the correct results. In order to perform multiple\n * change-applying iterations, the caller must (1) create a save point\n * on `prev` before each iteration, and (2) rollback to the save point after\n * the iteration.\n *\n * @param prevWrites How the caller writes to `prev` during the iteration,\n * which determines which `prev` reads can be shared through the\n * {@link SnapshotRowCache}. See {@link PrevWrites}.\n */\n advance(\n syncableTables: Map<string, LiteAndZqlSpec>,\n allTableNames: Set<string>,\n observedTables?: TableFilter | undefined,\n prevWrites: PrevWrites = 'uniform',\n ): SnapshotDiff {\n const {prev, curr} = this.advanceWithoutDiff();\n return new Diff(\n this.#appID,\n syncableTables,\n allTableNames,\n prev,\n curr,\n observedTables,\n this.#rowCache,\n prevWrites,\n );\n }\n\n advanceWithoutDiff() {\n assert(this.#curr !== undefined, 'Snapshotter has not been initialized');\n const next = this.#prev\n ? this.#prev.resetToHead()\n : Snapshot.create(\n this.#lc,\n this.#curr.db.db.name,\n this.#appID,\n this.#pageCacheSizeKib,\n );\n this.#prev = this.#curr;\n this.#curr = next;\n return {prev: this.#prev, curr: this.#curr};\n }\n\n /**\n * Call this to close the database connections when the Snapshotter is\n * no longer needed.\n */\n destroy() {\n this.#curr?.db.db.close();\n this.#prev?.db.db.close();\n this.#lc.debug?.('closed database connections');\n }\n}\n\nexport type Change = {\n readonly table: string;\n /**\n * If this change represents a remove the row to remove,\n * if nextValue is not null then all rows that have a unique constraint\n * violation with nextValue.\n * In both cases these rows should be removed.\n */\n readonly prevValues: Readonly<Row>[];\n readonly nextValue: Readonly<Row> | null;\n readonly rowKey: RowKey;\n};\n\nexport interface TableFilter {\n has(table: string): boolean;\n}\n\n/**\n * How the caller of {@link Snapshotter.advance()} writes the changes of a\n * {@link SnapshotDiff} to its `prev` snapshot while iterating over it.\n *\n * A `prev` read for a table with a single unique key (its primary key)\n * cannot observe earlier changes of the iteration, since a row key appears\n * once in the diff. A read for a table with other unique keys can, because\n * the rows it returns may have been displaced or edited by earlier changes.\n *\n * * `uniform`: every caller applies every change, so such a read depends on\n * the `<prev, curr>` versions of the diff, and is shared on that basis.\n * * `divergent`: callers apply different subsets of the changes (e.g. the\n * pipeline driver skips changes that no pipeline can observe), so such a\n * read depends on the caller and is not shared.\n * * `none`: the caller does not write to `prev` (e.g. IVM derivation is\n * held in memory), so every read depends solely on `prev.version`.\n */\nexport type PrevWrites = 'uniform' | 'divergent' | 'none';\n\n/**\n * Represents the difference between two database Snapshots.\n * Iterating over the object will produce a sequence of {@link Change}s\n * between the two snapshots.\n *\n * See {@link Snapshotter.advance()} for semantics and usage.\n */\nexport type DiffTimingStats = {\n changeLogReadMs: number;\n rowReadMs: number;\n parseMs: number;\n totalDiffMs: number;\n};\n\nexport interface SnapshotDiff extends Iterable<Change> {\n readonly prev: {\n readonly db: StatementRunner;\n readonly version: string;\n };\n readonly curr: {\n readonly db: StatementRunner;\n readonly version: string;\n };\n\n /**\n * The raw number of ChangeLog entries between the snapshots, before filtering\n * out unobserved/non-syncable tables or no-op changes. Progress against this\n * total must use {@link changesScanned}, rather than the number of yielded\n * changes. Table-wide operations abort iteration and reset the pipelines.\n */\n readonly changes: number;\n\n /** The number of raw ChangeLog entries read so far, including skipped entries. */\n readonly changesScanned: number;\n\n /** The scanned entries omitted because their table is unobserved/non-syncable or the change is a no-op. */\n readonly changesSkipped: number;\n\n /** Timing metrics recorded during diff iteration. */\n readonly stats?: DiffTimingStats | undefined;\n\n /**\n * Overrides the `prevWrites` passed to {@link Snapshotter.advance()}, for a\n * caller that decides how to write to `prev` once it knows the number of\n * {@link changes}. Must be called before the diff is iterated, except to\n * switch to `divergent`, which can be done between any two changes: it\n * shares only the reads that no write to `prev` can affect.\n */\n setPrevWrites(prevWrites: PrevWrites): void;\n}\n\n/**\n * Thrown during an iteration of a {@link SnapshotDiff} when a schema\n * change or truncate is encountered, which result in aborting the\n * advancement and resetting / rehydrating the pipelines.\n */\nexport type ResetPipelinesReason =\n | 'advancement-timeout'\n | 'scalar-subquery'\n | 'schema-change'\n | 'truncation'\n | 'permissions-change';\n\nexport class ResetPipelinesSignal extends Error {\n readonly name = 'ResetPipelinesSignal';\n readonly reason: ResetPipelinesReason;\n readonly breakdown?: string | undefined;\n\n constructor(msg: string, reason: ResetPipelinesReason, breakdown?: string) {\n super(msg);\n this.reason = reason;\n this.breakdown = breakdown;\n }\n}\n\n// Memoizes, per table spec, the SQL of the row lookups performed for each\n// change, which would otherwise be rebuilt from the table spec for every\n// read. Handing out the same string for every read of a given shape also\n// lets the SnapshotRowCache and the statement cache look it up without\n// rehashing it. Specs are recreated whenever they are recomputed, so stale\n// SQL is never served.\ntype TableReads = {\n /** `SELECT <columns> FROM <table> WHERE ` */\n readonly prefix: string;\n /** {@link Snapshot.getRow} SQL, by the NUL-joined key columns. */\n readonly byKeyColumns: Map<string, string>;\n /**\n * {@link Snapshot.getRows} SQL, by the bitmask of the table's `uniqueKeys`\n * that are queried.\n */\n readonly byUniqueKeys: Map<number, string>;\n};\n\nconst tableReads = new WeakMap<LiteTableSpecWithKeysAndVersion, TableReads>();\n\nfunction readsFor(table: LiteTableSpecWithKeysAndVersion): TableReads {\n return getOrInsertComputed(tableReads, table, () => ({\n prefix: `SELECT ${Object.keys(table.columns)\n .map(c => id(c))\n .join(',')} FROM ${id(table.name)} WHERE `,\n byKeyColumns: new Map(),\n byUniqueKeys: new Map(),\n }));\n}\n\n// The byUniqueKeys bitmask must fit in a (positive) 31-bit integer.\nconst MAX_MEMOIZED_UNIQUE_KEYS = 30;\n\n/**\n * The index that SQLite creates for the `PRIMARY KEY(\"stateVersion\", \"pos\")`\n * of `_zero.changeLog2` (the first constraint of its CREATE TABLE statement,\n * hence the `_1` suffix).\n *\n * For a `stateVersion > ?` query that reads a column outside of this index\n * (e.g. `op`) and has no ORDER BY, the query planner chooses between a range\n * search of the index and a full scan of the table based on `sqlite_stat1`.\n * Statistics recorded while the change log held only a few rows make the full\n * scan look cheaper, and they persist as the table grows to millions of rows:\n * the change log of a replication-manager's replica only receives entries\n * during column backfills, and view-syncers inherit its statistics when they\n * restore its backup. Such queries therefore pin this index with INDEXED BY,\n * which also makes them fail to prepare (rather than silently scan) if the\n * index is ever renamed.\n */\nconst CHANGE_LOG_STATE_VERSION_INDEX = 'sqlite_autoindex__zero.changeLog2_1';\n\nclass Snapshot {\n static create(\n lc: LogContext,\n dbFile: string,\n appID: string,\n pageCacheSizeKib: number | undefined,\n ) {\n const conn = new Database(lc, dbFile);\n conn.pragma('synchronous = OFF'); // Applied changes are ephemeral; COMMIT is never called.\n if (pageCacheSizeKib !== undefined) {\n conn.pragma(`cache_size = -${pageCacheSizeKib}`); // Negative = size in KiB\n }\n const [{journal_mode: mode}] = conn.pragma('journal_mode') as [\n {journal_mode: string},\n ];\n // BEGIN CONCURRENT lets the Snapshotter simulate changes on a historic\n // snapshot without taking the WAL writer lock. These private changes are\n // always rolled back; the replicator is the only committer and can use\n // BEGIN IMMEDIATE on the same WAL2 database.\n assert(\n mode === 'wal2',\n `replica db must be in wal2 mode (current: ${mode})`,\n );\n\n const db = new StatementRunner(conn);\n return new Snapshot(db, appID);\n }\n\n readonly db: StatementRunner;\n readonly #appID: string;\n readonly version: string;\n #reset = false;\n\n constructor(db: StatementRunner, appID: string) {\n db.beginConcurrent();\n // Note: The subsequent read is necessary to acquire the read lock\n // (which results in the logical creation of the snapshot). Calling\n // `BEGIN CONCURRENT` alone does not result in acquiring the read lock.\n const {stateVersion} = getReplicationState(db);\n\n this.db = db;\n this.#appID = appID;\n this.version = stateVersion;\n }\n\n numChangesSince(prevVersion: string) {\n const {count} = this.db.get(\n 'SELECT COUNT(*) AS count FROM \"_zero.changeLog2\" WHERE stateVersion > ?',\n prevVersion,\n );\n return count;\n }\n\n /**\n * Whether a table-wide RESET op (i.e. a schema change) was logged after\n * `prevVersion`. Table specs computed at or before `prevVersion` are\n * stale with respect to this snapshot if so.\n */\n schemaChangedSince(prevVersion: string): boolean {\n const row = this.db.get(\n `SELECT 1 FROM \"_zero.changeLog2\" INDEXED BY \"${CHANGE_LOG_STATE_VERSION_INDEX}\"\n WHERE stateVersion > ? AND op = ? LIMIT 1`,\n prevVersion,\n RESET_OP,\n );\n return row !== undefined;\n }\n\n /**\n * Whether any table-wide op (i.e. a RESET or TRUNCATE) was logged after\n * `prevVersion`. Iterating over a diff ending at this snapshot will\n * abort with a {@link ResetPipelinesSignal} if so.\n */\n hasTableWideOpSince(prevVersion: string): boolean {\n const row = this.db.get(\n `SELECT 1 FROM \"_zero.changeLog2\" INDEXED BY \"${CHANGE_LOG_STATE_VERSION_INDEX}\"\n WHERE stateVersion > ? AND op IN (?, ?) LIMIT 1`,\n prevVersion,\n RESET_OP,\n TRUNCATE_OP,\n );\n return row !== undefined;\n }\n\n changesSince(prevVersion: string) {\n // Note: The queried fields are constrained to only those that are relevant\n // to the snapshot diff, i.e. those defined in the changeLogEntrySchema.\n const cached = this.db.statementCache.get(\n `SELECT \"stateVersion\", \"table\", \"rowKey\", \"op\" FROM \"_zero.changeLog2\"\n WHERE \"stateVersion\" > ? ORDER BY \"stateVersion\" ASC, \"pos\" ASC`,\n );\n return {\n changes: cached.statement.iterate(prevVersion),\n cleanup: () => this.db.statementCache.return(cached),\n };\n }\n\n /**\n * Reads the row with the given `rowKey`, or `undefined` if it does not\n * exist.\n *\n * @param cache An optional {@link SnapshotRowCache} through which to\n * perform the read, along with the `tag` that identifies the\n * version(s) of the replica that the result depends on.\n */\n getRow(\n table: LiteTableSpecWithKeysAndVersion,\n rowKey: JSONValue,\n cache?: SnapshotRowCache,\n tag?: string,\n ) {\n const key = normalizedKeyOrder(rowKey as RowKey);\n const cols = Object.keys(key);\n const reads = readsFor(table);\n const sql = getOrInsertComputed(\n reads.byKeyColumns,\n cols.join('\\0'),\n () => reads.prefix + cols.map(c => `${id(c)}=?`).join(' AND '),\n );\n const args = Object.values(key);\n const read = () =>\n this.db.statementCache.use(sql, cached => {\n cached.statement.safeIntegers(true);\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n const row = cached.statement.get<any>(args);\n recordIndexUsage(cached.statement, table.name);\n return row;\n });\n return cache\n ? cache.getOrRead(tag ?? '', sql, 'get', args, () => {\n const row = read();\n if (row !== undefined) {\n this.#assertNotAdvanced(row);\n }\n return row;\n })\n : read();\n }\n\n /**\n * Reads the rows that conflict with `row` on any of the given unique\n * `keys` (which includes the row with the same primary key, if any).\n *\n * @param cache An optional {@link SnapshotRowCache} through which to\n * perform the read, along with the `tag` that identifies the\n * version(s) of the replica that the result depends on.\n */\n getRows(\n table: LiteTableSpecWithKeysAndVersion,\n keys: PrimaryKey[],\n row: RowValue,\n cache?: SnapshotRowCache,\n tag?: string,\n ) {\n // Filter out keys where any column is NULL. This is both correct and\n // critical for performance:\n // 1. Correctness: NULL values can't violate uniqueness (NULL != NULL in SQL)\n // 2. Performance: SQLite's MULTI-INDEX OR optimization completely fails when\n // any branch involves NULL, falling back to a full table scan. This was\n // causing slowdowns of hundreds of times on tables with nullable unique columns.\n let validKeys: PrimaryKey[] = keys;\n let mask = (1 << keys.length) - 1;\n\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n for (let j = 0; j < key.length; j++) {\n const val = row[key[j]];\n if (val === null || val === undefined) {\n validKeys = [];\n mask = 0;\n for (let k = 0; k < keys.length; k++) {\n const candidate = keys[k];\n if (\n candidate.every(\n col => row[col] !== null && row[col] !== undefined,\n )\n ) {\n validKeys.push(candidate);\n mask |= 1 << k;\n }\n }\n break;\n }\n }\n if (validKeys !== keys) {\n break;\n }\n }\n\n if (validKeys.length === 0) {\n return [];\n }\n const reads = readsFor(table);\n const buildSQL = () =>\n reads.prefix +\n validKeys\n .map(key => key.map(c => `${id(c)}=?`).join(' AND '))\n .join(' OR ');\n const sql =\n keys === table.uniqueKeys && keys.length <= MAX_MEMOIZED_UNIQUE_KEYS\n ? getOrInsertComputed(reads.byUniqueKeys, mask, buildSQL)\n : buildSQL();\n const args: unknown[] = [];\n for (let i = 0; i < validKeys.length; i++) {\n const key = validKeys[i];\n for (let j = 0; j < key.length; j++) {\n args.push(row[key[j]]);\n }\n }\n const read = () =>\n this.db.statementCache.use(sql, cached => {\n cached.statement.safeIntegers(true);\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n const rows = cached.statement.all<any>(args);\n recordIndexUsage(cached.statement, table.name);\n return rows;\n });\n return cache\n ? cache.getOrRead(tag ?? '', sql, 'all', args, () => {\n const rows = read();\n for (const row of rows) {\n this.#assertNotAdvanced(row);\n }\n return rows;\n })\n : read();\n }\n\n /**\n * Rows are shared with other client groups via the SnapshotRowCache, so\n * one read from a connection that has advanced past this snapshot (i.e. a\n * {@link SnapshotDiff} used after its Snapshotter advanced) must fail\n * before it is cached, rather than be served to the valid Diffs of other\n * client groups (whose own validity checks would then fail).\n */\n #assertNotAdvanced(row: RowValue) {\n if ((row[ROW_VERSION] ?? '~') > this.version) {\n throw new InvalidDiffError(\n `Diff is no longer valid. db has advanced past ${this.version}.`,\n );\n }\n }\n\n /**\n * Whether the connection has been reset to a newer snapshot (i.e. via\n * {@link resetToHead()}), after which reads no longer reflect\n * {@link version}.\n */\n get reset(): boolean {\n return this.#reset;\n }\n\n resetToHead(): Snapshot {\n this.#reset = true;\n this.db.rollback();\n return new Snapshot(this.db, this.#appID);\n }\n}\n\nclass Diff implements SnapshotDiff {\n readonly #permissionsTable: string;\n readonly #syncableTables: Map<string, LiteAndZqlSpec>;\n readonly #allTableNames: Set<string>;\n readonly #observedTables: TableFilter | undefined;\n readonly #rowCache: SnapshotRowCache | undefined;\n #prevWrites: PrevWrites;\n readonly #prevTagUniform: string;\n readonly #prevTagDivergent: string;\n #iterated = false;\n readonly prev: Snapshot;\n readonly curr: Snapshot;\n readonly changes: number;\n changesScanned = 0;\n changesSkipped = 0;\n readonly stats: DiffTimingStats = {\n changeLogReadMs: 0,\n rowReadMs: 0,\n parseMs: 0,\n totalDiffMs: 0,\n };\n\n constructor(\n appID: string,\n syncableTables: Map<string, LiteAndZqlSpec>,\n allTableNames: Set<string>,\n prev: Snapshot,\n curr: Snapshot,\n observedTables?: TableFilter | undefined,\n rowCache?: SnapshotRowCache | undefined,\n prevWrites: PrevWrites = 'uniform',\n ) {\n this.#prevWrites = prevWrites;\n this.#permissionsTable = `${appID}.permissions`;\n this.#syncableTables = syncableTables;\n this.#allTableNames = allTableNames;\n this.#observedTables = observedTables;\n this.prev = prev;\n this.curr = curr;\n this.#prevTagUniform = `p:${prev.version}`;\n this.#prevTagDivergent = `p:${prev.version}:${curr.version}`;\n this.changes = curr.numChangesSince(prev.version);\n // Row values are only shared (via the cache) between Diffs that do not\n // straddle a table-wide op. This guarantees that any two Diffs that\n // encounter the same change log entry read the row from tables with\n // the same schema (and contents), since a RESET or TRUNCATE between\n // their `curr` versions would necessarily fall within the range of the\n // later Diff. (Such a Diff aborts with a ResetPipelinesSignal anyway.)\n this.#rowCache =\n rowCache && !curr.hasTableWideOpSince(prev.version)\n ? rowCache\n : undefined;\n }\n\n setPrevWrites(prevWrites: PrevWrites): void {\n assert(\n !this.#iterated || prevWrites === 'divergent',\n 'prevWrites must be set before iterating the diff',\n );\n this.#prevWrites = prevWrites;\n }\n\n [Symbol.iterator](): Iterator<Change> {\n this.#iterated = true;\n const {changes, cleanup: done} = this.curr.changesSince(this.prev.version);\n\n const cleanup = () => {\n try {\n // Allow open iterators to clean up their state.\n changes.return?.(undefined);\n } finally {\n done();\n }\n };\n\n return {\n next: () => {\n const nextStart = performance.now();\n try {\n for (;;) {\n const clStart = performance.now();\n const {value, done} = changes.next();\n this.stats.changeLogReadMs += performance.now() - clStart;\n if (done) {\n cleanup();\n return {value, done: true};\n }\n\n this.changesScanned++;\n const parseStart = performance.now();\n const {table, rowKey, op, stateVersion} = v.parse(value, schema);\n this.stats.parseMs += performance.now() - parseStart;\n if (op === RESET_OP) {\n // The current map of `TableSpec`s may not have the correct or complete information.\n throw new ResetPipelinesSignal(\n `schema for table ${table} has changed`,\n 'schema-change',\n );\n }\n if (op === TRUNCATE_OP) {\n // Truncates are also processed by rehydrating pipelines at current.\n throw new ResetPipelinesSignal(\n `table ${table} has been truncated`,\n 'truncation',\n );\n }\n if (\n this.#observedTables &&\n !this.#observedTables.has(table) &&\n table !== this.#permissionsTable\n ) {\n if (this.#allTableNames.has(table)) {\n this.changesSkipped++;\n continue; // skip change log entries for unobserved tables.\n }\n throw new Error(`change for unknown table ${table}`);\n }\n const specs = this.#syncableTables.get(table);\n if (!specs) {\n if (this.#allTableNames.has(table)) {\n this.changesSkipped++;\n continue; // skip change log entries for non-syncable tables.\n }\n throw new Error(`change for unknown table ${table}`);\n }\n const {tableSpec, zqlSpec} = specs;\n\n // Sanity check: All change log ops should have a stateVersion\n // greater than minRowVersion in the table metadata. This is a\n // mini-proof that the overlay does not need to be applied to\n // rows produced in incremental catchup, based on the invariant in\n // change-processor's #bumpVersions(), whereby the setting of the\n // minRowVersion is always followed by a RESET OP, meaning that\n // subsequent change-log traversal happens at a later version.\n assert(\n (tableSpec.minRowVersion ?? '') < stateVersion,\n () =>\n `unexpected change @${stateVersion} for table ${table} with ` +\n `minRowVersion ${tableSpec.minRowVersion}: ${op}(${rowKey})`,\n );\n\n assert(rowKey !== null, 'rowKey must be present for row changes');\n\n // Row reads are shared with the other client groups on the\n // worker via the SnapshotRowCache (if configured):\n //\n // * The new value of the row is read from `curr`, which is\n // never written to during the iteration, and is the version\n // of the row at `stateVersion` (as verified by\n // checkThatDiffIsValid()). It is thus identical for every\n // Diff that encounters the change log entry.\n //\n // * Previous values are read from `prev`, at `prev.version`,\n // which the caller (i.e. IVM) writes to as the iteration\n // progresses. For a table whose only unique key is its\n // primary key, no change applied before this one can affect\n // this row, so the result depends solely on `prev.version`.\n // Otherwise, the result can include rows that conflict on\n // other unique keys, which may have been added or removed by\n // preceding changes; see PrevWrites for when that can be\n // shared.\n // The first advance() after this Diff was created resets the\n // `prev` connection to head. Reads would then no longer reflect\n // `prev.version`, and cache hits would not reveal it (the cached\n // rows were read by valid Diffs), so this must be checked\n // explicitly rather than inferred from the rows' versions.\n if (this.prev.reset) {\n throw new InvalidDiffError(\n `Diff is no longer valid. prev db has advanced past ${this.prev.version}.`,\n );\n }\n\n const cache = this.#rowCache;\n const prevDependsOnWrites =\n tableSpec.uniqueKeys.length > 1 && this.#prevWrites !== 'none';\n const prevCache =\n prevDependsOnWrites && this.#prevWrites === 'divergent'\n ? undefined\n : cache;\n const prevTag = prevDependsOnWrites\n ? this.#prevTagDivergent\n : this.#prevTagUniform;\n const rowStart = performance.now();\n const nextValue =\n op === SET_OP\n ? this.curr.getRow(\n tableSpec,\n rowKey,\n cache,\n `n:${stateVersion}`,\n )\n : null;\n let prevValues;\n if (nextValue) {\n prevValues = this.prev.getRows(\n tableSpec,\n tableSpec.uniqueKeys,\n nextValue,\n prevCache,\n prevTag,\n );\n } else {\n const prevValue = this.prev.getRow(\n tableSpec,\n rowKey,\n prevCache,\n prevTag,\n );\n prevValues = prevValue ? [prevValue] : [];\n }\n this.stats.rowReadMs += performance.now() - rowStart;\n if (nextValue === undefined) {\n throw new Error(\n `Missing value for ${table} ${stringify(rowKey)}`,\n );\n }\n // Sanity check detects if the diff is being accessed after the Snapshots have advanced.\n this.checkThatDiffIsValid(stateVersion, op, prevValues, nextValue);\n\n if (prevValues.length === 0 && nextValue === null) {\n // Filter out no-op changes (e.g. a delete of a row that does not exist in prev).\n // TODO: Consider doing this for deep-equal values.\n this.changesSkipped++;\n continue;\n }\n\n if (\n table === this.#permissionsTable &&\n prevValues.some(\n prevValue => prevValue.permissions !== nextValue.permissions,\n )\n ) {\n throw new ResetPipelinesSignal(\n `Permissions have changed ${\n prevValues.find(\n prevValue =>\n prevValue.permissions !== nextValue.permissions,\n ).hash\n } => ${nextValue.hash}`,\n 'permissions-change',\n );\n }\n\n // Note: The raw SQLite rows may be shared with other client\n // groups via the SnapshotRowCache, so they must not be modified.\n // fromSQLiteTypes() produces new row objects.\n // TODO Can we get rid of these RowValue casts?\n return {\n value: {\n table,\n prevValues: prevValues.map(prevValue =>\n fromSQLiteTypes(zqlSpec, prevValue, table),\n ),\n nextValue: nextValue\n ? fromSQLiteTypes(zqlSpec, nextValue, table)\n : null,\n rowKey,\n } satisfies Change,\n };\n }\n } catch (e) {\n // This control flow path is not covered by the return() method (i.e. `break`).\n cleanup();\n throw e;\n } finally {\n this.stats.totalDiffMs += performance.now() - nextStart;\n }\n },\n\n return: (value: unknown) => {\n cleanup();\n return {value, done: true};\n },\n };\n }\n\n checkThatDiffIsValid(\n stateVersion: string,\n op: string,\n prevValues: RowValue[],\n nextValue: RowValue,\n ) {\n // Sanity checks to detect that the diff is not being accessed after\n // the Snapshots have advanced.\n if (stateVersion > this.curr.version) {\n throw new InvalidDiffError(\n `Diff is no longer valid. curr db has advanced past ${this.curr.version}`,\n );\n }\n if (\n prevValues.findIndex(\n prevValue => (prevValue[ROW_VERSION] ?? '~') > this.prev.version,\n ) !== -1\n ) {\n throw new InvalidDiffError(\n `Diff is no longer valid. prev db has advanced past ${this.prev.version}.`,\n );\n }\n if (op === SET_OP && nextValue[ROW_VERSION] !== stateVersion) {\n throw new InvalidDiffError(\n 'Diff is no longer valid. curr db has advanced.',\n );\n }\n }\n}\n\nexport class InvalidDiffError extends Error {\n constructor(msg: string) {\n super(msg);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"snapshotter.js","names":["#lc","#dbFile","#appID","#pageCacheSizeKib","#rowCache","#curr","#prev","#assertNotAdvanced","#reset","#permissionsTable","#syncableTables","#allTableNames","#observedTables","#prevTagUniform","#prevTagDivergent","#prevWrites","#iterated"],"sources":["../../../../../../zero-cache/src/services/view-syncer/snapshotter.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {stringify, type JSONValue} from '../../../../shared/src/bigint-json.ts';\nimport {getOrInsertComputed} from '../../../../shared/src/map.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {Row} from '../../../../zero-protocol/src/data.ts';\nimport type {PrimaryKey} from '../../../../zero-types/src/schema.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {recordIndexUsage} from '../../../../zqlite/src/index-usage.ts';\nimport {fromSQLiteTypes} from '../../../../zqlite/src/table-source.ts';\nimport type {\n LiteAndZqlSpec,\n LiteTableSpecWithKeysAndVersion,\n} from '../../db/specs.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport {\n normalizedKeyOrder,\n type RowKey,\n type RowValue,\n} from '../../types/row-key.ts';\nimport type {AppID} from '../../types/shards.ts';\nimport {id} from '../../types/sql.ts';\nimport {\n RESET_OP,\n changeLogEntrySchema as schema,\n SET_OP,\n TRUNCATE_OP,\n} from '../replicator/schema/change-log.ts';\nimport {\n getReplicationState,\n ZERO_VERSION_COLUMN_NAME as ROW_VERSION,\n} from '../replicator/schema/replication-state.ts';\nimport type {SnapshotRowCache} from './snapshot-row-cache.ts';\n\n/**\n * A `Snapshotter` manages the progression of database snapshots for a\n * ViewSyncer.\n *\n * The Replicator and ViewSyncers operate on the same SQLite file, with the\n * Replicator being the sole writer to the database. The IVM logic in\n * ViewSyncers, however, rely on incrementally applying changes to the DB to\n * update the state of its pipelines.\n *\n * To avoid coupling the progress of the Replicator and all IVM pipelines on\n * each other, ViewSyncers operate on ephemeral forks of the database by holding\n * [concurrent](https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md)\n * snapshots of the database and simulating (but ultimately rolling back)\n * mutations on these snapshots.\n *\n * Example:\n * 1. ViewSyncer takes `snapshot_a` at version `t1` of the database and\n * hydrates its pipeline(s).\n * 2. Replicator applies a new transaction to the database and notifies\n * subscribers.\n * 3. ViewSyncer takes `snapshot_b` at `t2`, and queries the `ChangeLog` at\n * that snapshot for changes since `t1`.\n * 4. ViewSyncer applies those changes to `snapshot_a` for IVM, but does not\n * commit them. (Recall that the Replicator is the sole writer to the db, so\n * the ViewSyncer never commits any writes.)\n * 5. Replicator applies the next transaction and advances the database to `t3`.\n * 6. ViewSyncer rolls back `snapshot_a` and opens `snapshot_c` at `t3`, using\n * `snapshot_b` to simulate changes from `t2` to `t3`.\n *\n * ```\n * Replicator: t1 --------------> t2 --------------> t3 --------------->\n * ViewSyncer: [snapshot_a] ----> [snapshot_b] ----> [snapshot_c]\n * ```\n *\n * Note that the Replicator (and ViewSyncers) do not wait on the progress of\n * other ViewSyncers. If a ViewSyncer is busy hydrating at `t1`, the Replicator\n * and other ViewSyncers can progress through `t2`, `t3`, etc. independently,\n * as the busy ViewSyncer simply takes its own snapshot when it is ready.\n *\n * ```\n * Replicator: t1 --------------> t2 --------------> t3 --------------->\n * ViewSyncer1: [snapshot_a] ----> [snapshot_b] ----> [snapshot_c]\n * ViewSyncer2: [.......... snapshot_a ..........] ----> [snapshot_b]\n * ```\n *\n * To minimize Database connections (and statement preparation, etc.), the\n * Snapshotter reuses the connection from the previous (rolled back)\n * snapshot when opening the new one.\n *\n * ```\n * Replicator: t1 --------------> t2 --------------> t3 --------------->\n * ViewSyncer: [snapshot_a] ----> [snapshot_b] ----> [snapshot_c]\n * (conn_1) (conn_2) (conn_1)\n * ```\n *\n * In this manner, each ViewSyncer uses two connections that continually\n * \"leapfrog\" each other to replay the timeline of changes in isolation from\n * the Replicator and other ViewSyncers.\n */\nexport class Snapshotter {\n readonly #lc: LogContext;\n readonly #dbFile: string;\n readonly #appID: string;\n readonly #pageCacheSizeKib: number | undefined;\n readonly #rowCache: SnapshotRowCache | undefined;\n #curr: Snapshot | undefined;\n #prev: Snapshot | undefined;\n\n /**\n * @param rowCache An optional worker-wide {@link SnapshotRowCache} through\n * which the row reads performed when iterating over a\n * {@link SnapshotDiff} are shared with the other Snapshotters\n * (i.e. client groups) on the worker.\n */\n constructor(\n lc: LogContext,\n dbFile: string,\n {appID}: AppID,\n pageCacheSizeKib?: number,\n rowCache?: SnapshotRowCache,\n ) {\n this.#lc = lc;\n this.#dbFile = dbFile;\n this.#appID = appID;\n this.#pageCacheSizeKib = pageCacheSizeKib;\n this.#rowCache = rowCache;\n }\n\n /**\n * Initializes the snapshot to the current head of the database. This must be\n * only be called once. The state of whether a Snapshotter has been initialized\n * can be determined by calling {@link initialized()}.\n */\n init(): this {\n assert(this.#curr === undefined, 'Already initialized');\n this.#curr = Snapshot.create(\n this.#lc,\n this.#dbFile,\n this.#appID,\n this.#pageCacheSizeKib,\n );\n this.#lc.debug?.(`Initial snapshot at version ${this.#curr.version}`);\n return this;\n }\n\n initialized(): boolean {\n return this.#curr !== undefined;\n }\n\n /** Returns the current snapshot. Asserts if {@link initialized()} is false. */\n current(): Snapshot {\n assert(this.#curr !== undefined, 'Snapshotter has not been initialized');\n return this.#curr;\n }\n\n /**\n * Advances to the head of the Database, returning a diff between the\n * previously current Snapshot and a new Snapshot at head. This is called\n * in response to a notification from a Replicator subscription. Subsequent\n * calls to {@link current()} return the new Snapshot. Note that the Snapshotter\n * must be initialized before advancing.\n *\n * The returned {@link SnapshotDiff} contains snapshots at the endpoints\n * of the database timeline. Iterating over the diff generates a sequence\n * of {@link Change}s between the two snapshots.\n *\n * Note that this sequence is not chronological; rather, the sequence is\n * ordered by `<table, row-key>`, such that a row can appear at most once\n * in the common case, or twice if its table is `TRUNCATE`'d and a new value\n * is subsequently `INSERT`'ed. This results in dropping most intermediate\n * changes to a row and bounds the amount of work needed to catch up;\n * however, as a consequence, a consistent database state is only guaranteed\n * when the sequence has been fully consumed.\n *\n * Note that Change generation relies on the state of the underlying\n * database connections, and because the connection for the previous snapshot\n * is reused to produce the next snapshot, the diff object is only valid\n * until the next call to `advance()`.\n *\n * It is okay for the caller to apply `Change`s to the `prev` snapshot\n * during the iteration (e.g. this is necessary for IVM); the remainder\n * of the iteration is not affected because a given row can appear at most\n * once in the sequence (with the exception being TRUNCATE, after which the\n * deleted rows can be re-inserted, but this will also behave correctly if\n * the changes are applied).\n *\n * Once the changes have been applied, however, a _subsequent_ iteration\n * will not produce the correct results. In order to perform multiple\n * change-applying iterations, the caller must (1) create a save point\n * on `prev` before each iteration, and (2) rollback to the save point after\n * the iteration.\n *\n * @param prevWrites How the caller writes to `prev` during the iteration,\n * which determines which `prev` reads can be shared through the\n * {@link SnapshotRowCache}. See {@link PrevWrites}.\n */\n advance(\n syncableTables: Map<string, LiteAndZqlSpec>,\n allTableNames: Set<string>,\n observedTables?: TableFilter | undefined,\n prevWrites: PrevWrites = 'uniform',\n ): SnapshotDiff {\n const {prev, curr} = this.advanceWithoutDiff();\n return new Diff(\n this.#appID,\n syncableTables,\n allTableNames,\n prev,\n curr,\n observedTables,\n this.#rowCache,\n prevWrites,\n );\n }\n\n advanceWithoutDiff() {\n assert(this.#curr !== undefined, 'Snapshotter has not been initialized');\n const next = this.#prev\n ? this.#prev.resetToHead()\n : Snapshot.create(\n this.#lc,\n this.#curr.db.db.name,\n this.#appID,\n this.#pageCacheSizeKib,\n );\n this.#prev = this.#curr;\n this.#curr = next;\n return {prev: this.#prev, curr: this.#curr};\n }\n\n /**\n * Call this to close the database connections when the Snapshotter is\n * no longer needed.\n */\n destroy() {\n // The replica's statistics are maintained by the replicator. The\n // `PRAGMA optimize` that closing a writable connection otherwise runs\n // analyzes the tables that the connection read, which blocks the worker\n // for as long as that takes, and what it finds is discarded with the\n // snapshot's transaction.\n this.#curr?.db.db.close({optimize: false});\n this.#prev?.db.db.close({optimize: false});\n this.#lc.debug?.('closed database connections');\n }\n}\n\nexport type Change = {\n readonly table: string;\n /**\n * If this change represents a remove the row to remove,\n * if nextValue is not null then all rows that have a unique constraint\n * violation with nextValue.\n * In both cases these rows should be removed.\n */\n readonly prevValues: Readonly<Row>[];\n readonly nextValue: Readonly<Row> | null;\n readonly rowKey: RowKey;\n};\n\nexport interface TableFilter {\n has(table: string): boolean;\n}\n\n/**\n * How the caller of {@link Snapshotter.advance()} writes the changes of a\n * {@link SnapshotDiff} to its `prev` snapshot while iterating over it.\n *\n * A `prev` read for a table with a single unique key (its primary key)\n * cannot observe earlier changes of the iteration, since a row key appears\n * once in the diff. A read for a table with other unique keys can, because\n * the rows it returns may have been displaced or edited by earlier changes.\n *\n * * `uniform`: every caller applies every change, so such a read depends on\n * the `<prev, curr>` versions of the diff, and is shared on that basis.\n * * `divergent`: callers apply different subsets of the changes (e.g. the\n * pipeline driver skips changes that no pipeline can observe), so such a\n * read depends on the caller and is not shared.\n * * `none`: the caller does not write to `prev` (e.g. IVM derivation is\n * held in memory), so every read depends solely on `prev.version`.\n */\nexport type PrevWrites = 'uniform' | 'divergent' | 'none';\n\n/**\n * Represents the difference between two database Snapshots.\n * Iterating over the object will produce a sequence of {@link Change}s\n * between the two snapshots.\n *\n * See {@link Snapshotter.advance()} for semantics and usage.\n */\nexport type DiffTimingStats = {\n changeLogReadMs: number;\n rowReadMs: number;\n parseMs: number;\n totalDiffMs: number;\n};\n\nexport interface SnapshotDiff extends Iterable<Change> {\n readonly prev: {\n readonly db: StatementRunner;\n readonly version: string;\n };\n readonly curr: {\n readonly db: StatementRunner;\n readonly version: string;\n };\n\n /**\n * The raw number of ChangeLog entries between the snapshots, before filtering\n * out unobserved/non-syncable tables or no-op changes. Progress against this\n * total must use {@link changesScanned}, rather than the number of yielded\n * changes. Table-wide operations abort iteration and reset the pipelines.\n */\n readonly changes: number;\n\n /** The number of raw ChangeLog entries read so far, including skipped entries. */\n readonly changesScanned: number;\n\n /** The scanned entries omitted because their table is unobserved/non-syncable or the change is a no-op. */\n readonly changesSkipped: number;\n\n /** Timing metrics recorded during diff iteration. */\n readonly stats?: DiffTimingStats | undefined;\n\n /**\n * Overrides the `prevWrites` passed to {@link Snapshotter.advance()}, for a\n * caller that decides how to write to `prev` once it knows the number of\n * {@link changes}. Must be called before the diff is iterated, except to\n * switch to `divergent`, which can be done between any two changes: it\n * shares only the reads that no write to `prev` can affect.\n */\n setPrevWrites(prevWrites: PrevWrites): void;\n}\n\n/**\n * Thrown during an iteration of a {@link SnapshotDiff} when a schema\n * change or truncate is encountered, which result in aborting the\n * advancement and resetting / rehydrating the pipelines.\n */\nexport type ResetPipelinesReason =\n | 'advancement-timeout'\n | 'scalar-subquery'\n | 'schema-change'\n | 'truncation'\n | 'permissions-change';\n\nexport class ResetPipelinesSignal extends Error {\n readonly name = 'ResetPipelinesSignal';\n readonly reason: ResetPipelinesReason;\n readonly breakdown?: string | undefined;\n\n constructor(msg: string, reason: ResetPipelinesReason, breakdown?: string) {\n super(msg);\n this.reason = reason;\n this.breakdown = breakdown;\n }\n}\n\n// Memoizes, per table spec, the SQL of the row lookups performed for each\n// change, which would otherwise be rebuilt from the table spec for every\n// read. Handing out the same string for every read of a given shape also\n// lets the SnapshotRowCache and the statement cache look it up without\n// rehashing it. Specs are recreated whenever they are recomputed, so stale\n// SQL is never served.\ntype TableReads = {\n /** `SELECT <columns> FROM <table> WHERE ` */\n readonly prefix: string;\n /** {@link Snapshot.getRow} SQL, by the NUL-joined key columns. */\n readonly byKeyColumns: Map<string, string>;\n /**\n * {@link Snapshot.getRows} SQL, by the bitmask of the table's `uniqueKeys`\n * that are queried.\n */\n readonly byUniqueKeys: Map<number, string>;\n};\n\nconst tableReads = new WeakMap<LiteTableSpecWithKeysAndVersion, TableReads>();\n\nfunction readsFor(table: LiteTableSpecWithKeysAndVersion): TableReads {\n return getOrInsertComputed(tableReads, table, () => ({\n prefix: `SELECT ${Object.keys(table.columns)\n .map(c => id(c))\n .join(',')} FROM ${id(table.name)} WHERE `,\n byKeyColumns: new Map(),\n byUniqueKeys: new Map(),\n }));\n}\n\n// The byUniqueKeys bitmask must fit in a (positive) 31-bit integer.\nconst MAX_MEMOIZED_UNIQUE_KEYS = 30;\n\n/**\n * The index that SQLite creates for the `PRIMARY KEY(\"stateVersion\", \"pos\")`\n * of `_zero.changeLog2` (the first constraint of its CREATE TABLE statement,\n * hence the `_1` suffix).\n *\n * For a `stateVersion > ?` query that reads a column outside of this index\n * (e.g. `op`) and has no ORDER BY, the query planner chooses between a range\n * search of the index and a full scan of the table based on `sqlite_stat1`.\n * Statistics recorded while the change log held only a few rows make the full\n * scan look cheaper, and they persist as the table grows to millions of rows:\n * the change log of a replication-manager's replica only receives entries\n * during column backfills, and view-syncers inherit its statistics when they\n * restore its backup. Such queries therefore pin this index with INDEXED BY,\n * which also makes them fail to prepare (rather than silently scan) if the\n * index is ever renamed.\n */\nconst CHANGE_LOG_STATE_VERSION_INDEX = 'sqlite_autoindex__zero.changeLog2_1';\n\nclass Snapshot {\n static create(\n lc: LogContext,\n dbFile: string,\n appID: string,\n pageCacheSizeKib: number | undefined,\n ) {\n const conn = new Database(lc, dbFile);\n conn.pragma('synchronous = OFF'); // Applied changes are ephemeral; COMMIT is never called.\n if (pageCacheSizeKib !== undefined) {\n conn.pragma(`cache_size = -${pageCacheSizeKib}`); // Negative = size in KiB\n }\n const [{journal_mode: mode}] = conn.pragma('journal_mode') as [\n {journal_mode: string},\n ];\n // BEGIN CONCURRENT lets the Snapshotter simulate changes on a historic\n // snapshot without taking the WAL writer lock. These private changes are\n // always rolled back; the replicator is the only committer and can use\n // BEGIN IMMEDIATE on the same WAL2 database.\n assert(\n mode === 'wal2',\n `replica db must be in wal2 mode (current: ${mode})`,\n );\n\n const db = new StatementRunner(conn);\n return new Snapshot(db, appID);\n }\n\n readonly db: StatementRunner;\n readonly #appID: string;\n readonly version: string;\n #reset = false;\n\n constructor(db: StatementRunner, appID: string) {\n db.beginConcurrent();\n // Note: The subsequent read is necessary to acquire the read lock\n // (which results in the logical creation of the snapshot). Calling\n // `BEGIN CONCURRENT` alone does not result in acquiring the read lock.\n const {stateVersion} = getReplicationState(db);\n\n this.db = db;\n this.#appID = appID;\n this.version = stateVersion;\n }\n\n numChangesSince(prevVersion: string) {\n const {count} = this.db.get(\n 'SELECT COUNT(*) AS count FROM \"_zero.changeLog2\" WHERE stateVersion > ?',\n prevVersion,\n );\n return count;\n }\n\n /**\n * Whether a table-wide RESET op (i.e. a schema change) was logged after\n * `prevVersion`. Table specs computed at or before `prevVersion` are\n * stale with respect to this snapshot if so.\n */\n schemaChangedSince(prevVersion: string): boolean {\n const row = this.db.get(\n `SELECT 1 FROM \"_zero.changeLog2\" INDEXED BY \"${CHANGE_LOG_STATE_VERSION_INDEX}\"\n WHERE stateVersion > ? AND op = ? LIMIT 1`,\n prevVersion,\n RESET_OP,\n );\n return row !== undefined;\n }\n\n /**\n * Whether any table-wide op (i.e. a RESET or TRUNCATE) was logged after\n * `prevVersion`. Iterating over a diff ending at this snapshot will\n * abort with a {@link ResetPipelinesSignal} if so.\n */\n hasTableWideOpSince(prevVersion: string): boolean {\n const row = this.db.get(\n `SELECT 1 FROM \"_zero.changeLog2\" INDEXED BY \"${CHANGE_LOG_STATE_VERSION_INDEX}\"\n WHERE stateVersion > ? AND op IN (?, ?) LIMIT 1`,\n prevVersion,\n RESET_OP,\n TRUNCATE_OP,\n );\n return row !== undefined;\n }\n\n changesSince(prevVersion: string) {\n // Note: The queried fields are constrained to only those that are relevant\n // to the snapshot diff, i.e. those defined in the changeLogEntrySchema.\n const cached = this.db.statementCache.get(\n `SELECT \"stateVersion\", \"table\", \"rowKey\", \"op\" FROM \"_zero.changeLog2\"\n WHERE \"stateVersion\" > ? ORDER BY \"stateVersion\" ASC, \"pos\" ASC`,\n );\n return {\n changes: cached.statement.iterate(prevVersion),\n cleanup: () => this.db.statementCache.return(cached),\n };\n }\n\n /**\n * Reads the row with the given `rowKey`, or `undefined` if it does not\n * exist.\n *\n * @param cache An optional {@link SnapshotRowCache} through which to\n * perform the read, along with the `tag` that identifies the\n * version(s) of the replica that the result depends on.\n */\n getRow(\n table: LiteTableSpecWithKeysAndVersion,\n rowKey: JSONValue,\n cache?: SnapshotRowCache,\n tag?: string,\n ) {\n const key = normalizedKeyOrder(rowKey as RowKey);\n const cols = Object.keys(key);\n const reads = readsFor(table);\n const sql = getOrInsertComputed(\n reads.byKeyColumns,\n cols.join('\\0'),\n () => reads.prefix + cols.map(c => `${id(c)}=?`).join(' AND '),\n );\n const args = Object.values(key);\n const read = () =>\n this.db.statementCache.use(sql, cached => {\n cached.statement.safeIntegers(true);\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n const row = cached.statement.get<any>(args);\n recordIndexUsage(cached.statement, table.name);\n return row;\n });\n return cache\n ? cache.getOrRead(tag ?? '', sql, 'get', args, () => {\n const row = read();\n if (row !== undefined) {\n this.#assertNotAdvanced(row);\n }\n return row;\n })\n : read();\n }\n\n /**\n * Reads the rows that conflict with `row` on any of the given unique\n * `keys` (which includes the row with the same primary key, if any).\n *\n * @param cache An optional {@link SnapshotRowCache} through which to\n * perform the read, along with the `tag` that identifies the\n * version(s) of the replica that the result depends on.\n */\n getRows(\n table: LiteTableSpecWithKeysAndVersion,\n keys: PrimaryKey[],\n row: RowValue,\n cache?: SnapshotRowCache,\n tag?: string,\n ) {\n // Filter out keys where any column is NULL. This is both correct and\n // critical for performance:\n // 1. Correctness: NULL values can't violate uniqueness (NULL != NULL in SQL)\n // 2. Performance: SQLite's MULTI-INDEX OR optimization completely fails when\n // any branch involves NULL, falling back to a full table scan. This was\n // causing slowdowns of hundreds of times on tables with nullable unique columns.\n let validKeys: PrimaryKey[] = keys;\n let mask = (1 << keys.length) - 1;\n\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n for (let j = 0; j < key.length; j++) {\n const val = row[key[j]];\n if (val === null || val === undefined) {\n validKeys = [];\n mask = 0;\n for (let k = 0; k < keys.length; k++) {\n const candidate = keys[k];\n if (\n candidate.every(\n col => row[col] !== null && row[col] !== undefined,\n )\n ) {\n validKeys.push(candidate);\n mask |= 1 << k;\n }\n }\n break;\n }\n }\n if (validKeys !== keys) {\n break;\n }\n }\n\n if (validKeys.length === 0) {\n return [];\n }\n const reads = readsFor(table);\n const buildSQL = () =>\n reads.prefix +\n validKeys\n .map(key => key.map(c => `${id(c)}=?`).join(' AND '))\n .join(' OR ');\n const sql =\n keys === table.uniqueKeys && keys.length <= MAX_MEMOIZED_UNIQUE_KEYS\n ? getOrInsertComputed(reads.byUniqueKeys, mask, buildSQL)\n : buildSQL();\n const args: unknown[] = [];\n for (let i = 0; i < validKeys.length; i++) {\n const key = validKeys[i];\n for (let j = 0; j < key.length; j++) {\n args.push(row[key[j]]);\n }\n }\n const read = () =>\n this.db.statementCache.use(sql, cached => {\n cached.statement.safeIntegers(true);\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n const rows = cached.statement.all<any>(args);\n recordIndexUsage(cached.statement, table.name);\n return rows;\n });\n return cache\n ? cache.getOrRead(tag ?? '', sql, 'all', args, () => {\n const rows = read();\n for (const row of rows) {\n this.#assertNotAdvanced(row);\n }\n return rows;\n })\n : read();\n }\n\n /**\n * Rows are shared with other client groups via the SnapshotRowCache, so\n * one read from a connection that has advanced past this snapshot (i.e. a\n * {@link SnapshotDiff} used after its Snapshotter advanced) must fail\n * before it is cached, rather than be served to the valid Diffs of other\n * client groups (whose own validity checks would then fail).\n */\n #assertNotAdvanced(row: RowValue) {\n if ((row[ROW_VERSION] ?? '~') > this.version) {\n throw new InvalidDiffError(\n `Diff is no longer valid. db has advanced past ${this.version}.`,\n );\n }\n }\n\n /**\n * Whether the connection has been reset to a newer snapshot (i.e. via\n * {@link resetToHead()}), after which reads no longer reflect\n * {@link version}.\n */\n get reset(): boolean {\n return this.#reset;\n }\n\n resetToHead(): Snapshot {\n this.#reset = true;\n this.db.rollback();\n return new Snapshot(this.db, this.#appID);\n }\n}\n\nclass Diff implements SnapshotDiff {\n readonly #permissionsTable: string;\n readonly #syncableTables: Map<string, LiteAndZqlSpec>;\n readonly #allTableNames: Set<string>;\n readonly #observedTables: TableFilter | undefined;\n readonly #rowCache: SnapshotRowCache | undefined;\n #prevWrites: PrevWrites;\n readonly #prevTagUniform: string;\n readonly #prevTagDivergent: string;\n #iterated = false;\n readonly prev: Snapshot;\n readonly curr: Snapshot;\n readonly changes: number;\n changesScanned = 0;\n changesSkipped = 0;\n readonly stats: DiffTimingStats = {\n changeLogReadMs: 0,\n rowReadMs: 0,\n parseMs: 0,\n totalDiffMs: 0,\n };\n\n constructor(\n appID: string,\n syncableTables: Map<string, LiteAndZqlSpec>,\n allTableNames: Set<string>,\n prev: Snapshot,\n curr: Snapshot,\n observedTables?: TableFilter | undefined,\n rowCache?: SnapshotRowCache | undefined,\n prevWrites: PrevWrites = 'uniform',\n ) {\n this.#prevWrites = prevWrites;\n this.#permissionsTable = `${appID}.permissions`;\n this.#syncableTables = syncableTables;\n this.#allTableNames = allTableNames;\n this.#observedTables = observedTables;\n this.prev = prev;\n this.curr = curr;\n this.#prevTagUniform = `p:${prev.version}`;\n this.#prevTagDivergent = `p:${prev.version}:${curr.version}`;\n this.changes = curr.numChangesSince(prev.version);\n // Row values are only shared (via the cache) between Diffs that do not\n // straddle a table-wide op. This guarantees that any two Diffs that\n // encounter the same change log entry read the row from tables with\n // the same schema (and contents), since a RESET or TRUNCATE between\n // their `curr` versions would necessarily fall within the range of the\n // later Diff. (Such a Diff aborts with a ResetPipelinesSignal anyway.)\n this.#rowCache =\n rowCache && !curr.hasTableWideOpSince(prev.version)\n ? rowCache\n : undefined;\n }\n\n setPrevWrites(prevWrites: PrevWrites): void {\n assert(\n !this.#iterated || prevWrites === 'divergent',\n 'prevWrites must be set before iterating the diff',\n );\n this.#prevWrites = prevWrites;\n }\n\n [Symbol.iterator](): Iterator<Change> {\n this.#iterated = true;\n const {changes, cleanup: done} = this.curr.changesSince(this.prev.version);\n\n const cleanup = () => {\n try {\n // Allow open iterators to clean up their state.\n changes.return?.(undefined);\n } finally {\n done();\n }\n };\n\n return {\n next: () => {\n const nextStart = performance.now();\n try {\n for (;;) {\n const clStart = performance.now();\n const {value, done} = changes.next();\n this.stats.changeLogReadMs += performance.now() - clStart;\n if (done) {\n cleanup();\n return {value, done: true};\n }\n\n this.changesScanned++;\n const parseStart = performance.now();\n const {table, rowKey, op, stateVersion} = v.parse(value, schema);\n this.stats.parseMs += performance.now() - parseStart;\n if (op === RESET_OP) {\n // The current map of `TableSpec`s may not have the correct or complete information.\n throw new ResetPipelinesSignal(\n `schema for table ${table} has changed`,\n 'schema-change',\n );\n }\n if (op === TRUNCATE_OP) {\n // Truncates are also processed by rehydrating pipelines at current.\n throw new ResetPipelinesSignal(\n `table ${table} has been truncated`,\n 'truncation',\n );\n }\n if (\n this.#observedTables &&\n !this.#observedTables.has(table) &&\n table !== this.#permissionsTable\n ) {\n if (this.#allTableNames.has(table)) {\n this.changesSkipped++;\n continue; // skip change log entries for unobserved tables.\n }\n throw new Error(`change for unknown table ${table}`);\n }\n const specs = this.#syncableTables.get(table);\n if (!specs) {\n if (this.#allTableNames.has(table)) {\n this.changesSkipped++;\n continue; // skip change log entries for non-syncable tables.\n }\n throw new Error(`change for unknown table ${table}`);\n }\n const {tableSpec, zqlSpec} = specs;\n\n // Sanity check: All change log ops should have a stateVersion\n // greater than minRowVersion in the table metadata. This is a\n // mini-proof that the overlay does not need to be applied to\n // rows produced in incremental catchup, based on the invariant in\n // change-processor's #bumpVersions(), whereby the setting of the\n // minRowVersion is always followed by a RESET OP, meaning that\n // subsequent change-log traversal happens at a later version.\n assert(\n (tableSpec.minRowVersion ?? '') < stateVersion,\n () =>\n `unexpected change @${stateVersion} for table ${table} with ` +\n `minRowVersion ${tableSpec.minRowVersion}: ${op}(${rowKey})`,\n );\n\n assert(rowKey !== null, 'rowKey must be present for row changes');\n\n // Row reads are shared with the other client groups on the\n // worker via the SnapshotRowCache (if configured):\n //\n // * The new value of the row is read from `curr`, which is\n // never written to during the iteration, and is the version\n // of the row at `stateVersion` (as verified by\n // checkThatDiffIsValid()). It is thus identical for every\n // Diff that encounters the change log entry.\n //\n // * Previous values are read from `prev`, at `prev.version`,\n // which the caller (i.e. IVM) writes to as the iteration\n // progresses. For a table whose only unique key is its\n // primary key, no change applied before this one can affect\n // this row, so the result depends solely on `prev.version`.\n // Otherwise, the result can include rows that conflict on\n // other unique keys, which may have been added or removed by\n // preceding changes; see PrevWrites for when that can be\n // shared.\n // The first advance() after this Diff was created resets the\n // `prev` connection to head. Reads would then no longer reflect\n // `prev.version`, and cache hits would not reveal it (the cached\n // rows were read by valid Diffs), so this must be checked\n // explicitly rather than inferred from the rows' versions.\n if (this.prev.reset) {\n throw new InvalidDiffError(\n `Diff is no longer valid. prev db has advanced past ${this.prev.version}.`,\n );\n }\n\n const cache = this.#rowCache;\n const prevDependsOnWrites =\n tableSpec.uniqueKeys.length > 1 && this.#prevWrites !== 'none';\n const prevCache =\n prevDependsOnWrites && this.#prevWrites === 'divergent'\n ? undefined\n : cache;\n const prevTag = prevDependsOnWrites\n ? this.#prevTagDivergent\n : this.#prevTagUniform;\n const rowStart = performance.now();\n const nextValue =\n op === SET_OP\n ? this.curr.getRow(\n tableSpec,\n rowKey,\n cache,\n `n:${stateVersion}`,\n )\n : null;\n let prevValues;\n if (nextValue) {\n prevValues = this.prev.getRows(\n tableSpec,\n tableSpec.uniqueKeys,\n nextValue,\n prevCache,\n prevTag,\n );\n } else {\n const prevValue = this.prev.getRow(\n tableSpec,\n rowKey,\n prevCache,\n prevTag,\n );\n prevValues = prevValue ? [prevValue] : [];\n }\n this.stats.rowReadMs += performance.now() - rowStart;\n if (nextValue === undefined) {\n throw new Error(\n `Missing value for ${table} ${stringify(rowKey)}`,\n );\n }\n // Sanity check detects if the diff is being accessed after the Snapshots have advanced.\n this.checkThatDiffIsValid(stateVersion, op, prevValues, nextValue);\n\n if (prevValues.length === 0 && nextValue === null) {\n // Filter out no-op changes (e.g. a delete of a row that does not exist in prev).\n // TODO: Consider doing this for deep-equal values.\n this.changesSkipped++;\n continue;\n }\n\n if (\n table === this.#permissionsTable &&\n prevValues.some(\n prevValue => prevValue.permissions !== nextValue.permissions,\n )\n ) {\n throw new ResetPipelinesSignal(\n `Permissions have changed ${\n prevValues.find(\n prevValue =>\n prevValue.permissions !== nextValue.permissions,\n ).hash\n } => ${nextValue.hash}`,\n 'permissions-change',\n );\n }\n\n // Note: The raw SQLite rows may be shared with other client\n // groups via the SnapshotRowCache, so they must not be modified.\n // fromSQLiteTypes() produces new row objects.\n // TODO Can we get rid of these RowValue casts?\n return {\n value: {\n table,\n prevValues: prevValues.map(prevValue =>\n fromSQLiteTypes(zqlSpec, prevValue, table),\n ),\n nextValue: nextValue\n ? fromSQLiteTypes(zqlSpec, nextValue, table)\n : null,\n rowKey,\n } satisfies Change,\n };\n }\n } catch (e) {\n // This control flow path is not covered by the return() method (i.e. `break`).\n cleanup();\n throw e;\n } finally {\n this.stats.totalDiffMs += performance.now() - nextStart;\n }\n },\n\n return: (value: unknown) => {\n cleanup();\n return {value, done: true};\n },\n };\n }\n\n checkThatDiffIsValid(\n stateVersion: string,\n op: string,\n prevValues: RowValue[],\n nextValue: RowValue,\n ) {\n // Sanity checks to detect that the diff is not being accessed after\n // the Snapshots have advanced.\n if (stateVersion > this.curr.version) {\n throw new InvalidDiffError(\n `Diff is no longer valid. curr db has advanced past ${this.curr.version}`,\n );\n }\n if (\n prevValues.findIndex(\n prevValue => (prevValue[ROW_VERSION] ?? '~') > this.prev.version,\n ) !== -1\n ) {\n throw new InvalidDiffError(\n `Diff is no longer valid. prev db has advanced past ${this.prev.version}.`,\n );\n }\n if (op === SET_OP && nextValue[ROW_VERSION] !== stateVersion) {\n throw new InvalidDiffError(\n 'Diff is no longer valid. curr db has advanced.',\n );\n }\n }\n}\n\nexport class InvalidDiffError extends Error {\n constructor(msg: string) {\n super(msg);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,IAAa,cAAb,MAAyB;CACvB;CACA;CACA;CACA;CACA;CACA;CACA;;;;;;;CAQA,YACE,IACA,QACA,EAAC,SACD,kBACA,UACA;EACA,KAAKA,MAAM;EACX,KAAKC,UAAU;EACf,KAAKC,SAAS;EACd,KAAKC,oBAAoB;EACzB,KAAKC,YAAY;CACnB;;;;;;CAOA,OAAa;EACX,OAAO,KAAKC,UAAU,KAAA,GAAW,qBAAqB;EACtD,KAAKA,QAAQ,SAAS,OACpB,KAAKL,KACL,KAAKC,SACL,KAAKC,QACL,KAAKC,iBACP;EACA,KAAKH,IAAI,QAAQ,+BAA+B,KAAKK,MAAM,SAAS;EACpE,OAAO;CACT;CAEA,cAAuB;EACrB,OAAO,KAAKA,UAAU,KAAA;CACxB;;CAGA,UAAoB;EAClB,OAAO,KAAKA,UAAU,KAAA,GAAW,sCAAsC;EACvE,OAAO,KAAKA;CACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CA,QACE,gBACA,eACA,gBACA,aAAyB,WACX;EACd,MAAM,EAAC,MAAM,SAAQ,KAAK,mBAAmB;EAC7C,OAAO,IAAI,KACT,KAAKH,QACL,gBACA,eACA,MACA,MACA,gBACA,KAAKE,WACL,UACF;CACF;CAEA,qBAAqB;EACnB,OAAO,KAAKC,UAAU,KAAA,GAAW,sCAAsC;EACvE,MAAM,OAAO,KAAKC,QACd,KAAKA,MAAM,YAAY,IACvB,SAAS,OACP,KAAKN,KACL,KAAKK,MAAM,GAAG,GAAG,MACjB,KAAKH,QACL,KAAKC,iBACP;EACJ,KAAKG,QAAQ,KAAKD;EAClB,KAAKA,QAAQ;EACb,OAAO;GAAC,MAAM,KAAKC;GAAO,MAAM,KAAKD;EAAK;CAC5C;;;;;CAMA,UAAU;EAMR,KAAKA,OAAO,GAAG,GAAG,MAAM,EAAC,UAAU,MAAK,CAAC;EACzC,KAAKC,OAAO,GAAG,GAAG,MAAM,EAAC,UAAU,MAAK,CAAC;EACzC,KAAKN,IAAI,QAAQ,6BAA6B;CAChD;AACF;AAqGA,IAAa,uBAAb,cAA0C,MAAM;CAC9C,OAAgB;CAChB;CACA;CAEA,YAAY,KAAa,QAA8B,WAAoB;EACzE,MAAM,GAAG;EACT,KAAK,SAAS;EACd,KAAK,YAAY;CACnB;AACF;AAoBA,IAAM,6BAAa,IAAI,QAAqD;AAE5E,SAAS,SAAS,OAAoD;CACpE,OAAO,oBAAoB,YAAY,cAAc;EACnD,QAAQ,UAAU,OAAO,KAAK,MAAM,OAAO,EACxC,KAAI,MAAK,GAAG,CAAC,CAAC,EACd,KAAK,GAAG,EAAE,QAAQ,GAAG,MAAM,IAAI,EAAE;EACpC,8BAAc,IAAI,IAAI;EACtB,8BAAc,IAAI,IAAI;CACxB,EAAE;AACJ;AAGA,IAAM,2BAA2B;;;;;;;;;;;;;;;;;AAkBjC,IAAM,iCAAiC;AAEvC,IAAM,WAAN,MAAM,SAAS;CACb,OAAO,OACL,IACA,QACA,OACA,kBACA;EACA,MAAM,OAAO,IAAI,SAAS,IAAI,MAAM;EACpC,KAAK,OAAO,mBAAmB;EAC/B,IAAI,qBAAqB,KAAA,GACvB,KAAK,OAAO,iBAAiB,kBAAkB;EAEjD,MAAM,CAAC,EAAC,cAAc,UAAS,KAAK,OAAO,cAAc;EAOzD,OACE,SAAS,QACT,6CAA6C,KAAK,EACpD;EAGA,OAAO,IAAI,SAAS,IADL,gBAAgB,IACX,GAAI,KAAK;CAC/B;CAEA;CACA;CACA;CACA,SAAS;CAET,YAAY,IAAqB,OAAe;EAC9C,GAAG,gBAAgB;EAInB,MAAM,EAAC,iBAAgB,oBAAoB,EAAE;EAE7C,KAAK,KAAK;EACV,KAAKE,SAAS;EACd,KAAK,UAAU;CACjB;CAEA,gBAAgB,aAAqB;EACnC,MAAM,EAAC,UAAS,KAAK,GAAG,IACtB,6EACA,WACF;EACA,OAAO;CACT;;;;;;CAOA,mBAAmB,aAA8B;EAO/C,OANY,KAAK,GAAG,IAClB,gDAAgD,+BAA+B;qDAE/E,aAAA,GAGK,MAAQ,KAAA;CACjB;;;;;;CAOA,oBAAoB,aAA8B;EAQhD,OAPY,KAAK,GAAG,IAClB,gDAAgD,+BAA+B;2DAE/E,aAAA,KAAA,GAIK,MAAQ,KAAA;CACjB;CAEA,aAAa,aAAqB;EAGhC,MAAM,SAAS,KAAK,GAAG,eAAe,IACpC;yEAEF;EACA,OAAO;GACL,SAAS,OAAO,UAAU,QAAQ,WAAW;GAC7C,eAAe,KAAK,GAAG,eAAe,OAAO,MAAM;EACrD;CACF;;;;;;;;;CAUA,OACE,OACA,QACA,OACA,KACA;EACA,MAAM,MAAM,mBAAmB,MAAgB;EAC/C,MAAM,OAAO,OAAO,KAAK,GAAG;EAC5B,MAAM,QAAQ,SAAS,KAAK;EAC5B,MAAM,MAAM,oBACV,MAAM,cACN,KAAK,KAAK,IAAI,SACR,MAAM,SAAS,KAAK,KAAI,MAAK,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAC/D;EACA,MAAM,OAAO,OAAO,OAAO,GAAG;EAC9B,MAAM,aACJ,KAAK,GAAG,eAAe,IAAI,MAAK,WAAU;GACxC,OAAO,UAAU,aAAa,IAAI;GAElC,MAAM,MAAM,OAAO,UAAU,IAAS,IAAI;GAC1C,iBAAiB,OAAO,WAAW,MAAM,IAAI;GAC7C,OAAO;EACT,CAAC;EACH,OAAO,QACH,MAAM,UAAU,OAAO,IAAI,KAAK,OAAO,YAAY;GACjD,MAAM,MAAM,KAAK;GACjB,IAAI,QAAQ,KAAA,GACV,KAAKK,mBAAmB,GAAG;GAE7B,OAAO;EACT,CAAC,IACD,KAAK;CACX;;;;;;;;;CAUA,QACE,OACA,MACA,KACA,OACA,KACA;EAOA,IAAI,YAA0B;EAC9B,IAAI,QAAQ,KAAK,KAAK,UAAU;EAEhC,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;GACpC,MAAM,MAAM,KAAK;GACjB,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;IACnC,MAAM,MAAM,IAAI,IAAI;IACpB,IAAI,QAAQ,QAAQ,QAAQ,KAAA,GAAW;KACrC,YAAY,CAAC;KACb,OAAO;KACP,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;MACpC,MAAM,YAAY,KAAK;MACvB,IACE,UAAU,OACR,QAAO,IAAI,SAAS,QAAQ,IAAI,SAAS,KAAA,CAC3C,GACA;OACA,UAAU,KAAK,SAAS;OACxB,QAAQ,KAAK;MACf;KACF;KACA;IACF;GACF;GACA,IAAI,cAAc,MAChB;EAEJ;EAEA,IAAI,UAAU,WAAW,GACvB,OAAO,CAAC;EAEV,MAAM,QAAQ,SAAS,KAAK;EAC5B,MAAM,iBACJ,MAAM,SACN,UACG,KAAI,QAAO,IAAI,KAAI,MAAK,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,EACnD,KAAK,MAAM;EAChB,MAAM,MACJ,SAAS,MAAM,cAAc,KAAK,UAAU,2BACxC,oBAAoB,MAAM,cAAc,MAAM,QAAQ,IACtD,SAAS;EACf,MAAM,OAAkB,CAAC;EACzB,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;GACzC,MAAM,MAAM,UAAU;GACtB,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAC9B,KAAK,KAAK,IAAI,IAAI,GAAG;EAEzB;EACA,MAAM,aACJ,KAAK,GAAG,eAAe,IAAI,MAAK,WAAU;GACxC,OAAO,UAAU,aAAa,IAAI;GAElC,MAAM,OAAO,OAAO,UAAU,IAAS,IAAI;GAC3C,iBAAiB,OAAO,WAAW,MAAM,IAAI;GAC7C,OAAO;EACT,CAAC;EACH,OAAO,QACH,MAAM,UAAU,OAAO,IAAI,KAAK,OAAO,YAAY;GACjD,MAAM,OAAO,KAAK;GAClB,KAAK,MAAM,OAAO,MAChB,KAAKA,mBAAmB,GAAG;GAE7B,OAAO;EACT,CAAC,IACD,KAAK;CACX;;;;;;;;CASA,mBAAmB,KAAe;EAChC,KAAK,IAAA,iBAAoB,OAAO,KAAK,SACnC,MAAM,IAAI,iBACR,iDAAiD,KAAK,QAAQ,EAChE;CAEJ;;;;;;CAOA,IAAI,QAAiB;EACnB,OAAO,KAAKC;CACd;CAEA,cAAwB;EACtB,KAAKA,SAAS;EACd,KAAK,GAAG,SAAS;EACjB,OAAO,IAAI,SAAS,KAAK,IAAI,KAAKN,MAAM;CAC1C;AACF;AAEA,IAAM,OAAN,MAAmC;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA,YAAY;CACZ;CACA;CACA;CACA,iBAAiB;CACjB,iBAAiB;CACjB,QAAkC;EAChC,iBAAiB;EACjB,WAAW;EACX,SAAS;EACT,aAAa;CACf;CAEA,YACE,OACA,gBACA,eACA,MACA,MACA,gBACA,UACA,aAAyB,WACzB;EACA,KAAKa,cAAc;EACnB,KAAKN,oBAAoB,GAAG,MAAM;EAClC,KAAKC,kBAAkB;EACvB,KAAKC,iBAAiB;EACtB,KAAKC,kBAAkB;EACvB,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAKC,kBAAkB,KAAK,KAAK;EACjC,KAAKC,oBAAoB,KAAK,KAAK,QAAQ,GAAG,KAAK;EACnD,KAAK,UAAU,KAAK,gBAAgB,KAAK,OAAO;EAOhD,KAAKV,YACH,YAAY,CAAC,KAAK,oBAAoB,KAAK,OAAO,IAC9C,WACA,KAAA;CACR;CAEA,cAAc,YAA8B;EAC1C,OACE,CAAC,KAAKY,aAAa,eAAe,aAClC,kDACF;EACA,KAAKD,cAAc;CACrB;CAEA,CAAC,OAAO,YAA8B;EACpC,KAAKC,YAAY;EACjB,MAAM,EAAC,SAAS,SAAS,SAAQ,KAAK,KAAK,aAAa,KAAK,KAAK,OAAO;EAEzE,MAAM,gBAAgB;GACpB,IAAI;IAEF,QAAQ,SAAS,KAAA,CAAS;GAC5B,UAAU;IACR,KAAK;GACP;EACF;EAEA,OAAO;GACL,YAAY;IACV,MAAM,YAAY,YAAY,IAAI;IAClC,IAAI;KACF,SAAS;MACP,MAAM,UAAU,YAAY,IAAI;MAChC,MAAM,EAAC,OAAO,SAAQ,QAAQ,KAAK;MACnC,KAAK,MAAM,mBAAmB,YAAY,IAAI,IAAI;MAClD,IAAI,MAAM;OACR,QAAQ;OACR,OAAO;QAAC;QAAO,MAAM;OAAI;MAC3B;MAEA,KAAK;MACL,MAAM,aAAa,YAAY,IAAI;MACnC,MAAM,EAAC,OAAO,QAAQ,IAAI,iBAAgB,MAAQ,OAAO,oBAAM;MAC/D,KAAK,MAAM,WAAW,YAAY,IAAI,IAAI;MAC1C,IAAI,OAAA,KAEF,MAAM,IAAI,qBACR,oBAAoB,MAAM,eAC1B,eACF;MAEF,IAAI,OAAA,KAEF,MAAM,IAAI,qBACR,SAAS,MAAM,sBACf,YACF;MAEF,IACE,KAAKJ,mBACL,CAAC,KAAKA,gBAAgB,IAAI,KAAK,KAC/B,UAAU,KAAKH,mBACf;OACA,IAAI,KAAKE,eAAe,IAAI,KAAK,GAAG;QAClC,KAAK;QACL;OACF;OACA,MAAM,IAAI,MAAM,4BAA4B,OAAO;MACrD;MACA,MAAM,QAAQ,KAAKD,gBAAgB,IAAI,KAAK;MAC5C,IAAI,CAAC,OAAO;OACV,IAAI,KAAKC,eAAe,IAAI,KAAK,GAAG;QAClC,KAAK;QACL;OACF;OACA,MAAM,IAAI,MAAM,4BAA4B,OAAO;MACrD;MACA,MAAM,EAAC,WAAW,YAAW;MAS7B,QACG,UAAU,iBAAiB,MAAM,oBAEhC,sBAAsB,aAAa,aAAa,MAAM,sBACrC,UAAU,cAAc,IAAI,GAAG,GAAG,OAAO,EAC9D;MAEA,OAAO,WAAW,MAAM,wCAAwC;MAyBhE,IAAI,KAAK,KAAK,OACZ,MAAM,IAAI,iBACR,sDAAsD,KAAK,KAAK,QAAQ,EAC1E;MAGF,MAAM,QAAQ,KAAKP;MACnB,MAAM,sBACJ,UAAU,WAAW,SAAS,KAAK,KAAKW,gBAAgB;MAC1D,MAAM,YACJ,uBAAuB,KAAKA,gBAAgB,cACxC,KAAA,IACA;MACN,MAAM,UAAU,sBACZ,KAAKD,oBACL,KAAKD;MACT,MAAM,WAAW,YAAY,IAAI;MACjC,MAAM,YACJ,OAAA,MACI,KAAK,KAAK,OACR,WACA,QACA,OACA,KAAK,cACP,IACA;MACN,IAAI;MACJ,IAAI,WACF,aAAa,KAAK,KAAK,QACrB,WACA,UAAU,YACV,WACA,WACA,OACF;WACK;OACL,MAAM,YAAY,KAAK,KAAK,OAC1B,WACA,QACA,WACA,OACF;OACA,aAAa,YAAY,CAAC,SAAS,IAAI,CAAC;MAC1C;MACA,KAAK,MAAM,aAAa,YAAY,IAAI,IAAI;MAC5C,IAAI,cAAc,KAAA,GAChB,MAAM,IAAI,MACR,qBAAqB,MAAM,GAAG,UAAU,MAAM,GAChD;MAGF,KAAK,qBAAqB,cAAc,IAAI,YAAY,SAAS;MAEjE,IAAI,WAAW,WAAW,KAAK,cAAc,MAAM;OAGjD,KAAK;OACL;MACF;MAEA,IACE,UAAU,KAAKJ,qBACf,WAAW,MACT,cAAa,UAAU,gBAAgB,UAAU,WACnD,GAEA,MAAM,IAAI,qBACR,4BACE,WAAW,MACT,cACE,UAAU,gBAAgB,UAAU,WACxC,EAAE,KACH,MAAM,UAAU,QACjB,oBACF;MAOF,OAAO,EACL,OAAO;OACL;OACA,YAAY,WAAW,KAAI,cACzB,gBAAgB,SAAS,WAAW,KAAK,CAC3C;OACA,WAAW,YACP,gBAAgB,SAAS,WAAW,KAAK,IACzC;OACJ;MACF,EACF;KACF;IACF,SAAS,GAAG;KAEV,QAAQ;KACR,MAAM;IACR,UAAU;KACR,KAAK,MAAM,eAAe,YAAY,IAAI,IAAI;IAChD;GACF;GAEA,SAAS,UAAmB;IAC1B,QAAQ;IACR,OAAO;KAAC;KAAO,MAAM;IAAI;GAC3B;EACF;CACF;CAEA,qBACE,cACA,IACA,YACA,WACA;EAGA,IAAI,eAAe,KAAK,KAAK,SAC3B,MAAM,IAAI,iBACR,sDAAsD,KAAK,KAAK,SAClE;EAEF,IACE,WAAW,WACT,eAAc,UAAA,iBAA0B,OAAO,KAAK,KAAK,OAC3D,MAAM,IAEN,MAAM,IAAI,iBACR,sDAAsD,KAAK,KAAK,QAAQ,EAC1E;EAEF,IAAI,OAAA,OAAiB,UAAA,kBAA2B,cAC9C,MAAM,IAAI,iBACR,gDACF;CAEJ;AACF;AAEA,IAAa,mBAAb,cAAsC,MAAM;CAC1C,YAAY,KAAa;EACvB,MAAM,GAAG;CACX;AACF"}
|