@rocicorp/zero 1.10.0-canary.22 → 1.10.0-canary.23
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/analyze-query/src/bin-analyze.js +1 -1
- package/out/analyze-query/src/bin-analyze.js.map +1 -1
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +8 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +28 -1
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +11 -3
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/inspector-delegate.d.ts +18 -2
- package/out/zero-cache/src/server/inspector-delegate.d.ts.map +1 -1
- package/out/zero-cache/src/server/inspector-delegate.js +20 -7
- package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
- package/out/zero-cache/src/server/main.d.ts.map +1 -1
- package/out/zero-cache/src/server/main.js +8 -1
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/mutator.js +2 -0
- package/out/zero-cache/src/server/mutator.js.map +1 -1
- package/out/zero-cache/src/server/reaper.d.ts.map +1 -1
- package/out/zero-cache/src/server/reaper.js +2 -0
- package/out/zero-cache/src/server/reaper.js.map +1 -1
- package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/server/replicator.js +2 -0
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/runner/zero-dispatcher.d.ts.map +1 -1
- package/out/zero-cache/src/server/runner/zero-dispatcher.js +3 -0
- package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
- package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/server/syncer.js +2 -0
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/services/analyze.d.ts.map +1 -1
- package/out/zero-cache/src/services/analyze.js +3 -2
- package/out/zero-cache/src/services/analyze.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/backfill-manager.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/backfill-manager.js +14 -1
- package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js +15 -3
- package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.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/initial-sync.js +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +3 -0
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +6 -4
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.d.ts +5 -0
- package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +7 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js +2 -1
- package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
- package/out/zero-cache/src/services/profz.d.ts +7 -0
- package/out/zero-cache/src/services/profz.d.ts.map +1 -0
- package/out/zero-cache/src/services/profz.js +103 -0
- package/out/zero-cache/src/services/profz.js.map +1 -0
- package/out/zero-cache/src/services/run-ast.d.ts +12 -0
- package/out/zero-cache/src/services/run-ast.d.ts.map +1 -1
- package/out/zero-cache/src/services/run-ast.js +26 -14
- package/out/zero-cache/src/services/run-ast.js.map +1 -1
- package/out/zero-cache/src/services/runner.d.ts.map +1 -1
- package/out/zero-cache/src/services/runner.js +1 -1
- package/out/zero-cache/src/services/runner.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts +45 -0
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +133 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/drain-coordinator.d.ts +14 -1
- package/out/zero-cache/src/services/view-syncer/drain-coordinator.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/drain-coordinator.js +30 -4
- package/out/zero-cache/src/services/view-syncer/drain-coordinator.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/hydration-budget.d.ts +33 -0
- package/out/zero-cache/src/services/view-syncer/hydration-budget.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/hydration-budget.js +62 -0
- package/out/zero-cache/src/services/view-syncer/hydration-budget.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.d.ts +51 -0
- package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js +78 -0
- package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts +1 -1
- 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 +2 -2
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +2 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +27 -8
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +4 -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 +9 -3
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +11 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +470 -90
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/processes.d.ts +32 -0
- package/out/zero-cache/src/types/processes.d.ts.map +1 -1
- package/out/zero-cache/src/types/processes.js +43 -4
- package/out/zero-cache/src/types/processes.js.map +1 -1
- package/out/zero-cache/src/types/profiler.d.ts +12 -0
- package/out/zero-cache/src/types/profiler.d.ts.map +1 -1
- package/out/zero-cache/src/types/profiler.js +46 -1
- package/out/zero-cache/src/types/profiler.js.map +1 -1
- package/out/zero-cache/src/workers/connection.js +3 -3
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js +21 -16
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +10 -1
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/context.d.ts +16 -3
- package/out/zero-client/src/client/context.d.ts.map +1 -1
- package/out/zero-client/src/client/context.js +85 -14
- package/out/zero-client/src/client/context.js.map +1 -1
- package/out/zero-client/src/client/ivm-branch.d.ts.map +1 -1
- package/out/zero-client/src/client/ivm-branch.js +20 -3
- package/out/zero-client/src/client/ivm-branch.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero-rep.d.ts.map +1 -1
- package/out/zero-client/src/client/zero-rep.js +1 -1
- package/out/zero-client/src/client/zero-rep.js.map +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +2 -2
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-protocol/src/error.d.ts +1 -1
- package/out/zero-solid/src/solid-view.d.ts +7 -0
- package/out/zero-solid/src/solid-view.d.ts.map +1 -1
- package/out/zero-solid/src/solid-view.js +13 -2
- package/out/zero-solid/src/solid-view.js.map +1 -1
- package/out/zql/src/builder/debug-delegate.d.ts +6 -1
- package/out/zql/src/builder/debug-delegate.d.ts.map +1 -1
- package/out/zql/src/builder/debug-delegate.js +14 -2
- package/out/zql/src/builder/debug-delegate.js.map +1 -1
- package/out/zql/src/ivm/array-view.d.ts +19 -0
- package/out/zql/src/ivm/array-view.d.ts.map +1 -1
- package/out/zql/src/ivm/array-view.js +39 -2
- package/out/zql/src/ivm/array-view.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts +13 -1
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +26 -0
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/view.d.ts +15 -0
- package/out/zql/src/ivm/view.d.ts.map +1 -1
- package/out/zql/src/query/query-delegate-base.d.ts +2 -2
- package/out/zql/src/query/query-delegate-base.d.ts.map +1 -1
- package/out/zql/src/query/query-delegate-base.js +16 -10
- package/out/zql/src/query/query-delegate-base.js.map +1 -1
- package/out/zql/src/query/query-delegate.d.ts +22 -5
- package/out/zql/src/query/query-delegate.d.ts.map +1 -1
- package/out/zql/src/query/query.d.ts +4 -1
- package/out/zql/src/query/query.d.ts.map +1 -1
- package/out/zqlite/src/db.d.ts +9 -1
- package/out/zqlite/src/db.d.ts.map +1 -1
- package/out/zqlite/src/db.js +8 -2
- package/out/zqlite/src/db.js.map +1 -1
- package/out/zqlite/src/table-source.d.ts +1 -0
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +3 -0
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutagen.js","names":["#lc","#upstream","#shard","#stopped","#replica","#writeAuthorizer","#limiter","#crudMutations","#isStopped","#refCount"],"sources":["../../../../../../zero-cache/src/services/mutagen/mutagen.ts"],"sourcesContent":["import {PG_SERIALIZATION_FAILURE} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport type {JWTPayload} from 'jose';\nimport postgres from 'postgres';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {\n isProtocolError,\n ProtocolError,\n} from '../../../../zero-protocol/src/error.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {\n type CRUDMutation,\n type DeleteOp,\n type InsertOp,\n type Mutation,\n type UpdateOp,\n type UpsertOp,\n} from '../../../../zero-protocol/src/mutation.ts';\nimport {\n primaryKeyValueSchema,\n type PrimaryKeyValue,\n} from '../../../../zero-protocol/src/primary-key.ts';\nimport type {DatabaseStorage} from '../../../../zqlite/src/database-storage.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {\n WriteAuthorizerImpl,\n type WriteAuthorizer,\n} from '../../auth/write-authorizer.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport type {PostgresDB, PostgresTransaction} from '../../types/pg.ts';\nimport {upstreamSchema, type ShardID} from '../../types/shards.ts';\nimport {SlidingWindowLimiter} from '../limiter/sliding-window-limiter.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport {MutationAlreadyProcessedError} from './error.ts';\n\n// An error encountered processing a mutation.\n// Returned back to application for display to user.\nexport type MutationError = [\n kind: ErrorKind.MutationFailed | ErrorKind.MutationRateLimited,\n desc: string,\n];\n\nexport interface Mutagen extends RefCountedService {\n processMutation(\n mutation: Mutation,\n authData: JWTPayload | undefined,\n customMutatorsEnabled: boolean,\n ): Promise<MutationError | undefined>;\n}\n\nexport class MutagenService implements Mutagen, Service {\n readonly id: string;\n readonly #lc: LogContext;\n readonly #upstream: PostgresDB;\n readonly #shard: ShardID;\n readonly #stopped = resolver();\n readonly #replica: Database;\n readonly #writeAuthorizer: WriteAuthorizerImpl;\n readonly #limiter: SlidingWindowLimiter | undefined;\n #refCount = 0;\n #isStopped = false;\n\n readonly #crudMutations = getOrCreateCounter(\n 'mutation',\n 'crud',\n 'Number of CRUD mutations processed',\n );\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n clientGroupID: string,\n upstream: PostgresDB,\n config: ZeroConfig,\n writeAuthzStorage: DatabaseStorage,\n ) {\n this.id = clientGroupID;\n this.#lc = lc;\n this.#upstream = upstream;\n this.#shard = shard;\n this.#replica = new Database(this.#lc, config.replica.file, {\n fileMustExist: true,\n });\n this.#writeAuthorizer = new WriteAuthorizerImpl(\n this.#lc,\n config,\n this.#replica,\n shard.appID,\n clientGroupID,\n writeAuthzStorage,\n );\n\n if (config.perUserMutationLimit.max !== undefined) {\n this.#limiter = new SlidingWindowLimiter(\n config.perUserMutationLimit.windowMs,\n config.perUserMutationLimit.max,\n );\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'MutagenService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'MutagenService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n processMutation(\n mutation: Mutation,\n authData: JWTPayload | undefined,\n customMutatorsEnabled = false,\n ): Promise<MutationError | undefined> {\n if (this.#limiter?.canDo() === false) {\n return Promise.resolve([\n ErrorKind.MutationRateLimited,\n 'Rate limit exceeded',\n ]);\n }\n this.#crudMutations.add(1, {\n clientGroupID: this.id,\n });\n return processMutation(\n this.#lc,\n authData,\n this.#upstream,\n this.#shard,\n this.id,\n mutation,\n this.#writeAuthorizer,\n undefined,\n customMutatorsEnabled,\n );\n }\n\n run(): Promise<void> {\n return this.#stopped.promise;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return this.#stopped.promise;\n }\n this.#writeAuthorizer.destroy();\n this.#isStopped = true;\n this.#stopped.resolve();\n return this.#stopped.promise;\n }\n}\n\nconst MAX_SERIALIZATION_ATTEMPTS = 10;\n\nexport async function processMutation(\n lc: LogContext,\n authData: JWTPayload | undefined,\n db: PostgresDB,\n shard: ShardID,\n clientGroupID: string,\n mutation: Mutation,\n writeAuthorizer: WriteAuthorizer,\n onTxStart?: () => void | Promise<void>, // for testing\n customMutatorsEnabled = false,\n): Promise<MutationError | undefined> {\n assert(\n mutation.type === MutationType.CRUD,\n 'Only CRUD mutations are supported',\n );\n lc = lc.withContext('mutationID', mutation.id);\n lc = lc.withContext('processMutation');\n lc.debug?.('Process mutation start', mutation);\n\n // Record mutation processing attempt for telemetry (regardless of success/failure)\n recordMutation('crud');\n\n let result: MutationError | undefined;\n\n const start = Date.now();\n try {\n // Mutations can fail for a variety of reasons:\n //\n // - application error\n // - network/db error\n // - zero bug\n //\n // For application errors what we want is to re-run the mutation in\n // \"error mode\", which skips the actual mutation and just updates the\n // lastMutationID. Then return the error to the app.\n //\n // However, it's hard to tell the difference between application errors\n // and the other types.\n //\n // A reasonable policy ends up being to just retry every mutation once\n // in error mode. If the error mode mutation succeeds then we assume it\n // was an application error and return the error to the app. Otherwise,\n // we know it was something internal and we log it.\n //\n // This is not 100% correct - there are theoretical cases where we\n // return an internal error to the app that shouldn't have been. But it\n // would have to be a crazy coincidence: we'd have to have a network\n // error on the first attempt that resolves by the second attempt.\n //\n // One might ask why not try/catch just the calls to the mutators and\n // consider those application errors. That is actually what we do in\n // Replicache:\n //\n // https://github.com/rocicorp/todo-row-versioning/blob/9a0a79dc2d2de32c4fac61b5d1634bd9a9e66b7c/server/src/push.ts#L131\n //\n // We don't do it here because:\n //\n // 1. It's still not perfect. It's hard to isolate SQL errors in\n // mutators due to app developer mistakes from SQL errors due to\n // Zero mistakes.\n // 2. It's not possible to do this with the pg library we're using in\n // Zero anyway: https://github.com/porsager/postgres/issues/455.\n //\n // Personally I think this simple retry policy is nice.\n let errorMode = false;\n for (let i = 0; i < MAX_SERIALIZATION_ATTEMPTS; i++) {\n try {\n await runTx(\n db,\n async tx => {\n // Simulates a concurrent request for testing. In production this is a noop.\n const done = onTxStart?.();\n try {\n return await processMutationWithTx(\n lc,\n tx,\n authData,\n shard,\n clientGroupID,\n mutation,\n errorMode,\n writeAuthorizer,\n );\n } finally {\n await done;\n }\n },\n {mode: Mode.SERIALIZABLE},\n );\n if (errorMode) {\n lc.debug?.('Ran mutation successfully in error mode');\n }\n break;\n } catch (e) {\n if (e instanceof MutationAlreadyProcessedError) {\n lc.debug?.(e.message);\n // Don't double-count already processed mutations, but they were counted above\n return undefined;\n }\n if (\n isProtocolError(e) &&\n !errorMode &&\n e.kind === ErrorKind.InvalidPush &&\n customMutatorsEnabled &&\n i < 2\n ) {\n // We're temporarily supporting custom mutators AND CRUD mutators at the same time.\n // This can create a lot of OOO mutation errors since we do not know when the API server\n // has applied a custom mutation before moving on to process CRUD mutations.\n // The temporary workaround (since CRUD is being deprecated) is to retry the mutation\n // after a small delay. Users are not expected to be running both CRUD and Custom mutators.\n // They should migrate completely to custom mutators.\n lc.info?.(\n 'Both CRUD and Custom mutators are being used at once. This is supported for now but IS NOT RECOMMENDED. Migrate completely to custom mutators.',\n e,\n );\n await new Promise(resolve => setTimeout(resolve, 100));\n continue;\n }\n if (isProtocolError(e) || errorMode) {\n lc.error?.('Process mutation error', e);\n throw e;\n }\n if (\n e instanceof postgres.PostgresError &&\n e.code === PG_SERIALIZATION_FAILURE\n ) {\n lc.info?.(`attempt ${i + 1}: ${String(e)}`, e);\n continue; // Retry up to MAX_SERIALIZATION_ATTEMPTS.\n }\n result = [ErrorKind.MutationFailed, String(e)];\n if (errorMode) {\n break;\n }\n lc.error?.('Got error running mutation, re-running in error mode', e);\n errorMode = true;\n i--;\n }\n }\n } finally {\n lc.debug?.('Process mutation complete in', Date.now() - start);\n }\n return result;\n}\n\nexport async function processMutationWithTx(\n lc: LogContext,\n tx: PostgresTransaction,\n authData: JWTPayload | undefined,\n shard: ShardID,\n clientGroupID: string,\n mutation: CRUDMutation,\n errorMode: boolean,\n authorizer: WriteAuthorizer,\n) {\n const tasks: (() => Promise<unknown>)[] = [];\n\n async function execute(stmt: postgres.PendingQuery<postgres.Row[]>) {\n try {\n return await stmt.execute();\n } finally {\n const q = stmt as unknown as Query;\n // `q.parameters` are the bound row values -- log the parameterized\n // statement and the parameter count, never the values themselves.\n lc.debug?.(`${q.string} (${q.parameters.length} params)`);\n }\n }\n\n authorizer.reloadPermissions();\n\n if (!errorMode) {\n const {ops} = mutation.args[0];\n\n // Validate all table names before processing\n authorizer.validateTableNames(ops);\n\n const normalizedOps = authorizer.normalizeOps(ops);\n const [canPre, canPost] = await Promise.all([\n authorizer.canPreMutation(authData, normalizedOps),\n authorizer.canPostMutation(authData, normalizedOps),\n ]);\n if (canPre && canPost) {\n for (const op of ops) {\n switch (op.op) {\n case 'insert':\n tasks.push(() => execute(getInsertSQL(tx, op)));\n break;\n case 'upsert':\n tasks.push(() => execute(getUpsertSQL(tx, op)));\n break;\n case 'update':\n tasks.push(() => execute(getUpdateSQL(tx, op)));\n break;\n case 'delete':\n tasks.push(() => execute(getDeleteSQL(tx, op)));\n break;\n default:\n unreachable(op);\n }\n }\n }\n }\n\n // Confirm the mutation even though it may have been blocked by the authorizer.\n // Authorizer blocking a mutation is not an error but the correct result of the mutation.\n tasks.unshift(() =>\n checkSchemaVersionAndIncrementLastMutationID(\n tx,\n shard,\n clientGroupID,\n mutation.clientID,\n mutation.id,\n ),\n );\n\n // Note: An error thrown from any Promise aborts the entire transaction.\n await Promise.all(tasks.map(task => task()));\n}\n\nexport function getInsertSQL(\n tx: postgres.TransactionSql,\n create: InsertOp,\n): postgres.PendingQuery<postgres.Row[]> {\n return tx`\n INSERT INTO ${tx(create.tableName)} ${tx(create.value)}\n ON CONFLICT (${tx(create.primaryKey)}) DO NOTHING\n `;\n}\n\nexport function getUpsertSQL(\n tx: postgres.TransactionSql,\n set: UpsertOp,\n): postgres.PendingQuery<postgres.Row[]> {\n const {tableName, primaryKey, value} = set;\n return tx`\n INSERT INTO ${tx(tableName)} ${tx(value)}\n ON CONFLICT (${tx(primaryKey)})\n DO UPDATE SET ${tx(value)}\n `;\n}\n\nfunction getUpdateSQL(\n tx: postgres.TransactionSql,\n update: UpdateOp,\n): postgres.PendingQuery<postgres.Row[]> {\n const table = update.tableName;\n const {primaryKey, value} = update;\n const id: Record<string, PrimaryKeyValue> = {};\n for (const key of primaryKey) {\n id[key] = v.parse(value[key], primaryKeyValueSchema);\n }\n return tx`UPDATE ${tx(table)} SET ${tx(value)} WHERE ${Object.entries(\n id,\n ).flatMap(([key, value], i) =>\n i ? [tx`AND`, tx`${tx(key)} = ${value}`] : tx`${tx(key)} = ${value}`,\n )}`;\n}\n\nfunction getDeleteSQL(\n tx: postgres.TransactionSql,\n deleteOp: DeleteOp,\n): postgres.PendingQuery<postgres.Row[]> {\n const {tableName, primaryKey, value} = deleteOp;\n\n const conditions = [];\n for (const key of primaryKey) {\n if (conditions.length > 0) {\n conditions.push(tx`AND`);\n }\n conditions.push(tx`${tx(key)} = ${value[key]}`);\n }\n\n return tx`DELETE FROM ${tx(tableName)} WHERE ${conditions}`;\n}\n\nasync function checkSchemaVersionAndIncrementLastMutationID(\n tx: PostgresTransaction,\n shard: ShardID,\n clientGroupID: string,\n clientID: string,\n receivedMutationID: number,\n) {\n const [{lastMutationID}] = await tx<{lastMutationID: bigint}[]>`\n INSERT INTO ${tx(upstreamSchema(shard))}.clients \n as current (\"clientGroupID\", \"clientID\", \"lastMutationID\")\n VALUES (${clientGroupID}, ${clientID}, ${1})\n ON CONFLICT (\"clientGroupID\", \"clientID\")\n DO UPDATE SET \"lastMutationID\" = current.\"lastMutationID\" + 1\n RETURNING \"lastMutationID\"\n `;\n\n // ABORT if the resulting lastMutationID is not equal to the receivedMutationID.\n if (receivedMutationID < lastMutationID) {\n throw new MutationAlreadyProcessedError(\n clientID,\n receivedMutationID,\n lastMutationID,\n );\n } else if (receivedMutationID > lastMutationID) {\n throw new ProtocolError({\n kind: ErrorKind.InvalidPush,\n message: `Push contains unexpected mutation id ${receivedMutationID} for client ${clientID}. Expected mutation id ${lastMutationID.toString()}.`,\n origin: ErrorOrigin.ZeroCache,\n });\n }\n}\n\n// The slice of information from the Query object in Postgres.js that gets logged for debugging.\n// https://github.com/porsager/postgres/blob/f58cd4f3affd3e8ce8f53e42799672d86cd2c70b/src/connection.js#L219\ntype Query = {string: string; parameters: object[]};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mutagen.js","names":["#lc","#upstream","#shard","#stopped","#replica","#writeAuthorizer","#limiter","#crudMutations","#isStopped","#refCount"],"sources":["../../../../../../zero-cache/src/services/mutagen/mutagen.ts"],"sourcesContent":["import {PG_SERIALIZATION_FAILURE} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport type {JWTPayload} from 'jose';\nimport postgres from 'postgres';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {\n isProtocolError,\n ProtocolError,\n} from '../../../../zero-protocol/src/error.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {\n type CRUDMutation,\n type DeleteOp,\n type InsertOp,\n type Mutation,\n type UpdateOp,\n type UpsertOp,\n} from '../../../../zero-protocol/src/mutation.ts';\nimport {\n primaryKeyValueSchema,\n type PrimaryKeyValue,\n} from '../../../../zero-protocol/src/primary-key.ts';\nimport type {DatabaseStorage} from '../../../../zqlite/src/database-storage.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {\n WriteAuthorizerImpl,\n type WriteAuthorizer,\n} from '../../auth/write-authorizer.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport type {PostgresDB, PostgresTransaction} from '../../types/pg.ts';\nimport {upstreamSchema, type ShardID} from '../../types/shards.ts';\nimport {SlidingWindowLimiter} from '../limiter/sliding-window-limiter.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport {MutationAlreadyProcessedError} from './error.ts';\n\n// An error encountered processing a mutation.\n// Returned back to application for display to user.\nexport type MutationError = [\n kind: ErrorKind.MutationFailed | ErrorKind.MutationRateLimited,\n desc: string,\n];\n\nexport interface Mutagen extends RefCountedService {\n /**\n * Callers must hold a {@link ref} for the duration of a push (i.e. across\n * all of its mutations). The service stops, closing its replica handle,\n * when its ref count drops to zero.\n */\n processMutation(\n mutation: Mutation,\n authData: JWTPayload | undefined,\n customMutatorsEnabled: boolean,\n ): Promise<MutationError | undefined>;\n}\n\nexport class MutagenService implements Mutagen, Service {\n readonly id: string;\n readonly #lc: LogContext;\n readonly #upstream: PostgresDB;\n readonly #shard: ShardID;\n readonly #stopped = resolver();\n readonly #replica: Database;\n readonly #writeAuthorizer: WriteAuthorizerImpl;\n readonly #limiter: SlidingWindowLimiter | undefined;\n #refCount = 0;\n #isStopped = false;\n\n readonly #crudMutations = getOrCreateCounter(\n 'mutation',\n 'crud',\n 'Number of CRUD mutations processed',\n );\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n clientGroupID: string,\n upstream: PostgresDB,\n config: ZeroConfig,\n writeAuthzStorage: DatabaseStorage,\n ) {\n this.id = clientGroupID;\n this.#lc = lc;\n this.#upstream = upstream;\n this.#shard = shard;\n this.#replica = new Database(this.#lc, config.replica.file, {\n fileMustExist: true,\n });\n this.#writeAuthorizer = new WriteAuthorizerImpl(\n this.#lc,\n config,\n this.#replica,\n shard.appID,\n clientGroupID,\n writeAuthzStorage,\n );\n\n if (config.perUserMutationLimit.max !== undefined) {\n this.#limiter = new SlidingWindowLimiter(\n config.perUserMutationLimit.windowMs,\n config.perUserMutationLimit.max,\n );\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'MutagenService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'MutagenService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n processMutation(\n mutation: Mutation,\n authData: JWTPayload | undefined,\n customMutatorsEnabled = false,\n ): Promise<MutationError | undefined> {\n if (this.#limiter?.canDo() === false) {\n return Promise.resolve([\n ErrorKind.MutationRateLimited,\n 'Rate limit exceeded',\n ]);\n }\n this.#crudMutations.add(1, {\n clientGroupID: this.id,\n });\n return processMutation(\n this.#lc,\n authData,\n this.#upstream,\n this.#shard,\n this.id,\n mutation,\n this.#writeAuthorizer,\n undefined,\n customMutatorsEnabled,\n );\n }\n\n run(): Promise<void> {\n return this.#stopped.promise;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return this.#stopped.promise;\n }\n this.#isStopped = true;\n try {\n this.#writeAuthorizer.destroy();\n } catch (e) {\n this.#lc.error?.('error destroying write authorizer storage', e);\n } finally {\n // The replica's statistics are maintained by the replicator, so skip\n // the `PRAGMA optimize` that closing a writable handle would otherwise\n // run on every client group churn.\n this.#replica.close({optimize: false});\n }\n this.#stopped.resolve();\n return this.#stopped.promise;\n }\n}\n\nconst MAX_SERIALIZATION_ATTEMPTS = 10;\n\nexport async function processMutation(\n lc: LogContext,\n authData: JWTPayload | undefined,\n db: PostgresDB,\n shard: ShardID,\n clientGroupID: string,\n mutation: Mutation,\n writeAuthorizer: WriteAuthorizer,\n onTxStart?: () => void | Promise<void>, // for testing\n customMutatorsEnabled = false,\n): Promise<MutationError | undefined> {\n assert(\n mutation.type === MutationType.CRUD,\n 'Only CRUD mutations are supported',\n );\n lc = lc.withContext('mutationID', mutation.id);\n lc = lc.withContext('processMutation');\n lc.debug?.('Process mutation start', mutation);\n\n // Record mutation processing attempt for telemetry (regardless of success/failure)\n recordMutation('crud');\n\n let result: MutationError | undefined;\n\n const start = Date.now();\n try {\n // Mutations can fail for a variety of reasons:\n //\n // - application error\n // - network/db error\n // - zero bug\n //\n // For application errors what we want is to re-run the mutation in\n // \"error mode\", which skips the actual mutation and just updates the\n // lastMutationID. Then return the error to the app.\n //\n // However, it's hard to tell the difference between application errors\n // and the other types.\n //\n // A reasonable policy ends up being to just retry every mutation once\n // in error mode. If the error mode mutation succeeds then we assume it\n // was an application error and return the error to the app. Otherwise,\n // we know it was something internal and we log it.\n //\n // This is not 100% correct - there are theoretical cases where we\n // return an internal error to the app that shouldn't have been. But it\n // would have to be a crazy coincidence: we'd have to have a network\n // error on the first attempt that resolves by the second attempt.\n //\n // One might ask why not try/catch just the calls to the mutators and\n // consider those application errors. That is actually what we do in\n // Replicache:\n //\n // https://github.com/rocicorp/todo-row-versioning/blob/9a0a79dc2d2de32c4fac61b5d1634bd9a9e66b7c/server/src/push.ts#L131\n //\n // We don't do it here because:\n //\n // 1. It's still not perfect. It's hard to isolate SQL errors in\n // mutators due to app developer mistakes from SQL errors due to\n // Zero mistakes.\n // 2. It's not possible to do this with the pg library we're using in\n // Zero anyway: https://github.com/porsager/postgres/issues/455.\n //\n // Personally I think this simple retry policy is nice.\n let errorMode = false;\n for (let i = 0; i < MAX_SERIALIZATION_ATTEMPTS; i++) {\n try {\n await runTx(\n db,\n async tx => {\n // Simulates a concurrent request for testing. In production this is a noop.\n const done = onTxStart?.();\n try {\n return await processMutationWithTx(\n lc,\n tx,\n authData,\n shard,\n clientGroupID,\n mutation,\n errorMode,\n writeAuthorizer,\n );\n } finally {\n await done;\n }\n },\n {mode: Mode.SERIALIZABLE},\n );\n if (errorMode) {\n lc.debug?.('Ran mutation successfully in error mode');\n }\n break;\n } catch (e) {\n if (e instanceof MutationAlreadyProcessedError) {\n lc.debug?.(e.message);\n // Don't double-count already processed mutations, but they were counted above\n return undefined;\n }\n if (\n isProtocolError(e) &&\n !errorMode &&\n e.kind === ErrorKind.InvalidPush &&\n customMutatorsEnabled &&\n i < 2\n ) {\n // We're temporarily supporting custom mutators AND CRUD mutators at the same time.\n // This can create a lot of OOO mutation errors since we do not know when the API server\n // has applied a custom mutation before moving on to process CRUD mutations.\n // The temporary workaround (since CRUD is being deprecated) is to retry the mutation\n // after a small delay. Users are not expected to be running both CRUD and Custom mutators.\n // They should migrate completely to custom mutators.\n lc.info?.(\n 'Both CRUD and Custom mutators are being used at once. This is supported for now but IS NOT RECOMMENDED. Migrate completely to custom mutators.',\n e,\n );\n await new Promise(resolve => setTimeout(resolve, 100));\n continue;\n }\n if (isProtocolError(e) || errorMode) {\n lc.error?.('Process mutation error', e);\n throw e;\n }\n if (\n e instanceof postgres.PostgresError &&\n e.code === PG_SERIALIZATION_FAILURE\n ) {\n lc.info?.(`attempt ${i + 1}: ${String(e)}`, e);\n continue; // Retry up to MAX_SERIALIZATION_ATTEMPTS.\n }\n result = [ErrorKind.MutationFailed, String(e)];\n if (errorMode) {\n break;\n }\n lc.error?.('Got error running mutation, re-running in error mode', e);\n errorMode = true;\n i--;\n }\n }\n } finally {\n lc.debug?.('Process mutation complete in', Date.now() - start);\n }\n return result;\n}\n\nexport async function processMutationWithTx(\n lc: LogContext,\n tx: PostgresTransaction,\n authData: JWTPayload | undefined,\n shard: ShardID,\n clientGroupID: string,\n mutation: CRUDMutation,\n errorMode: boolean,\n authorizer: WriteAuthorizer,\n) {\n const tasks: (() => Promise<unknown>)[] = [];\n\n async function execute(stmt: postgres.PendingQuery<postgres.Row[]>) {\n try {\n return await stmt.execute();\n } finally {\n const q = stmt as unknown as Query;\n // `q.parameters` are the bound row values -- log the parameterized\n // statement and the parameter count, never the values themselves.\n lc.debug?.(`${q.string} (${q.parameters.length} params)`);\n }\n }\n\n authorizer.reloadPermissions();\n\n if (!errorMode) {\n const {ops} = mutation.args[0];\n\n // Validate all table names before processing\n authorizer.validateTableNames(ops);\n\n const normalizedOps = authorizer.normalizeOps(ops);\n const [canPre, canPost] = await Promise.all([\n authorizer.canPreMutation(authData, normalizedOps),\n authorizer.canPostMutation(authData, normalizedOps),\n ]);\n if (canPre && canPost) {\n for (const op of ops) {\n switch (op.op) {\n case 'insert':\n tasks.push(() => execute(getInsertSQL(tx, op)));\n break;\n case 'upsert':\n tasks.push(() => execute(getUpsertSQL(tx, op)));\n break;\n case 'update':\n tasks.push(() => execute(getUpdateSQL(tx, op)));\n break;\n case 'delete':\n tasks.push(() => execute(getDeleteSQL(tx, op)));\n break;\n default:\n unreachable(op);\n }\n }\n }\n }\n\n // Confirm the mutation even though it may have been blocked by the authorizer.\n // Authorizer blocking a mutation is not an error but the correct result of the mutation.\n tasks.unshift(() =>\n checkSchemaVersionAndIncrementLastMutationID(\n tx,\n shard,\n clientGroupID,\n mutation.clientID,\n mutation.id,\n ),\n );\n\n // Note: An error thrown from any Promise aborts the entire transaction.\n await Promise.all(tasks.map(task => task()));\n}\n\nexport function getInsertSQL(\n tx: postgres.TransactionSql,\n create: InsertOp,\n): postgres.PendingQuery<postgres.Row[]> {\n return tx`\n INSERT INTO ${tx(create.tableName)} ${tx(create.value)}\n ON CONFLICT (${tx(create.primaryKey)}) DO NOTHING\n `;\n}\n\nexport function getUpsertSQL(\n tx: postgres.TransactionSql,\n set: UpsertOp,\n): postgres.PendingQuery<postgres.Row[]> {\n const {tableName, primaryKey, value} = set;\n return tx`\n INSERT INTO ${tx(tableName)} ${tx(value)}\n ON CONFLICT (${tx(primaryKey)})\n DO UPDATE SET ${tx(value)}\n `;\n}\n\nfunction getUpdateSQL(\n tx: postgres.TransactionSql,\n update: UpdateOp,\n): postgres.PendingQuery<postgres.Row[]> {\n const table = update.tableName;\n const {primaryKey, value} = update;\n const id: Record<string, PrimaryKeyValue> = {};\n for (const key of primaryKey) {\n id[key] = v.parse(value[key], primaryKeyValueSchema);\n }\n return tx`UPDATE ${tx(table)} SET ${tx(value)} WHERE ${Object.entries(\n id,\n ).flatMap(([key, value], i) =>\n i ? [tx`AND`, tx`${tx(key)} = ${value}`] : tx`${tx(key)} = ${value}`,\n )}`;\n}\n\nfunction getDeleteSQL(\n tx: postgres.TransactionSql,\n deleteOp: DeleteOp,\n): postgres.PendingQuery<postgres.Row[]> {\n const {tableName, primaryKey, value} = deleteOp;\n\n const conditions = [];\n for (const key of primaryKey) {\n if (conditions.length > 0) {\n conditions.push(tx`AND`);\n }\n conditions.push(tx`${tx(key)} = ${value[key]}`);\n }\n\n return tx`DELETE FROM ${tx(tableName)} WHERE ${conditions}`;\n}\n\nasync function checkSchemaVersionAndIncrementLastMutationID(\n tx: PostgresTransaction,\n shard: ShardID,\n clientGroupID: string,\n clientID: string,\n receivedMutationID: number,\n) {\n const [{lastMutationID}] = await tx<{lastMutationID: bigint}[]>`\n INSERT INTO ${tx(upstreamSchema(shard))}.clients \n as current (\"clientGroupID\", \"clientID\", \"lastMutationID\")\n VALUES (${clientGroupID}, ${clientID}, ${1})\n ON CONFLICT (\"clientGroupID\", \"clientID\")\n DO UPDATE SET \"lastMutationID\" = current.\"lastMutationID\" + 1\n RETURNING \"lastMutationID\"\n `;\n\n // ABORT if the resulting lastMutationID is not equal to the receivedMutationID.\n if (receivedMutationID < lastMutationID) {\n throw new MutationAlreadyProcessedError(\n clientID,\n receivedMutationID,\n lastMutationID,\n );\n } else if (receivedMutationID > lastMutationID) {\n throw new ProtocolError({\n kind: ErrorKind.InvalidPush,\n message: `Push contains unexpected mutation id ${receivedMutationID} for client ${clientID}. Expected mutation id ${lastMutationID.toString()}.`,\n origin: ErrorOrigin.ZeroCache,\n });\n }\n}\n\n// The slice of information from the Query object in Postgres.js that gets logged for debugging.\n// https://github.com/porsager/postgres/blob/f58cd4f3affd3e8ce8f53e42799672d86cd2c70b/src/connection.js#L219\ntype Query = {string: string; parameters: object[]};\n"],"mappings":"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"}
|
|
@@ -55,7 +55,8 @@ var PusherService = class {
|
|
|
55
55
|
return this.#pusher.initConnection(selector);
|
|
56
56
|
}
|
|
57
57
|
enqueuePush(selector, push) {
|
|
58
|
-
this.#
|
|
58
|
+
const connCtx = this.#connContextManager.getConnectionContext(selector);
|
|
59
|
+
if (connCtx) this.#pusher.enqueuePush(connCtx, push);
|
|
59
60
|
return { type: "ok" };
|
|
60
61
|
}
|
|
61
62
|
async ackMutationResponses(requester, upToID) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pusher.js","names":["#connContextManager","#pusher","#queue","#config","#lc","#isStopped","#refCount","#stopped","#clients","#customMutations","#pushes","#processPush","#fanOutResponses","#failDownstream"],"sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"sourcesContent":["import {context, propagation, ROOT_CONTEXT} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {groupBy} from '../../../../shared/src/arrays.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {getErrorMessage} from '../../../../shared/src/error.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../../zero-protocol/src/error-reason.ts';\nimport {\n isProtocolError,\n type PushFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport {\n mutateResponseSchema,\n type MutateResponse,\n} from '../../../../zero-protocol/src/mutate-server.ts';\nimport type {MutationID} from '../../../../zero-protocol/src/mutation-id.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {CLEANUP_RESULTS_MUTATION_NAME} from '../../../../zero-protocol/src/mutation.ts';\nimport {type PushBody} from '../../../../zero-protocol/src/push.ts';\nimport {authEquals, isAuthErrorBody} from '../../auth/auth.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport {fetchFromAPIServer} from '../../custom/fetch.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {HandlerResult, StreamResult} from '../../workers/connection.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport type {\n ConnectionContext,\n ConnectionContextManager,\n ConnectionSelector,\n} from '../view-syncer/connection-context-manager.ts';\n\nexport interface Pusher extends RefCountedService {\n initConnection(selector: ConnectionSelector): Source<Downstream>;\n enqueuePush(selector: ConnectionSelector, push: PushBody): HandlerResult;\n ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void>;\n deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void>;\n}\n\ntype Config = Pick<ZeroConfig, 'app' | 'shard'>;\n\n/**\n * Receives push messages from zero-client and forwards\n * them the the user's API server.\n *\n * If the user's API server is taking too long to process\n * the push, the PusherService will add the push to a queue\n * and send pushes in bulk the next time the user's API server\n * is available.\n *\n * - One PusherService exists per client group.\n * - Mutations for a given client are always sent in-order\n * - Mutations for different clients in the same group may be interleaved\n */\nexport class PusherService implements Service, Pusher {\n readonly id: string;\n readonly #connContextManager: ConnectionContextManager;\n readonly #pusher: PushWorker;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #config: Config;\n readonly #lc: LogContext;\n #stopped: Promise<void> | undefined;\n #refCount = 0;\n #isStopped = false;\n\n constructor(\n appConfig: Config,\n lc: LogContext,\n clientGroupID: string,\n connContextManager: ConnectionContextManager,\n ) {\n this.#connContextManager = connContextManager;\n this.#config = appConfig;\n this.#lc = lc.withContext('component', 'pusherService');\n this.#queue = new Queue();\n this.#pusher = new PushWorker(\n appConfig,\n lc,\n this.#connContextManager,\n this.#queue,\n );\n this.id = clientGroupID;\n }\n\n initConnection(selector: ConnectionSelector) {\n return this.#pusher.initConnection(selector);\n }\n\n enqueuePush(\n selector: ConnectionSelector,\n push: PushBody,\n ): Exclude<HandlerResult, StreamResult> {\n this.#pusher.enqueuePush(\n this.#connContextManager.mustGetConnectionContext(selector),\n push,\n );\n\n return {\n type: 'ok',\n };\n }\n\n async ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void> {\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: upToID.clientID,\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'single',\n clientGroupID: this.id,\n clientID: upToID.clientID,\n upToMutationID: upToID.id,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-${this.id}-${upToID.clientID}-${upToID.id}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n {operation: 'cleanup', cleanupType: 'single'},\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n /**\n * Bulk cleanup is routed through the requester's push context.\n *\n * This assumes the client group shares a compatible push endpoint/auth\n * context.\n */\n async deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void> {\n if (clientIDs.length === 0) {\n return;\n }\n\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: clientIDs[0], // Use first client as sender\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'bulk',\n clientGroupID: this.id,\n clientIDs,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-bulk-${this.id}-${Date.now()}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n {operation: 'cleanup', cleanupType: 'bulk'},\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send bulk cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n run(): Promise<void> {\n this.#stopped = this.#pusher.run();\n return this.#stopped;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return must(this.#stopped, 'Stop was called before `run`');\n }\n this.#isStopped = true;\n this.#queue.enqueue('stop');\n return must(this.#stopped, 'Stop was called before `run`');\n }\n}\n\ntype PusherEntry = {\n push: PushBody;\n connCtx: ConnectionContext;\n};\ntype PusherEntryOrStop = PusherEntry | 'stop';\n\n/**\n * Awaits items in the queue then drains and sends them all\n * to the user's API server.\n */\nclass PushWorker {\n readonly #connContextManager: ConnectionContextManager;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #lc: LogContext;\n readonly #config: Config;\n readonly #clients: Map<\n string,\n {wsID: string; downstream: Subscription<Downstream>}\n >;\n\n readonly #customMutations = getOrCreateCounter(\n 'mutation',\n 'custom',\n 'Number of custom mutations processed',\n );\n readonly #pushes = getOrCreateCounter(\n 'mutation',\n 'pushes',\n 'Number of pushes processed by the pusher',\n );\n\n constructor(\n config: Config,\n lc: LogContext,\n connContextManager: ConnectionContextManager,\n queue: Queue<PusherEntryOrStop>,\n ) {\n this.#lc = lc.withContext('component', 'pusher');\n this.#connContextManager = connContextManager;\n this.#queue = queue;\n this.#config = config;\n this.#clients = new Map();\n }\n\n /**\n * Returns a new downstream stream if the clientID,wsID pair has not been seen before.\n * If a clientID already exists with a different wsID, that client's downstream is cancelled.\n */\n initConnection(selector: ConnectionSelector) {\n const existing = this.#clients.get(selector.clientID);\n if (existing && existing.wsID === selector.wsID) {\n // already initialized for this socket\n throw new Error('Connection was already initialized');\n }\n\n // client is back on a new connection\n if (existing) {\n existing.downstream.cancel();\n }\n\n const downstream = Subscription.create<Downstream>({\n cleanup: () => {\n this.#clients.delete(selector.clientID);\n },\n });\n this.#clients.set(selector.clientID, {\n wsID: selector.wsID,\n downstream,\n });\n return downstream;\n }\n\n enqueuePush(connCtx: ConnectionContext, push: PushBody) {\n this.#queue.enqueue({\n push,\n connCtx,\n });\n }\n\n async run() {\n for (;;) {\n const task = await this.#queue.dequeue();\n const rest = this.#queue.drain();\n const [pushes, terminate] = combinePushes([task, ...rest]);\n for (const push of pushes) {\n const parentContext = push.push.traceparent\n ? propagation.extract(ROOT_CONTEXT, {\n traceparent: push.push.traceparent,\n })\n : context.active();\n const response = await context.with(parentContext, () =>\n this.#processPush(push),\n );\n await this.#fanOutResponses(response);\n }\n\n if (terminate) {\n break;\n }\n }\n }\n\n /**\n * 1. If the entire `push` fails, we send the error to relevant clients.\n * 2. If the push succeeds, we look for any mutation failure that should cause the connection to terminate\n * and terminate the connection for those clients.\n */\n #fanOutResponses(response: MutateResponse) {\n const connectionTerminations: (() => void)[] = [];\n\n // if the entire push failed, send that to the client.\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a push error.',\n response,\n );\n // TODO(0xcadams): Fanout is keyed only by clientID here. If a response arrives\n // after reconnect or re-auth, `#clients.get(clientID)` may point at a\n // newer wsID/revision and fail the replacement downstream instead.\n const groupedMutationIDs = groupBy(\n response.mutationIDs ?? [],\n m => m.clientID,\n );\n for (const [clientID, mutationIDs] of groupedMutationIDs) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n // We do not resolve mutations on the client if the push fails\n // as those mutations will be retried.\n if ('error' in response) {\n // This error code path will eventually be removed when we\n // no longer support the legacy push error format.\n const pushFailedBody: PushFailedBody =\n response.error === 'http'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n status: response.status,\n bodyPreview: response.details,\n mutationIDs,\n message: `Fetch from API server returned non-OK status ${response.status}`,\n }\n : response.error === 'unsupportedPushVersion'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.UnsupportedPushVersion,\n mutationIDs,\n message: `Unsupported push version`,\n }\n : {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Internal,\n mutationIDs,\n message:\n response.error === 'zeroPusher'\n ? response.details\n : response.error === 'unsupportedSchemaVersion'\n ? 'Unsupported schema version'\n : 'An unknown error occurred while pushing to the API server',\n };\n\n this.#failDownstream(client.downstream, pushFailedBody);\n } else {\n this.#failDownstream(client.downstream, response);\n }\n }\n } else {\n // Look for mutations results that should cause us to terminate the connection\n // TODO(0xcadams): Same stale-routing issue as above: fatal mutation results are\n // still mapped to the current downstream by clientID only.\n const groupedMutations = groupBy(response.mutations, m => m.id.clientID);\n for (const [clientID, mutations] of groupedMutations) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n let failure: PushFailedBody | undefined;\n let i = 0;\n for (; i < mutations.length; i++) {\n const m = mutations[i];\n if ('error' in m.result) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a mutation error.',\n m.result,\n );\n }\n // This error code path will eventually be removed,\n // keeping this for backwards compatibility, but the server\n // should now return a PushFailedBody with the mutationIDs\n if ('error' in m.result && m.result.error === 'oooMutation') {\n failure = {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.OutOfOrderMutation,\n message: 'mutation was out of order',\n details: m.result.details,\n mutationIDs: mutations.map(m => ({\n clientID: m.id.clientID,\n id: m.id.id,\n })),\n };\n break;\n }\n }\n\n if (failure && i < mutations.length - 1) {\n this.#lc.warn?.(\n 'push-response contains mutations after a mutation which should fatal the connection',\n );\n }\n\n if (failure) {\n connectionTerminations.push(() =>\n this.#failDownstream(client.downstream, failure),\n );\n }\n }\n }\n\n connectionTerminations.forEach(cb => cb());\n }\n\n async #processPush(entry: PusherEntry): Promise<MutateResponse> {\n this.#customMutations.add(entry.push.mutations.length, {\n clientGroupID: entry.push.clientGroupID,\n });\n this.#pushes.add(1, {\n clientGroupID: entry.push.clientGroupID,\n });\n\n // Record custom mutations for telemetry\n recordMutation('custom', entry.push.mutations.length);\n\n const url = must(\n entry.connCtx.mutateContext.url,\n 'ZERO_MUTATE_URL is not set',\n );\n\n this.#lc.debug?.(\n 'pushing to',\n url,\n 'with',\n entry.push.mutations.length,\n 'mutations',\n );\n\n let mutationIDs: MutationID[] = [];\n\n try {\n mutationIDs = entry.push.mutations.map(m => ({\n id: m.id,\n clientID: m.clientID,\n }));\n\n const response = await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n entry.connCtx,\n {\n appID: this.#config.app.id,\n shardNum: this.#config.shard.num,\n },\n entry.push,\n {operation: 'mutate'},\n );\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n // A successful push also validates this connection's current auth snapshot.\n // That lets later shared work reuse it without trusting stale credentials.\n this.#connContextManager.validateConnection(\n entry.connCtx,\n entry.connCtx.revision,\n 'kind' in response &&\n response.kind === 'MutateResponse' &&\n response?.userID !== undefined\n ? {kind: 'server-validated', validatedUserID: response.userID}\n : {kind: 'client-fallback'},\n );\n return response;\n } catch (e) {\n if (isProtocolError(e) && e.errorBody.kind === ErrorKind.PushFailed) {\n const response = {\n ...e.errorBody,\n mutationIDs,\n } as const satisfies PushFailedBody;\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n\n if (isProtocolError(e) && isAuthErrorBody(e.errorBody)) {\n // The push completed far enough for local validation to reject the\n // connection, so invalidate it and surface the result as PushFailed.\n this.#lc.warn?.('Push validation failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: e.message,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n message: e.message,\n status: 401,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to push: ${getErrorMessage(e)}`,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n }\n\n #failDownstream(\n downstream: Subscription<Downstream>,\n errorBody: PushFailedBody,\n ): void {\n downstream.fail(new ProtocolErrorWithLevel(errorBody, 'warn'));\n }\n}\n\n/**\n * Pushes for different clients, sockets, or auth revisions could be interleaved.\n *\n * In order to batch safely, we only combine pushes from the same\n * clientID/wsID/revision snapshot.\n */\nexport function combinePushes(\n entries: readonly (PusherEntryOrStop | undefined)[],\n): [PusherEntry[], boolean] {\n const pushesByConnection = new Map<string, PusherEntry[]>();\n\n function collect() {\n const ret: PusherEntry[] = [];\n for (const entries of pushesByConnection.values()) {\n const composite: PusherEntry = {\n ...entries[0],\n push: {\n ...entries[0].push,\n mutations: [],\n },\n };\n ret.push(composite);\n for (const entry of entries) {\n assertAreCompatiblePushes(composite, entry);\n composite.push.mutations.push(...entry.push.mutations);\n }\n }\n return ret;\n }\n\n for (const entry of entries) {\n if (entry === 'stop' || entry === undefined) {\n return [collect(), true];\n }\n\n const key = `${entry.connCtx.clientID}:${entry.connCtx.wsID}:${entry.connCtx.revision}`;\n const existing = pushesByConnection.get(key);\n if (existing) {\n existing.push(entry);\n } else {\n pushesByConnection.set(key, [entry]);\n }\n }\n\n return [collect(), false] as const;\n}\n\n// These invariants should always be true for a given clientID.\n// If they are not, we have a bug in the code somewhere.\nfunction assertAreCompatiblePushes(left: PusherEntry, right: PusherEntry) {\n assert(\n left.connCtx.clientID === right.connCtx.clientID,\n 'clientID must be the same for all pushes',\n );\n assert(\n left.connCtx.wsID === right.connCtx.wsID,\n 'wsID must be the same for all pushes',\n );\n assert(\n left.connCtx.revision === right.connCtx.revision,\n 'revision must be the same for all pushes',\n );\n assert(\n authEquals(left.connCtx.auth, right.connCtx.auth),\n 'auth must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.schemaVersion === right.push.schemaVersion,\n 'schemaVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.pushVersion === right.push.pushVersion,\n 'pushVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.cookie ===\n right.connCtx.mutateContext.headerOptions.cookie,\n 'httpCookie must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.origin ===\n right.connCtx.mutateContext.headerOptions.origin,\n 'origin must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.user.id === right.connCtx.user.id,\n 'userID must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.url === right.connCtx.mutateContext.url,\n 'userPushURL must be the same for all pushes with the same clientID',\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pusher.js","names":["#connContextManager","#pusher","#queue","#config","#lc","#isStopped","#refCount","#stopped","#clients","#customMutations","#pushes","#processPush","#fanOutResponses","#failDownstream"],"sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"sourcesContent":["import {context, propagation, ROOT_CONTEXT} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {groupBy} from '../../../../shared/src/arrays.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {getErrorMessage} from '../../../../shared/src/error.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../../zero-protocol/src/error-reason.ts';\nimport {\n isProtocolError,\n type PushFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport {\n mutateResponseSchema,\n type MutateResponse,\n} from '../../../../zero-protocol/src/mutate-server.ts';\nimport type {MutationID} from '../../../../zero-protocol/src/mutation-id.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {CLEANUP_RESULTS_MUTATION_NAME} from '../../../../zero-protocol/src/mutation.ts';\nimport {type PushBody} from '../../../../zero-protocol/src/push.ts';\nimport {authEquals, isAuthErrorBody} from '../../auth/auth.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport {fetchFromAPIServer} from '../../custom/fetch.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {HandlerResult, StreamResult} from '../../workers/connection.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport type {\n ConnectionContext,\n ConnectionContextManager,\n ConnectionSelector,\n} from '../view-syncer/connection-context-manager.ts';\n\nexport interface Pusher extends RefCountedService {\n initConnection(selector: ConnectionSelector): Source<Downstream>;\n enqueuePush(selector: ConnectionSelector, push: PushBody): HandlerResult;\n ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void>;\n deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void>;\n}\n\ntype Config = Pick<ZeroConfig, 'app' | 'shard'>;\n\n/**\n * Receives push messages from zero-client and forwards\n * them the the user's API server.\n *\n * If the user's API server is taking too long to process\n * the push, the PusherService will add the push to a queue\n * and send pushes in bulk the next time the user's API server\n * is available.\n *\n * - One PusherService exists per client group.\n * - Mutations for a given client are always sent in-order\n * - Mutations for different clients in the same group may be interleaved\n */\nexport class PusherService implements Service, Pusher {\n readonly id: string;\n readonly #connContextManager: ConnectionContextManager;\n readonly #pusher: PushWorker;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #config: Config;\n readonly #lc: LogContext;\n #stopped: Promise<void> | undefined;\n #refCount = 0;\n #isStopped = false;\n\n constructor(\n appConfig: Config,\n lc: LogContext,\n clientGroupID: string,\n connContextManager: ConnectionContextManager,\n ) {\n this.#connContextManager = connContextManager;\n this.#config = appConfig;\n this.#lc = lc.withContext('component', 'pusherService');\n this.#queue = new Queue();\n this.#pusher = new PushWorker(\n appConfig,\n lc,\n this.#connContextManager,\n this.#queue,\n );\n this.id = clientGroupID;\n }\n\n initConnection(selector: ConnectionSelector) {\n return this.#pusher.initConnection(selector);\n }\n\n enqueuePush(\n selector: ConnectionSelector,\n push: PushBody,\n ): Exclude<HandlerResult, StreamResult> {\n const connCtx = this.#connContextManager.getConnectionContext(selector);\n if (connCtx) {\n this.#pusher.enqueuePush(connCtx, push);\n }\n\n return {\n type: 'ok',\n };\n }\n\n async ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void> {\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: upToID.clientID,\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'single',\n clientGroupID: this.id,\n clientID: upToID.clientID,\n upToMutationID: upToID.id,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-${this.id}-${upToID.clientID}-${upToID.id}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n {operation: 'cleanup', cleanupType: 'single'},\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n /**\n * Bulk cleanup is routed through the requester's push context.\n *\n * This assumes the client group shares a compatible push endpoint/auth\n * context.\n */\n async deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void> {\n if (clientIDs.length === 0) {\n return;\n }\n\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: clientIDs[0], // Use first client as sender\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'bulk',\n clientGroupID: this.id,\n clientIDs,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-bulk-${this.id}-${Date.now()}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n {operation: 'cleanup', cleanupType: 'bulk'},\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send bulk cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n run(): Promise<void> {\n this.#stopped = this.#pusher.run();\n return this.#stopped;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return must(this.#stopped, 'Stop was called before `run`');\n }\n this.#isStopped = true;\n this.#queue.enqueue('stop');\n return must(this.#stopped, 'Stop was called before `run`');\n }\n}\n\ntype PusherEntry = {\n push: PushBody;\n connCtx: ConnectionContext;\n};\ntype PusherEntryOrStop = PusherEntry | 'stop';\n\n/**\n * Awaits items in the queue then drains and sends them all\n * to the user's API server.\n */\nclass PushWorker {\n readonly #connContextManager: ConnectionContextManager;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #lc: LogContext;\n readonly #config: Config;\n readonly #clients: Map<\n string,\n {wsID: string; downstream: Subscription<Downstream>}\n >;\n\n readonly #customMutations = getOrCreateCounter(\n 'mutation',\n 'custom',\n 'Number of custom mutations processed',\n );\n readonly #pushes = getOrCreateCounter(\n 'mutation',\n 'pushes',\n 'Number of pushes processed by the pusher',\n );\n\n constructor(\n config: Config,\n lc: LogContext,\n connContextManager: ConnectionContextManager,\n queue: Queue<PusherEntryOrStop>,\n ) {\n this.#lc = lc.withContext('component', 'pusher');\n this.#connContextManager = connContextManager;\n this.#queue = queue;\n this.#config = config;\n this.#clients = new Map();\n }\n\n /**\n * Returns a new downstream stream if the clientID,wsID pair has not been seen before.\n * If a clientID already exists with a different wsID, that client's downstream is cancelled.\n */\n initConnection(selector: ConnectionSelector) {\n const existing = this.#clients.get(selector.clientID);\n if (existing && existing.wsID === selector.wsID) {\n // already initialized for this socket\n throw new Error('Connection was already initialized');\n }\n\n // client is back on a new connection\n if (existing) {\n existing.downstream.cancel();\n }\n\n const downstream = Subscription.create<Downstream>({\n cleanup: () => {\n this.#clients.delete(selector.clientID);\n },\n });\n this.#clients.set(selector.clientID, {\n wsID: selector.wsID,\n downstream,\n });\n return downstream;\n }\n\n enqueuePush(connCtx: ConnectionContext, push: PushBody) {\n this.#queue.enqueue({\n push,\n connCtx,\n });\n }\n\n async run() {\n for (;;) {\n const task = await this.#queue.dequeue();\n const rest = this.#queue.drain();\n const [pushes, terminate] = combinePushes([task, ...rest]);\n for (const push of pushes) {\n const parentContext = push.push.traceparent\n ? propagation.extract(ROOT_CONTEXT, {\n traceparent: push.push.traceparent,\n })\n : context.active();\n const response = await context.with(parentContext, () =>\n this.#processPush(push),\n );\n await this.#fanOutResponses(response);\n }\n\n if (terminate) {\n break;\n }\n }\n }\n\n /**\n * 1. If the entire `push` fails, we send the error to relevant clients.\n * 2. If the push succeeds, we look for any mutation failure that should cause the connection to terminate\n * and terminate the connection for those clients.\n */\n #fanOutResponses(response: MutateResponse) {\n const connectionTerminations: (() => void)[] = [];\n\n // if the entire push failed, send that to the client.\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a push error.',\n response,\n );\n // TODO(0xcadams): Fanout is keyed only by clientID here. If a response arrives\n // after reconnect or re-auth, `#clients.get(clientID)` may point at a\n // newer wsID/revision and fail the replacement downstream instead.\n const groupedMutationIDs = groupBy(\n response.mutationIDs ?? [],\n m => m.clientID,\n );\n for (const [clientID, mutationIDs] of groupedMutationIDs) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n // We do not resolve mutations on the client if the push fails\n // as those mutations will be retried.\n if ('error' in response) {\n // This error code path will eventually be removed when we\n // no longer support the legacy push error format.\n const pushFailedBody: PushFailedBody =\n response.error === 'http'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n status: response.status,\n bodyPreview: response.details,\n mutationIDs,\n message: `Fetch from API server returned non-OK status ${response.status}`,\n }\n : response.error === 'unsupportedPushVersion'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.UnsupportedPushVersion,\n mutationIDs,\n message: `Unsupported push version`,\n }\n : {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Internal,\n mutationIDs,\n message:\n response.error === 'zeroPusher'\n ? response.details\n : response.error === 'unsupportedSchemaVersion'\n ? 'Unsupported schema version'\n : 'An unknown error occurred while pushing to the API server',\n };\n\n this.#failDownstream(client.downstream, pushFailedBody);\n } else {\n this.#failDownstream(client.downstream, response);\n }\n }\n } else {\n // Look for mutations results that should cause us to terminate the connection\n // TODO(0xcadams): Same stale-routing issue as above: fatal mutation results are\n // still mapped to the current downstream by clientID only.\n const groupedMutations = groupBy(response.mutations, m => m.id.clientID);\n for (const [clientID, mutations] of groupedMutations) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n let failure: PushFailedBody | undefined;\n let i = 0;\n for (; i < mutations.length; i++) {\n const m = mutations[i];\n if ('error' in m.result) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a mutation error.',\n m.result,\n );\n }\n // This error code path will eventually be removed,\n // keeping this for backwards compatibility, but the server\n // should now return a PushFailedBody with the mutationIDs\n if ('error' in m.result && m.result.error === 'oooMutation') {\n failure = {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.OutOfOrderMutation,\n message: 'mutation was out of order',\n details: m.result.details,\n mutationIDs: mutations.map(m => ({\n clientID: m.id.clientID,\n id: m.id.id,\n })),\n };\n break;\n }\n }\n\n if (failure && i < mutations.length - 1) {\n this.#lc.warn?.(\n 'push-response contains mutations after a mutation which should fatal the connection',\n );\n }\n\n if (failure) {\n connectionTerminations.push(() =>\n this.#failDownstream(client.downstream, failure),\n );\n }\n }\n }\n\n connectionTerminations.forEach(cb => cb());\n }\n\n async #processPush(entry: PusherEntry): Promise<MutateResponse> {\n this.#customMutations.add(entry.push.mutations.length, {\n clientGroupID: entry.push.clientGroupID,\n });\n this.#pushes.add(1, {\n clientGroupID: entry.push.clientGroupID,\n });\n\n // Record custom mutations for telemetry\n recordMutation('custom', entry.push.mutations.length);\n\n const url = must(\n entry.connCtx.mutateContext.url,\n 'ZERO_MUTATE_URL is not set',\n );\n\n this.#lc.debug?.(\n 'pushing to',\n url,\n 'with',\n entry.push.mutations.length,\n 'mutations',\n );\n\n let mutationIDs: MutationID[] = [];\n\n try {\n mutationIDs = entry.push.mutations.map(m => ({\n id: m.id,\n clientID: m.clientID,\n }));\n\n const response = await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n entry.connCtx,\n {\n appID: this.#config.app.id,\n shardNum: this.#config.shard.num,\n },\n entry.push,\n {operation: 'mutate'},\n );\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n // A successful push also validates this connection's current auth snapshot.\n // That lets later shared work reuse it without trusting stale credentials.\n this.#connContextManager.validateConnection(\n entry.connCtx,\n entry.connCtx.revision,\n 'kind' in response &&\n response.kind === 'MutateResponse' &&\n response?.userID !== undefined\n ? {kind: 'server-validated', validatedUserID: response.userID}\n : {kind: 'client-fallback'},\n );\n return response;\n } catch (e) {\n if (isProtocolError(e) && e.errorBody.kind === ErrorKind.PushFailed) {\n const response = {\n ...e.errorBody,\n mutationIDs,\n } as const satisfies PushFailedBody;\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n\n if (isProtocolError(e) && isAuthErrorBody(e.errorBody)) {\n // The push completed far enough for local validation to reject the\n // connection, so invalidate it and surface the result as PushFailed.\n this.#lc.warn?.('Push validation failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: e.message,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n message: e.message,\n status: 401,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to push: ${getErrorMessage(e)}`,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n }\n\n #failDownstream(\n downstream: Subscription<Downstream>,\n errorBody: PushFailedBody,\n ): void {\n downstream.fail(new ProtocolErrorWithLevel(errorBody, 'warn'));\n }\n}\n\n/**\n * Pushes for different clients, sockets, or auth revisions could be interleaved.\n *\n * In order to batch safely, we only combine pushes from the same\n * clientID/wsID/revision snapshot.\n */\nexport function combinePushes(\n entries: readonly (PusherEntryOrStop | undefined)[],\n): [PusherEntry[], boolean] {\n const pushesByConnection = new Map<string, PusherEntry[]>();\n\n function collect() {\n const ret: PusherEntry[] = [];\n for (const entries of pushesByConnection.values()) {\n const composite: PusherEntry = {\n ...entries[0],\n push: {\n ...entries[0].push,\n mutations: [],\n },\n };\n ret.push(composite);\n for (const entry of entries) {\n assertAreCompatiblePushes(composite, entry);\n composite.push.mutations.push(...entry.push.mutations);\n }\n }\n return ret;\n }\n\n for (const entry of entries) {\n if (entry === 'stop' || entry === undefined) {\n return [collect(), true];\n }\n\n const key = `${entry.connCtx.clientID}:${entry.connCtx.wsID}:${entry.connCtx.revision}`;\n const existing = pushesByConnection.get(key);\n if (existing) {\n existing.push(entry);\n } else {\n pushesByConnection.set(key, [entry]);\n }\n }\n\n return [collect(), false] as const;\n}\n\n// These invariants should always be true for a given clientID.\n// If they are not, we have a bug in the code somewhere.\nfunction assertAreCompatiblePushes(left: PusherEntry, right: PusherEntry) {\n assert(\n left.connCtx.clientID === right.connCtx.clientID,\n 'clientID must be the same for all pushes',\n );\n assert(\n left.connCtx.wsID === right.connCtx.wsID,\n 'wsID must be the same for all pushes',\n );\n assert(\n left.connCtx.revision === right.connCtx.revision,\n 'revision must be the same for all pushes',\n );\n assert(\n authEquals(left.connCtx.auth, right.connCtx.auth),\n 'auth must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.schemaVersion === right.push.schemaVersion,\n 'schemaVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.pushVersion === right.push.pushVersion,\n 'pushVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.cookie ===\n right.connCtx.mutateContext.headerOptions.cookie,\n 'httpCookie must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.origin ===\n right.connCtx.mutateContext.headerOptions.origin,\n 'origin must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.user.id === right.connCtx.user.id,\n 'userID must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.url === right.connCtx.mutateContext.url,\n 'userPushURL must be the same for all pushes with the same clientID',\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEA,IAAa,gBAAb,MAAsD;CACpD;CACA;CACA;CACA;CACA;CACA;CACA;CACA,YAAY;CACZ,aAAa;CAEb,YACE,WACA,IACA,eACA,oBACA;EACA,KAAKA,sBAAsB;EAC3B,KAAKG,UAAU;EACf,KAAKC,MAAM,GAAG,YAAY,aAAa,eAAe;EACtD,KAAKF,SAAS,IAAI,MAAM;EACxB,KAAKD,UAAU,IAAI,WACjB,WACA,IACA,KAAKD,qBACL,KAAKE,MACP;EACA,KAAK,KAAK;CACZ;CAEA,eAAe,UAA8B;EAC3C,OAAO,KAAKD,QAAQ,eAAe,QAAQ;CAC7C;CAEA,YACE,UACA,MACsC;EACtC,MAAM,UAAU,KAAKD,oBAAoB,qBAAqB,QAAQ;EACtE,IAAI,SACF,KAAKC,QAAQ,YAAY,SAAS,IAAI;EAGxC,OAAO,EACL,MAAM,KACR;CACF;CAEA,MAAM,qBACJ,WACA,QACe;EACf,MAAM,UAAU,KAAKD,oBAAoB,qBAAqB,SAAS;EACvE,IAAI,CAAC,SAAS,eAAe,KAE3B;EAGF,MAAM,cAAwB;GAC5B,eAAe,KAAK;GACpB,WAAW,CACT;IACE,MAAM;IACN,IAAI;IACJ,UAAU,OAAO;IACjB,MAAM;IACN,MAAM,CACJ;KACE,MAAM;KACN,eAAe,KAAK;KACpB,UAAU,OAAO;KACjB,gBAAgB,OAAO;IACzB,CACF;IACA,WAAW,KAAK,IAAI;GACtB,CACF;GACA,aAAa;GACb,WAAW,KAAK,IAAI;GACpB,WAAW,WAAW,KAAK,GAAG,GAAG,OAAO,SAAS,GAAG,OAAO;EAC7D;EAEA,IAAI;GACF,MAAM,mBACJ,sBACA,QACA,KAAKI,KACL,SACA;IAAC,OAAO,KAAKD,QAAQ,IAAI;IAAI,UAAU,KAAKA,QAAQ,MAAM;GAAG,GAC7D,aACA;IAAC,WAAW;IAAW,aAAa;GAAQ,CAC9C;EACF,SAAS,GAAG;GACV,KAAKC,IAAI,OAAO,mCAAmC,EACjD,OAAO,gBAAgB,CAAC,EAC1B,CAAC;EACH;CACF;;;;;;;CAQA,MAAM,sBACJ,WACA,WACe;EACf,IAAI,UAAU,WAAW,GACvB;EAGF,MAAM,UAAU,KAAKJ,oBAAoB,qBAAqB,SAAS;EACvE,IAAI,CAAC,SAAS,eAAe,KAE3B;EAGF,MAAM,cAAwB;GAC5B,eAAe,KAAK;GACpB,WAAW,CACT;IACE,MAAM;IACN,IAAI;IACJ,UAAU,UAAU;IACpB,MAAM;IACN,MAAM,CACJ;KACE,MAAM;KACN,eAAe,KAAK;KACpB;IACF,CACF;IACA,WAAW,KAAK,IAAI;GACtB,CACF;GACA,aAAa;GACb,WAAW,KAAK,IAAI;GACpB,WAAW,gBAAgB,KAAK,GAAG,GAAG,KAAK,IAAI;EACjD;EAEA,IAAI;GACF,MAAM,mBACJ,sBACA,QACA,KAAKI,KACL,SACA;IAAC,OAAO,KAAKD,QAAQ,IAAI;IAAI,UAAU,KAAKA,QAAQ,MAAM;GAAG,GAC7D,aACA;IAAC,WAAW;IAAW,aAAa;GAAM,CAC5C;EACF,SAAS,GAAG;GACV,KAAKC,IAAI,OAAO,wCAAwC,EACtD,OAAO,gBAAgB,CAAC,EAC1B,CAAC;EACH;CACF;CAEA,MAAM;EACJ,OAAO,CAAC,KAAKC,YAAY,kCAAkC;EAC3D,EAAE,KAAKC;CACT;CAEA,QAAQ;EACN,OAAO,CAAC,KAAKD,YAAY,kCAAkC;EAC3D,EAAE,KAAKC;EACP,IAAI,KAAKA,aAAa,GACpB,KAAU,KAAK;CAEnB;CAEA,UAAmB;EACjB,OAAO,KAAKA,YAAY;CAC1B;CAEA,MAAqB;EACnB,KAAKC,WAAW,KAAKN,QAAQ,IAAI;EACjC,OAAO,KAAKM;CACd;CAEA,OAAsB;EACpB,IAAI,KAAKF,YACP,OAAO,KAAK,KAAKE,UAAU,8BAA8B;EAE3D,KAAKF,aAAa;EAClB,KAAKH,OAAO,QAAQ,MAAM;EAC1B,OAAO,KAAK,KAAKK,UAAU,8BAA8B;CAC3D;AACF;;;;;AAYA,IAAM,aAAN,MAAiB;CACf;CACA;CACA;CACA;CACA;CAKA,mBAA4B,mBAC1B,YACA,UACA,sCACF;CACA,UAAmB,mBACjB,YACA,UACA,0CACF;CAEA,YACE,QACA,IACA,oBACA,OACA;EACA,KAAKH,MAAM,GAAG,YAAY,aAAa,QAAQ;EAC/C,KAAKJ,sBAAsB;EAC3B,KAAKE,SAAS;EACd,KAAKC,UAAU;EACf,KAAKK,2BAAW,IAAI,IAAI;CAC1B;;;;;CAMA,eAAe,UAA8B;EAC3C,MAAM,WAAW,KAAKA,SAAS,IAAI,SAAS,QAAQ;EACpD,IAAI,YAAY,SAAS,SAAS,SAAS,MAEzC,MAAM,IAAI,MAAM,oCAAoC;EAItD,IAAI,UACF,SAAS,WAAW,OAAO;EAG7B,MAAM,aAAa,aAAa,OAAmB,EACjD,eAAe;GACb,KAAKA,SAAS,OAAO,SAAS,QAAQ;EACxC,EACF,CAAC;EACD,KAAKA,SAAS,IAAI,SAAS,UAAU;GACnC,MAAM,SAAS;GACf;EACF,CAAC;EACD,OAAO;CACT;CAEA,YAAY,SAA4B,MAAgB;EACtD,KAAKN,OAAO,QAAQ;GAClB;GACA;EACF,CAAC;CACH;CAEA,MAAM,MAAM;EACV,SAAS;GAGP,MAAM,CAAC,QAAQ,aAAa,cAAc,CAAC,MAFxB,KAAKA,OAAO,QAAQ,GAEU,GADpC,KAAKA,OAAO,MAC2B,CAAI,CAAC;GACzD,KAAK,MAAM,QAAQ,QAAQ;IACzB,MAAM,gBAAgB,KAAK,KAAK,cAC5B,YAAY,QAAQ,cAAc,EAChC,aAAa,KAAK,KAAK,YACzB,CAAC,IACD,QAAQ,OAAO;IACnB,MAAM,WAAW,MAAM,QAAQ,KAAK,qBAClC,KAAKS,aAAa,IAAI,CACxB;IACA,MAAM,KAAKC,iBAAiB,QAAQ;GACtC;GAEA,IAAI,WACF;EAEJ;CACF;;;;;;CAOA,iBAAiB,UAA0B;EACzC,MAAM,yBAAyC,CAAC;EAGhD,IACG,UAAU,YAAY,SAAS,SAAS,gBACzC,WAAW,UACX;GACA,KAAKR,IAAI,OACP,4DACA,QACF;GAIA,MAAM,qBAAqB,QACzB,SAAS,eAAe,CAAC,IACzB,MAAK,EAAE,QACT;GACA,KAAK,MAAM,CAAC,UAAU,gBAAgB,oBAAoB;IACxD,MAAM,SAAS,KAAKI,SAAS,IAAI,QAAQ;IACzC,IAAI,CAAC,QACH;IAKF,IAAI,WAAW,UAAU;KAGvB,MAAM,iBACJ,SAAS,UAAU,SACf;MACE,MAAM;MACN,QAAQ;MACR,QAAQ;MACR,QAAQ,SAAS;MACjB,aAAa,SAAS;MACtB;MACA,SAAS,gDAAgD,SAAS;KACpE,IACA,SAAS,UAAU,2BACjB;MACE,MAAM;MACN,QAAQ;MACR,QAAQ;MACR;MACA,SAAS;KACX,IACA;MACE,MAAM;MACN,QAAQ;MACR,QAAQ;MACR;MACA,SACE,SAAS,UAAU,eACf,SAAS,UACT,SAAS,UAAU,6BACjB,+BACA;KACV;KAER,KAAKK,gBAAgB,OAAO,YAAY,cAAc;IACxD,OACE,KAAKA,gBAAgB,OAAO,YAAY,QAAQ;GAEpD;EACF,OAAO;GAIL,MAAM,mBAAmB,QAAQ,SAAS,YAAW,MAAK,EAAE,GAAG,QAAQ;GACvE,KAAK,MAAM,CAAC,UAAU,cAAc,kBAAkB;IACpD,MAAM,SAAS,KAAKL,SAAS,IAAI,QAAQ;IACzC,IAAI,CAAC,QACH;IAGF,IAAI;IACJ,IAAI,IAAI;IACR,OAAO,IAAI,UAAU,QAAQ,KAAK;KAChC,MAAM,IAAI,UAAU;KACpB,IAAI,WAAW,EAAE,QACf,KAAKJ,IAAI,OACP,gEACA,EAAE,MACJ;KAKF,IAAI,WAAW,EAAE,UAAU,EAAE,OAAO,UAAU,eAAe;MAC3D,UAAU;OACR,MAAM;OACN,QAAQ;OACR,QAAQ;OACR,SAAS;OACT,SAAS,EAAE,OAAO;OAClB,aAAa,UAAU,KAAI,OAAM;QAC/B,UAAU,EAAE,GAAG;QACf,IAAI,EAAE,GAAG;OACX,EAAE;MACJ;MACA;KACF;IACF;IAEA,IAAI,WAAW,IAAI,UAAU,SAAS,GACpC,KAAKA,IAAI,OACP,qFACF;IAGF,IAAI,SACF,uBAAuB,WACrB,KAAKS,gBAAgB,OAAO,YAAY,OAAO,CACjD;GAEJ;EACF;EAEA,uBAAuB,SAAQ,OAAM,GAAG,CAAC;CAC3C;CAEA,MAAMF,aAAa,OAA6C;EAC9D,KAAKF,iBAAiB,IAAI,MAAM,KAAK,UAAU,QAAQ,EACrD,eAAe,MAAM,KAAK,cAC5B,CAAC;EACD,KAAKC,QAAQ,IAAI,GAAG,EAClB,eAAe,MAAM,KAAK,cAC5B,CAAC;EAGD,eAAe,UAAU,MAAM,KAAK,UAAU,MAAM;EAEpD,MAAM,MAAM,KACV,MAAM,QAAQ,cAAc,KAC5B,4BACF;EAEA,KAAKN,IAAI,QACP,cACA,KACA,QACA,MAAM,KAAK,UAAU,QACrB,WACF;EAEA,IAAI,cAA4B,CAAC;EAEjC,IAAI;GACF,cAAc,MAAM,KAAK,UAAU,KAAI,OAAM;IAC3C,IAAI,EAAE;IACN,UAAU,EAAE;GACd,EAAE;GAEF,MAAM,WAAW,MAAM,mBACrB,sBACA,QACA,KAAKA,KACL,MAAM,SACN;IACE,OAAO,KAAKD,QAAQ,IAAI;IACxB,UAAU,KAAKA,QAAQ,MAAM;GAC/B,GACA,MAAM,MACN,EAAC,WAAW,SAAQ,CACtB;GACA,IACG,UAAU,YAAY,SAAS,SAAS,gBACzC,WAAW,UACX;IACA,IAAI,gBAAgB,QAAQ,GAAG;KAC7B,KAAKC,IAAI,OAAO,6CAA6C;MAC3D,UAAU,MAAM,QAAQ;MACxB,UAAU,UAAU,WAAW,SAAS,UAAU,KAAA;KACpD,CAAC;KACD,KAAKJ,oBAAoB,eACvB,MAAM,SACN,MAAM,QAAQ,QAChB;IACF;IACA,OAAO;GACT;GAGA,KAAKA,oBAAoB,mBACvB,MAAM,SACN,MAAM,QAAQ,UACd,UAAU,YACR,SAAS,SAAS,oBAClB,UAAU,WAAW,KAAA,IACnB;IAAC,MAAM;IAAoB,iBAAiB,SAAS;GAAM,IAC3D,EAAC,MAAM,kBAAiB,CAC9B;GACA,OAAO;EACT,SAAS,GAAG;GACV,IAAI,gBAAgB,CAAC,KAAK,EAAE,UAAU,SAAS,cAAsB;IACnE,MAAM,WAAW;KACf,GAAG,EAAE;KACL;IACF;IACA,IAAI,gBAAgB,QAAQ,GAAG;KAC7B,KAAKI,IAAI,OAAO,6CAA6C;MAC3D,UAAU,MAAM,QAAQ;MACxB,UAAU,UAAU,WAAW,SAAS,UAAU,KAAA;KACpD,CAAC;KACD,KAAKJ,oBAAoB,eACvB,MAAM,SACN,MAAM,QAAQ,QAChB;IACF;IACA,OAAO;GACT;GAEA,IAAI,gBAAgB,CAAC,KAAK,gBAAgB,EAAE,SAAS,GAAG;IAGtD,KAAKI,IAAI,OAAO,mDAAmD;KACjE,UAAU,MAAM,QAAQ;KACxB,UAAU,EAAE;IACd,CAAC;IACD,KAAKJ,oBAAoB,eACvB,MAAM,SACN,MAAM,QAAQ,QAChB;IACA,OAAO;KACL,MAAM;KACN,QAAQ;KACR,QAAQ;KACR,SAAS,EAAE;KACX,QAAQ;KACR;IACF;GACF;GAEA,OAAO;IACL,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,SAAS,mBAAmB,gBAAgB,CAAC;IAC7C;GACF;EACF;CACF;CAEA,gBACE,YACA,WACM;EACN,WAAW,KAAK,IAAI,uBAAuB,WAAW,MAAM,CAAC;CAC/D;AACF;;;;;;;AAQA,SAAgB,cACd,SAC0B;CAC1B,MAAM,qCAAqB,IAAI,IAA2B;CAE1D,SAAS,UAAU;EACjB,MAAM,MAAqB,CAAC;EAC5B,KAAK,MAAM,WAAW,mBAAmB,OAAO,GAAG;GACjD,MAAM,YAAyB;IAC7B,GAAG,QAAQ;IACX,MAAM;KACJ,GAAG,QAAQ,GAAG;KACd,WAAW,CAAC;IACd;GACF;GACA,IAAI,KAAK,SAAS;GAClB,KAAK,MAAM,SAAS,SAAS;IAC3B,0BAA0B,WAAW,KAAK;IAC1C,UAAU,KAAK,UAAU,KAAK,GAAG,MAAM,KAAK,SAAS;GACvD;EACF;EACA,OAAO;CACT;CAEA,KAAK,MAAM,SAAS,SAAS;EAC3B,IAAI,UAAU,UAAU,UAAU,KAAA,GAChC,OAAO,CAAC,QAAQ,GAAG,IAAI;EAGzB,MAAM,MAAM,GAAG,MAAM,QAAQ,SAAS,GAAG,MAAM,QAAQ,KAAK,GAAG,MAAM,QAAQ;EAC7E,MAAM,WAAW,mBAAmB,IAAI,GAAG;EAC3C,IAAI,UACF,SAAS,KAAK,KAAK;OAEnB,mBAAmB,IAAI,KAAK,CAAC,KAAK,CAAC;CAEvC;CAEA,OAAO,CAAC,QAAQ,GAAG,KAAK;AAC1B;AAIA,SAAS,0BAA0B,MAAmB,OAAoB;CACxE,OACE,KAAK,QAAQ,aAAa,MAAM,QAAQ,UACxC,0CACF;CACA,OACE,KAAK,QAAQ,SAAS,MAAM,QAAQ,MACpC,sCACF;CACA,OACE,KAAK,QAAQ,aAAa,MAAM,QAAQ,UACxC,0CACF;CACA,OACE,WAAW,KAAK,QAAQ,MAAM,MAAM,QAAQ,IAAI,GAChD,6DACF;CACA,OACE,KAAK,KAAK,kBAAkB,MAAM,KAAK,eACvC,sEACF;CACA,OACE,KAAK,KAAK,gBAAgB,MAAM,KAAK,aACrC,oEACF;CACA,OACE,KAAK,QAAQ,cAAc,cAAc,WACvC,MAAM,QAAQ,cAAc,cAAc,QAC5C,mEACF;CACA,OACE,KAAK,QAAQ,cAAc,cAAc,WACvC,MAAM,QAAQ,cAAc,cAAc,QAC5C,+DACF;CACA,OACE,KAAK,QAAQ,KAAK,OAAO,MAAM,QAAQ,KAAK,IAC5C,+DACF;CACA,OACE,KAAK,QAAQ,cAAc,QAAQ,MAAM,QAAQ,cAAc,KAC/D,oEACF;AACF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LogContext } from '@rocicorp/logger';
|
|
2
|
+
import type { FastifyReply, FastifyRequest } from 'fastify';
|
|
3
|
+
import type { NormalizedZeroConfig } from '../config/normalize.ts';
|
|
4
|
+
import { type Worker } from '../types/processes.ts';
|
|
5
|
+
export declare function handleProfzRequest(lc: LogContext, config: Pick<NormalizedZeroConfig, 'adminPassword'>, req: FastifyRequest, res: FastifyReply, getWorker?: (() => Promise<Worker>) | undefined, forcedWorker?: string | undefined, localProcessName?: string): Promise<void>;
|
|
6
|
+
export declare function handleProfrmzRequest(lc: LogContext, config: NormalizedZeroConfig, req: FastifyRequest, res: FastifyReply, getWorker?: (() => Promise<Worker>) | undefined): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=profz.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profz.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/profz.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,YAAY,EAAE,cAAc,EAAC,MAAM,SAAS,CAAC;AAE1D,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAEjE,OAAO,EAKL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAI/B,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,eAAe,CAAC,EACnD,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,YAAY,EACjB,SAAS,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,EAC/C,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,GAAE,MAAqB,GACtC,OAAO,CAAC,IAAI,CAAC,CAgIf;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,oBAAoB,EAC5B,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,YAAY,EACjB,SAAS,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,GAC9C,OAAO,CAAC,IAAI,CAAC,CAkDf"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { sleep } from "../../../shared/src/sleep.js";
|
|
2
|
+
import { singleProcessMode, subscribeToMessageType } from "../types/processes.js";
|
|
3
|
+
import { isAdminPasswordValid } from "../config/zero-config.js";
|
|
4
|
+
import { CpuProfiler } from "../types/profiler.js";
|
|
5
|
+
import { URLParams } from "../types/url-params.js";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import auth from "basic-auth";
|
|
8
|
+
//#region ../zero-cache/src/services/profz.ts
|
|
9
|
+
async function handleProfzRequest(lc, config, req, res, getWorker, forcedWorker, localProcessName = "dispatcher") {
|
|
10
|
+
if (!isAdminPasswordValid(lc, config, auth(req)?.pass)) {
|
|
11
|
+
res.code(401).header("WWW-Authenticate", "Basic realm=\"Profz Protected Area\"").send("Unauthorized");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const params = new URLParams(new URL(req.url, "http://localhost"));
|
|
15
|
+
const rawDuration = params.getInteger("duration", false) ?? params.getInteger("seconds", false) ?? 5;
|
|
16
|
+
const durationMs = Math.max(1, Math.min(60, rawDuration)) * 1e3;
|
|
17
|
+
const targetWorker = forcedWorker ?? params.get("worker", false) ?? void 0;
|
|
18
|
+
const targetWorkerIndex = params.getInteger("index", false) ?? void 0;
|
|
19
|
+
const id = randomUUID();
|
|
20
|
+
const responses = /* @__PURE__ */ new Map();
|
|
21
|
+
const localProfilePromise = singleProcessMode() || getWorker === void 0 || targetWorker === void 0 || targetWorker === localProcessName ? CpuProfiler.profile(durationMs).catch((err) => {
|
|
22
|
+
lc.warn?.("Failed to capture local CPU profile:", err);
|
|
23
|
+
return null;
|
|
24
|
+
}) : Promise.resolve(null);
|
|
25
|
+
if (!singleProcessMode() && getWorker !== void 0) try {
|
|
26
|
+
const worker = await getWorker();
|
|
27
|
+
const onMessage = (msg) => {
|
|
28
|
+
if (msg.id === id && msg.profile) responses.set(msg.name, msg.profile);
|
|
29
|
+
else if (msg.id === id && msg.error) lc.warn?.(`Worker ${msg.name} profile error: ${msg.error}`);
|
|
30
|
+
};
|
|
31
|
+
const unsubscribe = subscribeToMessageType(worker, "profileResponse", onMessage);
|
|
32
|
+
try {
|
|
33
|
+
worker.send(["profile", {
|
|
34
|
+
id,
|
|
35
|
+
durationMs,
|
|
36
|
+
worker: targetWorker === localProcessName ? "__none__" : targetWorker,
|
|
37
|
+
workerIndex: targetWorkerIndex
|
|
38
|
+
}]);
|
|
39
|
+
await sleep(durationMs + 500);
|
|
40
|
+
} finally {
|
|
41
|
+
unsubscribe();
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
lc.warn?.("Failed to dispatch profile request to child workers:", err);
|
|
45
|
+
}
|
|
46
|
+
const localProfile = await localProfilePromise;
|
|
47
|
+
if (localProfile) {
|
|
48
|
+
const localName = targetWorker ?? (singleProcessMode() ? "zero-cache" : localProcessName);
|
|
49
|
+
responses.set(localName, localProfile);
|
|
50
|
+
}
|
|
51
|
+
if (targetWorker !== void 0 && targetWorker !== "all") {
|
|
52
|
+
let matchedProfile = responses.get(targetWorker);
|
|
53
|
+
if (!matchedProfile) {
|
|
54
|
+
for (const [name, prof] of responses.entries()) if (name === targetWorker || name.startsWith(`${targetWorker}-`)) {
|
|
55
|
+
matchedProfile = prof;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (matchedProfile) {
|
|
60
|
+
res.header("Content-Type", "application/json").header("Content-Disposition", `attachment; filename="${targetWorker}.cpuprofile"`).send(matchedProfile);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
res.code(404).send({ error: `No profile captured for worker "${targetWorker}". Available: ${[...responses.keys()].join(", ")}` });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const bundle = {};
|
|
67
|
+
for (const [name, prof] of responses.entries()) bundle[name] = prof;
|
|
68
|
+
res.header("Content-Type", "application/json").header("Content-Disposition", `attachment; filename="zero-profile-bundle-${Date.now()}.json"`).send(bundle);
|
|
69
|
+
}
|
|
70
|
+
async function handleProfrmzRequest(lc, config, req, res, getWorker) {
|
|
71
|
+
if (!isAdminPasswordValid(lc, config, auth(req)?.pass)) {
|
|
72
|
+
res.code(401).header("WWW-Authenticate", "Basic realm=\"Profrmz Protected Area\"").send("Unauthorized");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (config.changeStreamer.uri) {
|
|
76
|
+
const upstreamURL = new URL("/profz", config.changeStreamer.uri);
|
|
77
|
+
const incomingURL = new URL(req.url, "http://localhost");
|
|
78
|
+
for (const [key, value] of incomingURL.searchParams.entries()) upstreamURL.searchParams.set(key, value);
|
|
79
|
+
try {
|
|
80
|
+
const headers = {};
|
|
81
|
+
if (req.headers.authorization) headers.authorization = req.headers.authorization;
|
|
82
|
+
const response = await fetch(upstreamURL.toString(), {
|
|
83
|
+
method: "GET",
|
|
84
|
+
headers
|
|
85
|
+
});
|
|
86
|
+
res.code(response.status);
|
|
87
|
+
const contentType = response.headers.get("content-type");
|
|
88
|
+
if (contentType) res.header("content-type", contentType);
|
|
89
|
+
const contentDisposition = response.headers.get("content-disposition");
|
|
90
|
+
if (contentDisposition) res.header("content-disposition", contentDisposition);
|
|
91
|
+
const body = await response.text();
|
|
92
|
+
return res.send(body);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
lc.error?.(`Failed to proxy /profrmz to RM at ${upstreamURL}:`, err);
|
|
95
|
+
return res.code(502).send({ error: `Failed to proxy profiling request to RM at ${upstreamURL}: ${String(err)}` });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return handleProfzRequest(lc, config, req, res, getWorker, "change-streamer");
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
export { handleProfrmzRequest, handleProfzRequest };
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=profz.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profz.js","names":[],"sources":["../../../../../zero-cache/src/services/profz.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport type {LogContext} from '@rocicorp/logger';\nimport auth from 'basic-auth';\nimport type {FastifyReply, FastifyRequest} from 'fastify';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport type {NormalizedZeroConfig} from '../config/normalize.ts';\nimport {isAdminPasswordValid} from '../config/zero-config.ts';\nimport {\n singleProcessMode,\n subscribeToMessageType,\n type ProfileResponse,\n type ProfileResponseMessage,\n type Worker,\n} from '../types/processes.ts';\nimport {CpuProfiler} from '../types/profiler.ts';\nimport {URLParams} from '../types/url-params.ts';\n\nexport async function handleProfzRequest(\n lc: LogContext,\n config: Pick<NormalizedZeroConfig, 'adminPassword'>,\n req: FastifyRequest,\n res: FastifyReply,\n getWorker?: (() => Promise<Worker>) | undefined,\n forcedWorker?: string | undefined,\n localProcessName: string = 'dispatcher',\n): Promise<void> {\n const credentials = auth(req);\n if (!isAdminPasswordValid(lc, config, credentials?.pass)) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Profz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n const params = new URLParams(new URL(req.url, 'http://localhost'));\n const rawDuration =\n params.getInteger('duration', false) ??\n params.getInteger('seconds', false) ??\n 5;\n const durationSec = Math.max(1, Math.min(60, rawDuration));\n const durationMs = durationSec * 1000;\n const targetWorker = forcedWorker ?? params.get('worker', false) ?? undefined;\n const targetWorkerIndex = params.getInteger('index', false) ?? undefined;\n const id = randomUUID();\n\n const responses = new Map<string, unknown>();\n\n // Profile dispatcher / local process if requested, when profiling all processes,\n // or when no worker dispatcher is available (single process / standalone mode)\n const shouldProfileLocal =\n singleProcessMode() ||\n getWorker === undefined ||\n targetWorker === undefined ||\n targetWorker === localProcessName;\n\n const localProfilePromise = shouldProfileLocal\n ? CpuProfiler.profile(durationMs).catch(err => {\n lc.warn?.('Failed to capture local CPU profile:', err);\n return null;\n })\n : Promise.resolve(null);\n\n if (!singleProcessMode() && getWorker !== undefined) {\n try {\n const worker = await getWorker();\n const onMessage = (msg: ProfileResponse) => {\n if (msg.id === id && msg.profile) {\n responses.set(msg.name, msg.profile);\n } else if (msg.id === id && msg.error) {\n lc.warn?.(`Worker ${msg.name} profile error: ${msg.error}`);\n }\n };\n\n // The worker is long-lived (and shared across requests), so the\n // per-request handler must be removed once this request is done.\n const unsubscribe = subscribeToMessageType<ProfileResponseMessage>(\n worker,\n 'profileResponse',\n onMessage,\n );\n try {\n worker.send([\n 'profile',\n {\n id,\n durationMs,\n worker:\n targetWorker === localProcessName ? '__none__' : targetWorker,\n workerIndex: targetWorkerIndex,\n },\n ]);\n\n // Wait for duration plus grace period for IPC transfer\n await sleep(durationMs + 500);\n } finally {\n unsubscribe();\n }\n } catch (err) {\n lc.warn?.('Failed to dispatch profile request to child workers:', err);\n }\n }\n\n const localProfile = await localProfilePromise;\n if (localProfile) {\n const localName =\n targetWorker ?? (singleProcessMode() ? 'zero-cache' : localProcessName);\n responses.set(localName, localProfile);\n }\n\n // If a single specific worker was requested, return that profile directly\n if (targetWorker !== undefined && targetWorker !== 'all') {\n let matchedProfile = responses.get(targetWorker);\n if (!matchedProfile) {\n // Try matching by prefix or substring (e.g. 'syncer' matches 'syncer-0')\n for (const [name, prof] of responses.entries()) {\n if (name === targetWorker || name.startsWith(`${targetWorker}-`)) {\n matchedProfile = prof;\n break;\n }\n }\n }\n\n if (matchedProfile) {\n void res\n .header('Content-Type', 'application/json')\n .header(\n 'Content-Disposition',\n `attachment; filename=\"${targetWorker}.cpuprofile\"`,\n )\n .send(matchedProfile);\n return;\n }\n\n void res.code(404).send({\n error: `No profile captured for worker \"${targetWorker}\". Available: ${[...responses.keys()].join(', ')}`,\n });\n return;\n }\n\n // Default (Option A): Return multi-process profile bundle as a JSON map\n const bundle: Record<string, unknown> = {};\n for (const [name, prof] of responses.entries()) {\n bundle[name] = prof;\n }\n\n void res\n .header('Content-Type', 'application/json')\n .header(\n 'Content-Disposition',\n `attachment; filename=\"zero-profile-bundle-${Date.now()}.json\"`,\n )\n .send(bundle);\n}\n\nexport async function handleProfrmzRequest(\n lc: LogContext,\n config: NormalizedZeroConfig,\n req: FastifyRequest,\n res: FastifyReply,\n getWorker?: (() => Promise<Worker>) | undefined,\n): Promise<void> {\n const credentials = auth(req);\n if (!isAdminPasswordValid(lc, config, credentials?.pass)) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Profrmz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n // In distributed mode, proxy to the upstream Replication Manager\n if (config.changeStreamer.uri) {\n const upstreamURL = new URL('/profz', config.changeStreamer.uri);\n const incomingURL = new URL(req.url, 'http://localhost');\n for (const [key, value] of incomingURL.searchParams.entries()) {\n upstreamURL.searchParams.set(key, value);\n }\n\n try {\n const headers: Record<string, string> = {};\n if (req.headers.authorization) {\n headers.authorization = req.headers.authorization;\n }\n const response = await fetch(upstreamURL.toString(), {\n method: 'GET',\n headers,\n });\n\n res.code(response.status);\n const contentType = response.headers.get('content-type');\n if (contentType) {\n res.header('content-type', contentType);\n }\n const contentDisposition = response.headers.get('content-disposition');\n if (contentDisposition) {\n res.header('content-disposition', contentDisposition);\n }\n\n const body = await response.text();\n return res.send(body);\n } catch (err) {\n lc.error?.(`Failed to proxy /profrmz to RM at ${upstreamURL}:`, err);\n return res.code(502).send({\n error: `Failed to proxy profiling request to RM at ${upstreamURL}: ${String(err)}`,\n });\n }\n }\n\n // In single-node mode, profile local change-streamer / RM process\n return handleProfzRequest(lc, config, req, res, getWorker, 'change-streamer');\n}\n"],"mappings":"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"}
|
|
@@ -10,6 +10,13 @@ import type { PlanDebugger } from '../../../zql/src/planner/planner-debug.ts';
|
|
|
10
10
|
import type { Database } from '../../../zqlite/src/db.ts';
|
|
11
11
|
import type { JWTAuth } from '../auth/auth.ts';
|
|
12
12
|
import type { LiteAndZqlSpec } from '../db/specs.ts';
|
|
13
|
+
/**
|
|
14
|
+
* Maximum number of synced rows returned per table, and read rows returned
|
|
15
|
+
* per table/query, when analyzing a query in zero-cache. Rows past this are
|
|
16
|
+
* still counted. Keeping every row can exhaust the heap when analyzing queries
|
|
17
|
+
* over large tables.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MAX_ANALYZE_ROWS = 1000;
|
|
13
20
|
export type RunAstOptions = {
|
|
14
21
|
applyPermissions?: boolean | undefined;
|
|
15
22
|
auth?: JWTAuth | undefined;
|
|
@@ -17,6 +24,11 @@ export type RunAstOptions = {
|
|
|
17
24
|
costModel?: ConnectionCostModel | undefined;
|
|
18
25
|
db: Database;
|
|
19
26
|
host: BuilderDelegate;
|
|
27
|
+
/**
|
|
28
|
+
* Maximum number of synced rows returned per table. Rows past this are still
|
|
29
|
+
* counted. Defaults to no limit.
|
|
30
|
+
*/
|
|
31
|
+
maxSyncedRowsPerTable?: number | undefined;
|
|
20
32
|
permissions?: PermissionsConfig | undefined;
|
|
21
33
|
planDebugger?: PlanDebugger | undefined;
|
|
22
34
|
syncedRows?: boolean | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-ast.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/run-ast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAQjD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,oDAAoD,CAAC;AAC3F,OAAO,KAAK,EAAC,GAAG,EAAe,MAAM,mCAAmC,CAAC;AAEzE,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,6CAA6C,CAAC;AAG9E,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,kDAAkD,CAAC;AACxF,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,yCAAyC,CAAC;AACxE,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,qCAAqC,CAAC;AAI7C,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,gDAAgD,CAAC;AACxF,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,2CAA2C,CAAC;AAC5E,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,2BAA2B,CAAC;AAExD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAG7C,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,gBAAgB,CAAC;AAGnD,MAAM,MAAM,aAAa,GAAG;IAC1B,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,oBAAoB,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IAC9C,SAAS,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAC5C,EAAE,EAAE,QAAQ,CAAC;IACb,IAAI,EAAE,eAAe,CAAC;IACtB,WAAW,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAC5C,YAAY,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IACxC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACxC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAClC,CAAC;AAEF,wBAAsB,MAAM,CAC1B,EAAE,EAAE,UAAU,EACd,YAAY,EAAE,YAAY,EAC1B,GAAG,EAAE,GAAG,EACR,aAAa,EAAE,OAAO,EACtB,OAAO,EAAE,aAAa,EACtB,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAChC,OAAO,CAAC,kBAAkB,CAAC,
|
|
1
|
+
{"version":3,"file":"run-ast.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/run-ast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAQjD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,oDAAoD,CAAC;AAC3F,OAAO,KAAK,EAAC,GAAG,EAAe,MAAM,mCAAmC,CAAC;AAEzE,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,6CAA6C,CAAC;AAG9E,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,kDAAkD,CAAC;AACxF,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,yCAAyC,CAAC;AACxE,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,qCAAqC,CAAC;AAI7C,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,gDAAgD,CAAC;AACxF,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,2CAA2C,CAAC;AAC5E,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,2BAA2B,CAAC;AAExD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAG7C,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,gBAAgB,CAAC;AAGnD;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,MAAM,MAAM,aAAa,GAAG;IAC1B,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,oBAAoB,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IAC9C,SAAS,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAC5C,EAAE,EAAE,QAAQ,CAAC;IACb,IAAI,EAAE,eAAe,CAAC;IACtB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,WAAW,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAC5C,YAAY,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IACxC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACxC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAClC,CAAC;AAEF,wBAAsB,MAAM,CAC1B,EAAE,EAAE,UAAU,EACd,YAAY,EAAE,YAAY,EAC1B,GAAG,EAAE,GAAG,EACR,aAAa,EAAE,OAAO,EACtB,OAAO,EAAE,aAAa,EACtB,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAChC,OAAO,CAAC,kBAAkB,CAAC,CA6K7B"}
|
|
@@ -12,8 +12,15 @@ import { resolveSimpleScalarSubqueries } from "../../../zqlite/src/resolve-scala
|
|
|
12
12
|
import { transformAndHashQuery } from "../auth/read-authorizer.js";
|
|
13
13
|
import { hydrate } from "./view-syncer/pipeline-driver.js";
|
|
14
14
|
//#region ../zero-cache/src/services/run-ast.ts
|
|
15
|
+
/**
|
|
16
|
+
* Maximum number of synced rows returned per table, and read rows returned
|
|
17
|
+
* per table/query, when analyzing a query in zero-cache. Rows past this are
|
|
18
|
+
* still counted. Keeping every row can exhaust the heap when analyzing queries
|
|
19
|
+
* over large tables.
|
|
20
|
+
*/
|
|
21
|
+
var MAX_ANALYZE_ROWS = 1e3;
|
|
15
22
|
async function runAst(lc, clientSchema, ast, isTransformed, options, yieldProcess) {
|
|
16
|
-
const { clientToServerMapper, permissions, host, db } = options;
|
|
23
|
+
const { clientToServerMapper, permissions, host, db, maxSyncedRowsPerTable = Infinity } = options;
|
|
17
24
|
const result = {
|
|
18
25
|
warnings: [],
|
|
19
26
|
syncedRows: void 0,
|
|
@@ -46,7 +53,8 @@ async function runAst(lc, clientSchema, ast, isTransformed, options, yieldProces
|
|
|
46
53
|
const start = performance.now();
|
|
47
54
|
let syncedRowCount = 0;
|
|
48
55
|
const rowsByTable = {};
|
|
49
|
-
const
|
|
56
|
+
const seen = /* @__PURE__ */ new Set();
|
|
57
|
+
const truncatedTables = /* @__PURE__ */ new Set();
|
|
50
58
|
for (const rowChange of hydrate(pipeline, hashOfAST(resolvedAst), clientSchema, computeZqlSpecs(lc, db, { includeBackfillingColumns: false }))) {
|
|
51
59
|
if (rowChange === "yield") {
|
|
52
60
|
await yieldProcess();
|
|
@@ -55,21 +63,21 @@ async function runAst(lc, clientSchema, ast, isTransformed, options, yieldProces
|
|
|
55
63
|
assert(rowChange.type === 0, "Hydration only handles add row changes");
|
|
56
64
|
if (syncedRowCount % 10 === 0) await Promise.resolve();
|
|
57
65
|
if (syncedRowCount % 100 === 0) await sleep(1);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (seenByTable.has(s)) continue;
|
|
66
|
+
const key = JSON.stringify([rowChange.table, rowChange.rowKey]);
|
|
67
|
+
if (seen.has(key)) continue;
|
|
61
68
|
syncedRowCount++;
|
|
62
|
-
|
|
69
|
+
seen.add(key);
|
|
63
70
|
if (options.syncedRows) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
rows.push(rowChange.row);
|
|
71
|
+
const rows = rowsByTable[rowChange.table] ??= [];
|
|
72
|
+
if (rows.length < maxSyncedRowsPerTable) rows.push(rowChange.row);
|
|
73
|
+
else truncatedTables.add(rowChange.table);
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
const end = performance.now();
|
|
72
|
-
if (options.syncedRows)
|
|
77
|
+
if (options.syncedRows) {
|
|
78
|
+
result.syncedRows = rowsByTable;
|
|
79
|
+
for (const table of truncatedTables) result.warnings.push(`Only the first ${maxSyncedRowsPerTable} synced rows of "${table}" are included.`);
|
|
80
|
+
}
|
|
73
81
|
result.start = start;
|
|
74
82
|
result.end = end;
|
|
75
83
|
result.elapsed = end - start;
|
|
@@ -80,10 +88,14 @@ async function runAst(lc, clientSchema, ast, isTransformed, options, yieldProces
|
|
|
80
88
|
result.readRowCount = readRowCount;
|
|
81
89
|
result.dbScansByQuery = host.debug?.getNVisitCounts() ?? {};
|
|
82
90
|
result.sqlitePlans = host.debug?.getSQLitePlans() ?? {};
|
|
83
|
-
if (options.vendedRows)
|
|
91
|
+
if (options.vendedRows) {
|
|
92
|
+
const readRows = host.debug?.getVendedRows();
|
|
93
|
+
result.readRows = readRows;
|
|
94
|
+
for (const [table, byQuery] of Object.entries(readRows ?? {})) for (const [query, rows] of Object.entries(byQuery)) if ((result.readRowCountsByQuery[table]?.[query] ?? 0) > rows.length) result.warnings.push(`Only the first ${rows.length} read rows of "${table}" are included for: ${query}`);
|
|
95
|
+
}
|
|
84
96
|
return result;
|
|
85
97
|
}
|
|
86
98
|
//#endregion
|
|
87
|
-
export { runAst };
|
|
99
|
+
export { MAX_ANALYZE_ROWS, runAst };
|
|
88
100
|
|
|
89
101
|
//# sourceMappingURL=run-ast.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-ast.js","names":[],"sources":["../../../../../zero-cache/src/services/run-ast.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\n// @circular-dep-ignore\nimport {astToZQL} from '../../../ast-to-zql/src/ast-to-zql.ts';\n// @circular-dep-ignore\nimport {formatOutput} from '../../../ast-to-zql/src/format.ts';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport type {AnalyzeQueryResult} from '../../../zero-protocol/src/analyze-query-result.ts';\nimport type {AST, LiteralValue} from '../../../zero-protocol/src/ast.ts';\nimport {mapAST} from '../../../zero-protocol/src/ast.ts';\nimport type {ClientSchema} from '../../../zero-protocol/src/client-schema.ts';\nimport type {Row} from '../../../zero-protocol/src/data.ts';\nimport {hashOfAST} from '../../../zero-protocol/src/query-hash.ts';\nimport type {PermissionsConfig} from '../../../zero-schema/src/compiled-permissions.ts';\nimport type {NameMapper} from '../../../zero-schema/src/name-mapper.ts';\nimport {\n buildPipeline,\n type BuilderDelegate,\n} from '../../../zql/src/builder/builder.ts';\nimport {ChangeType} from '../../../zql/src/ivm/change-type.ts';\nimport type {Node} from '../../../zql/src/ivm/data.ts';\nimport {skipYields} from '../../../zql/src/ivm/operator.ts';\nimport type {ConnectionCostModel} from '../../../zql/src/planner/planner-connection.ts';\nimport type {PlanDebugger} from '../../../zql/src/planner/planner-debug.ts';\nimport type {Database} from '../../../zqlite/src/db.ts';\nimport {resolveSimpleScalarSubqueries} from '../../../zqlite/src/resolve-scalar-subqueries.ts';\nimport type {JWTAuth} from '../auth/auth.ts';\nimport {transformAndHashQuery} from '../auth/read-authorizer.ts';\nimport {computeZqlSpecs} from '../db/lite-tables.ts';\nimport type {LiteAndZqlSpec} from '../db/specs.ts';\nimport {hydrate} from './view-syncer/pipeline-driver.ts';\n\nexport type RunAstOptions = {\n applyPermissions?: boolean | undefined;\n auth?: JWTAuth | undefined;\n clientToServerMapper?: NameMapper | undefined;\n costModel?: ConnectionCostModel | undefined;\n db: Database;\n host: BuilderDelegate;\n permissions?: PermissionsConfig | undefined;\n planDebugger?: PlanDebugger | undefined;\n syncedRows?: boolean | undefined;\n tableSpecs: Map<string, LiteAndZqlSpec>;\n vendedRows?: boolean | undefined;\n};\n\nexport async function runAst(\n lc: LogContext,\n clientSchema: ClientSchema,\n ast: AST,\n isTransformed: boolean,\n options: RunAstOptions,\n yieldProcess: () => Promise<void>,\n): Promise<AnalyzeQueryResult> {\n const {clientToServerMapper, permissions, host, db} = options;\n const result: AnalyzeQueryResult = {\n warnings: [],\n syncedRows: undefined,\n syncedRowCount: 0,\n start: 0,\n end: 0,\n elapsed: 0,\n afterPermissions: undefined,\n readRows: undefined,\n readRowCountsByQuery: {},\n readRowCount: undefined,\n };\n\n if (!isTransformed) {\n // map the AST to server names if not already transformed\n ast = mapAST(ast, must(clientToServerMapper));\n }\n if (options.applyPermissions) {\n const auth = options.auth;\n if (!auth) {\n result.warnings.push(\n 'No auth data provided. Permission rules will compare to `NULL` wherever an auth data field is referenced.',\n );\n }\n ast = transformAndHashQuery(\n lc,\n 'clientGroupIDForAnalyze',\n ast,\n must(permissions),\n auth,\n false,\n ).transformedAst;\n result.afterPermissions = await formatOutput(ast.table + astToZQL(ast));\n }\n\n // Resolve scalar subqueries (e.g. whereExists with {scalar: true}) to\n // literal equality conditions so that SQLite can use indexes effectively.\n // Without this, correlated subqueries get stripped from SQL filters and\n // queries on large tables fall back to full table scans.\n const executor = (\n subqueryAST: AST,\n childField: string,\n ): LiteralValue | null | undefined => {\n const input = buildPipeline(subqueryAST, host, 'scalar-subquery');\n // Consume the full stream rather than using first() to avoid\n // triggering early return on Take's #initialFetch assertion.\n // The subquery AST already has limit: 1, so at most one row is produced.\n let node: Node | undefined;\n for (const n of skipYields(input.fetch({}))) {\n node ??= n;\n }\n input.destroy();\n return node ? ((node.row[childField] as LiteralValue) ?? null) : undefined;\n };\n\n const {ast: resolvedAst, ignoredScalarHints} = resolveSimpleScalarSubqueries(\n ast,\n options.tableSpecs,\n executor,\n );\n for (const {table, uniqueKeys} of ignoredScalarHints) {\n lc.warn?.(\n `Ignoring {scalar: true} on the \"${table}\" subquery: it does not ` +\n `constrain every column of any unique key ` +\n `[${uniqueKeys.map(k => `(${k.join(', ')})`).join(', ')}] to a ` +\n `literal with \"=\", so it is not provably limited to one row. ` +\n `The gate runs as a plain EXISTS.`,\n );\n }\n\n const pipeline = buildPipeline(\n resolvedAst,\n host,\n 'query-id',\n options.costModel,\n lc,\n options.planDebugger,\n );\n\n const start = performance.now();\n\n let syncedRowCount = 0;\n const rowsByTable: Record<string, Row[]> = {};\n const seenByTable: Set<string> = new Set();\n for (const rowChange of hydrate(\n pipeline,\n hashOfAST(resolvedAst),\n clientSchema,\n computeZqlSpecs(lc, db, {includeBackfillingColumns: false}),\n )) {\n if (rowChange === 'yield') {\n await yieldProcess();\n continue;\n }\n assert(\n rowChange.type === ChangeType.ADD,\n 'Hydration only handles add row changes',\n );\n\n // yield to other tasks to avoid blocking for too long\n if (syncedRowCount % 10 === 0) {\n await Promise.resolve();\n }\n if (syncedRowCount % 100 === 0) {\n await sleep(1);\n }\n\n let rows: Row[] = rowsByTable[rowChange.table];\n const s = rowChange.table + '.' + JSON.stringify(rowChange.row);\n if (seenByTable.has(s)) {\n continue; // skip duplicates\n }\n syncedRowCount++;\n seenByTable.add(s);\n if (options.syncedRows) {\n if (!rows) {\n rows = [];\n rowsByTable[rowChange.table] = rows;\n }\n rows.push(rowChange.row);\n }\n }\n\n const end = performance.now();\n if (options.syncedRows) {\n result.syncedRows = rowsByTable;\n }\n result.start = start;\n result.end = end;\n result.elapsed = end - start;\n\n // Always include the count of synced and vended rows.\n result.syncedRowCount = syncedRowCount;\n result.readRowCountsByQuery = host.debug?.getVendedRowCounts() ?? {};\n let readRowCount = 0;\n for (const c of Object.values(result.readRowCountsByQuery)) {\n for (const v of Object.values(c)) {\n readRowCount += v;\n }\n }\n result.readRowCount = readRowCount;\n result.dbScansByQuery = host.debug?.getNVisitCounts() ?? {};\n result.sqlitePlans = host.debug?.getSQLitePlans() ?? {};\n\n if (options.vendedRows) {\n result.readRows = host.debug?.getVendedRows();\n }\n return result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"run-ast.js","names":[],"sources":["../../../../../zero-cache/src/services/run-ast.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\n// @circular-dep-ignore\nimport {astToZQL} from '../../../ast-to-zql/src/ast-to-zql.ts';\n// @circular-dep-ignore\nimport {formatOutput} from '../../../ast-to-zql/src/format.ts';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport type {AnalyzeQueryResult} from '../../../zero-protocol/src/analyze-query-result.ts';\nimport type {AST, LiteralValue} from '../../../zero-protocol/src/ast.ts';\nimport {mapAST} from '../../../zero-protocol/src/ast.ts';\nimport type {ClientSchema} from '../../../zero-protocol/src/client-schema.ts';\nimport type {Row} from '../../../zero-protocol/src/data.ts';\nimport {hashOfAST} from '../../../zero-protocol/src/query-hash.ts';\nimport type {PermissionsConfig} from '../../../zero-schema/src/compiled-permissions.ts';\nimport type {NameMapper} from '../../../zero-schema/src/name-mapper.ts';\nimport {\n buildPipeline,\n type BuilderDelegate,\n} from '../../../zql/src/builder/builder.ts';\nimport {ChangeType} from '../../../zql/src/ivm/change-type.ts';\nimport type {Node} from '../../../zql/src/ivm/data.ts';\nimport {skipYields} from '../../../zql/src/ivm/operator.ts';\nimport type {ConnectionCostModel} from '../../../zql/src/planner/planner-connection.ts';\nimport type {PlanDebugger} from '../../../zql/src/planner/planner-debug.ts';\nimport type {Database} from '../../../zqlite/src/db.ts';\nimport {resolveSimpleScalarSubqueries} from '../../../zqlite/src/resolve-scalar-subqueries.ts';\nimport type {JWTAuth} from '../auth/auth.ts';\nimport {transformAndHashQuery} from '../auth/read-authorizer.ts';\nimport {computeZqlSpecs} from '../db/lite-tables.ts';\nimport type {LiteAndZqlSpec} from '../db/specs.ts';\nimport {hydrate} from './view-syncer/pipeline-driver.ts';\n\n/**\n * Maximum number of synced rows returned per table, and read rows returned\n * per table/query, when analyzing a query in zero-cache. Rows past this are\n * still counted. Keeping every row can exhaust the heap when analyzing queries\n * over large tables.\n */\nexport const MAX_ANALYZE_ROWS = 1000;\n\nexport type RunAstOptions = {\n applyPermissions?: boolean | undefined;\n auth?: JWTAuth | undefined;\n clientToServerMapper?: NameMapper | undefined;\n costModel?: ConnectionCostModel | undefined;\n db: Database;\n host: BuilderDelegate;\n /**\n * Maximum number of synced rows returned per table. Rows past this are still\n * counted. Defaults to no limit.\n */\n maxSyncedRowsPerTable?: number | undefined;\n permissions?: PermissionsConfig | undefined;\n planDebugger?: PlanDebugger | undefined;\n syncedRows?: boolean | undefined;\n tableSpecs: Map<string, LiteAndZqlSpec>;\n vendedRows?: boolean | undefined;\n};\n\nexport async function runAst(\n lc: LogContext,\n clientSchema: ClientSchema,\n ast: AST,\n isTransformed: boolean,\n options: RunAstOptions,\n yieldProcess: () => Promise<void>,\n): Promise<AnalyzeQueryResult> {\n const {\n clientToServerMapper,\n permissions,\n host,\n db,\n maxSyncedRowsPerTable = Infinity,\n } = options;\n const result: AnalyzeQueryResult = {\n warnings: [],\n syncedRows: undefined,\n syncedRowCount: 0,\n start: 0,\n end: 0,\n elapsed: 0,\n afterPermissions: undefined,\n readRows: undefined,\n readRowCountsByQuery: {},\n readRowCount: undefined,\n };\n\n if (!isTransformed) {\n // map the AST to server names if not already transformed\n ast = mapAST(ast, must(clientToServerMapper));\n }\n if (options.applyPermissions) {\n const auth = options.auth;\n if (!auth) {\n result.warnings.push(\n 'No auth data provided. Permission rules will compare to `NULL` wherever an auth data field is referenced.',\n );\n }\n ast = transformAndHashQuery(\n lc,\n 'clientGroupIDForAnalyze',\n ast,\n must(permissions),\n auth,\n false,\n ).transformedAst;\n result.afterPermissions = await formatOutput(ast.table + astToZQL(ast));\n }\n\n // Resolve scalar subqueries (e.g. whereExists with {scalar: true}) to\n // literal equality conditions so that SQLite can use indexes effectively.\n // Without this, correlated subqueries get stripped from SQL filters and\n // queries on large tables fall back to full table scans.\n const executor = (\n subqueryAST: AST,\n childField: string,\n ): LiteralValue | null | undefined => {\n const input = buildPipeline(subqueryAST, host, 'scalar-subquery');\n // Consume the full stream rather than using first() to avoid\n // triggering early return on Take's #initialFetch assertion.\n // The subquery AST already has limit: 1, so at most one row is produced.\n let node: Node | undefined;\n for (const n of skipYields(input.fetch({}))) {\n node ??= n;\n }\n input.destroy();\n return node ? ((node.row[childField] as LiteralValue) ?? null) : undefined;\n };\n\n const {ast: resolvedAst, ignoredScalarHints} = resolveSimpleScalarSubqueries(\n ast,\n options.tableSpecs,\n executor,\n );\n for (const {table, uniqueKeys} of ignoredScalarHints) {\n lc.warn?.(\n `Ignoring {scalar: true} on the \"${table}\" subquery: it does not ` +\n `constrain every column of any unique key ` +\n `[${uniqueKeys.map(k => `(${k.join(', ')})`).join(', ')}] to a ` +\n `literal with \"=\", so it is not provably limited to one row. ` +\n `The gate runs as a plain EXISTS.`,\n );\n }\n\n const pipeline = buildPipeline(\n resolvedAst,\n host,\n 'query-id',\n options.costModel,\n lc,\n options.planDebugger,\n );\n\n const start = performance.now();\n\n let syncedRowCount = 0;\n const rowsByTable: Record<string, Row[]> = {};\n // Dedupe on the row key rather than the whole row to bound memory.\n const seen: Set<string> = new Set();\n const truncatedTables: Set<string> = new Set();\n for (const rowChange of hydrate(\n pipeline,\n hashOfAST(resolvedAst),\n clientSchema,\n computeZqlSpecs(lc, db, {includeBackfillingColumns: false}),\n )) {\n if (rowChange === 'yield') {\n await yieldProcess();\n continue;\n }\n assert(\n rowChange.type === ChangeType.ADD,\n 'Hydration only handles add row changes',\n );\n\n // yield to other tasks to avoid blocking for too long\n if (syncedRowCount % 10 === 0) {\n await Promise.resolve();\n }\n if (syncedRowCount % 100 === 0) {\n await sleep(1);\n }\n\n const key = JSON.stringify([rowChange.table, rowChange.rowKey]);\n if (seen.has(key)) {\n continue; // skip duplicates\n }\n syncedRowCount++;\n seen.add(key);\n if (options.syncedRows) {\n const rows = (rowsByTable[rowChange.table] ??= []);\n if (rows.length < maxSyncedRowsPerTable) {\n rows.push(rowChange.row);\n } else {\n truncatedTables.add(rowChange.table);\n }\n }\n }\n\n const end = performance.now();\n if (options.syncedRows) {\n result.syncedRows = rowsByTable;\n for (const table of truncatedTables) {\n result.warnings.push(\n `Only the first ${maxSyncedRowsPerTable} synced rows of \"${table}\" are included.`,\n );\n }\n }\n result.start = start;\n result.end = end;\n result.elapsed = end - start;\n\n // Always include the count of synced and vended rows.\n result.syncedRowCount = syncedRowCount;\n result.readRowCountsByQuery = host.debug?.getVendedRowCounts() ?? {};\n let readRowCount = 0;\n for (const c of Object.values(result.readRowCountsByQuery)) {\n for (const v of Object.values(c)) {\n readRowCount += v;\n }\n }\n result.readRowCount = readRowCount;\n result.dbScansByQuery = host.debug?.getNVisitCounts() ?? {};\n result.sqlitePlans = host.debug?.getSQLitePlans() ?? {};\n\n if (options.vendedRows) {\n const readRows = host.debug?.getVendedRows();\n result.readRows = readRows;\n for (const [table, byQuery] of Object.entries(readRows ?? {})) {\n for (const [query, rows] of Object.entries(byQuery)) {\n if ((result.readRowCountsByQuery[table]?.[query] ?? 0) > rows.length) {\n result.warnings.push(\n `Only the first ${rows.length} read rows of \"${table}\" are included for: ${query}`,\n );\n }\n }\n }\n }\n return result;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAE1C;;;GAGG;AACH,qBAAa,aAAa,CAAC,CAAC,SAAS,OAAO;;gBAOxC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,CAAC,EAC1B,OAAO,GAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,OAAoB;IAOhD;;;OAGG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAE1C;;;GAGG;AACH,qBAAa,aAAa,CAAC,CAAC,SAAS,OAAO;;gBAOxC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,CAAC,EAC1B,OAAO,GAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,OAAoB;IAOhD;;;OAGG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC;IA6BzB,IAAI,IAAI,WAEP;IAED,WAAW,IAAI,QAAQ,CAAC,CAAC,CAAC;CAG3B"}
|
|
@@ -23,7 +23,7 @@ var ServiceRunner = class {
|
|
|
23
23
|
const service = this.#create(id);
|
|
24
24
|
this.#instances.set(id, service);
|
|
25
25
|
service.run().catch((e) => this.#lc.error?.(`Error running ${service.constructor?.name} ${service.id}`, e)).finally(() => {
|
|
26
|
-
this.#instances.delete(id);
|
|
26
|
+
if (this.#instances.get(id) === service) this.#instances.delete(id);
|
|
27
27
|
});
|
|
28
28
|
return service;
|
|
29
29
|
}
|