@rocicorp/zero 1.5.0-canary.0 → 1.5.0-canary.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/analyze-query/src/analyze-cli.js +2 -2
- package/out/analyze-query/src/analyze-cli.js.map +1 -1
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +1 -1
- package/out/zero-cache/src/server/change-streamer.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-streamer/change-streamer-http.d.ts +1 -2
- 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 +5 -12
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +2 -0
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +9 -2
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -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 +4 -0
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/workers/connection.js +2 -2
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-server/src/mod.d.ts +2 -2
- package/out/zero-server/src/process-mutations.d.ts +41 -32
- package/out/zero-server/src/process-mutations.d.ts.map +1 -1
- package/out/zero-server/src/process-mutations.js +49 -29
- package/out/zero-server/src/process-mutations.js.map +1 -1
- package/out/zero-server/src/queries/process-queries.d.ts +40 -18
- package/out/zero-server/src/queries/process-queries.d.ts.map +1 -1
- package/out/zero-server/src/queries/process-queries.js +37 -19
- package/out/zero-server/src/queries/process-queries.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { isProtocolError } from "../../../zero-protocol/src/error.js";
|
|
|
6
6
|
import "../../../zero-protocol/src/protocol-version.js";
|
|
7
7
|
import { ProtocolErrorWithLevel, getLogLevel, wrapWithProtocolError } from "../types/error-with-level.js";
|
|
8
8
|
import { upstreamSchema } from "../../../zero-protocol/src/up.js";
|
|
9
|
-
import WebSocket
|
|
9
|
+
import WebSocket, { createWebSocketStream } from "ws";
|
|
10
10
|
import { Readable, Writable, pipeline } from "node:stream";
|
|
11
11
|
//#region ../zero-cache/src/workers/connection.ts
|
|
12
12
|
var DOWNSTREAM_MSG_INTERVAL_MS = 6e3;
|
|
@@ -180,7 +180,7 @@ var Connection = class {
|
|
|
180
180
|
}
|
|
181
181
|
};
|
|
182
182
|
function send(lc, ws, data, callback) {
|
|
183
|
-
if (ws.readyState === WebSocket
|
|
183
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(data), callback === "ignore-backpressure" ? void 0 : callback);
|
|
184
184
|
else {
|
|
185
185
|
lc.debug?.(`Dropping outbound message on ws (state: ${ws.readyState})`, { dropped: data });
|
|
186
186
|
if (callback !== "ignore-backpressure") callback(new ProtocolErrorWithLevel({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","names":["#ws","#wsID","#protocolVersion","#lc","#onClose","#messageHandler","#downstreamMsgTimer","#handleClose","#handleError","#proxyInbound","#maybeSendPong","#closeWithError","#closed","#viewSyncerOutboundStream","#pusherOutboundStream","#handleMessage","#handleMessageResult","#closeWithThrown","#proxyOutbound","#lastDownstreamMsgTime"],"sources":["../../../../../zero-cache/src/workers/connection.ts"],"sourcesContent":["import {pipeline, Readable, Writable} from 'node:stream';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport type {CloseEvent, Data, ErrorEvent} from 'ws';\nimport WebSocket, {createWebSocketStream} from 'ws';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport * as valita from '../../../shared/src/valita.ts';\nimport type {ConnectedMessage} from '../../../zero-protocol/src/connect.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 type {ErrorBody} from '../../../zero-protocol/src/error.ts';\nimport {\n isProtocolError,\n type ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {\n MIN_SERVER_SUPPORTED_SYNC_PROTOCOL,\n PROTOCOL_VERSION,\n} from '../../../zero-protocol/src/protocol-version.ts';\nimport {upstreamSchema, type Upstream} from '../../../zero-protocol/src/up.ts';\nimport {\n ProtocolErrorWithLevel,\n getLogLevel,\n wrapWithProtocolError,\n} from '../types/error-with-level.ts';\nimport type {Source} from '../types/streams.ts';\nimport type {ConnectParams} from './connect-params.ts';\n\nexport type HandlerResult =\n | {\n type: 'ok';\n }\n | {\n type: 'fatal';\n error: ErrorBody;\n }\n | {\n type: 'transient';\n errors: ErrorBody[];\n }\n | StreamResult;\n\nexport type StreamResult = {\n type: 'stream';\n source: 'viewSyncer' | 'pusher';\n stream: Source<Downstream>;\n};\n\nexport interface MessageHandler {\n handleMessage(msg: Upstream): Promise<HandlerResult[]>;\n}\n\n// Ensures that a downstream message is sent at least every interval, sending a\n// 'pong' if necessary. This is set to be slightly longer than the client-side\n// PING_INTERVAL of 5 seconds, so that in the common case, 'pong's are sent in\n// response to client-initiated 'ping's. However, if the inbound stream is\n// backed up because a command is taking a long time to process, the pings\n// will be stuck in the queue (i.e. back-pressured), in which case pongs will\n// be manually sent to notify the client of server liveness.\n//\n// This is equivalent to what is done for Postgres keepalives on the\n// replication stream (which can similarly be back-pressured):\n// https://github.com/rocicorp/mono/blob/f98cb369a2dbb15650328859c732db358f187ef0/packages/zero-cache/src/services/change-source/pg/logical-replication/stream.ts#L21\nconst DOWNSTREAM_MSG_INTERVAL_MS = 6_000;\n\n/**\n * Represents a connection between the client and server.\n *\n * Handles incoming messages on the connection and dispatches\n * them to the correct service.\n *\n * Listens to the ViewSyncer and sends messages to the client.\n */\nexport class Connection {\n readonly #ws: WebSocket;\n readonly #wsID: string;\n readonly #protocolVersion: number;\n readonly #lc: LogContext;\n readonly #onClose: () => void;\n readonly #messageHandler: MessageHandler;\n readonly #downstreamMsgTimer: NodeJS.Timeout | undefined;\n\n #viewSyncerOutboundStream: Source<Downstream> | undefined;\n #pusherOutboundStream: Source<Downstream> | undefined;\n #closed = false;\n\n constructor(\n lc: LogContext,\n connectParams: ConnectParams,\n ws: WebSocket,\n messageHandler: MessageHandler,\n onClose: () => void,\n ) {\n const {clientGroupID, clientID, wsID, protocolVersion} = connectParams;\n this.#messageHandler = messageHandler;\n\n this.#ws = ws;\n this.#wsID = wsID;\n this.#protocolVersion = protocolVersion;\n\n this.#lc = lc\n .withContext('connection')\n .withContext('clientID', clientID)\n .withContext('clientGroupID', clientGroupID)\n .withContext('wsID', wsID);\n this.#lc.debug?.('new connection');\n this.#onClose = onClose;\n\n this.#ws.addEventListener('close', this.#handleClose);\n this.#ws.addEventListener('error', this.#handleError);\n\n this.#proxyInbound();\n this.#downstreamMsgTimer = setInterval(\n this.#maybeSendPong,\n DOWNSTREAM_MSG_INTERVAL_MS / 2,\n );\n }\n\n /**\n * Checks the protocol version and errors for unsupported protocols,\n * sending the initial `connected` response on success.\n *\n * This is early in the connection lifecycle because {@link #handleMessage}\n * will only parse messages with schema(s) of supported protocol versions.\n */\n init(): boolean {\n if (\n this.#protocolVersion > PROTOCOL_VERSION ||\n this.#protocolVersion < MIN_SERVER_SUPPORTED_SYNC_PROTOCOL\n ) {\n this.#closeWithError({\n kind: ErrorKind.VersionNotSupported,\n message: `server is at sync protocol v${PROTOCOL_VERSION} and does not support v${\n this.#protocolVersion\n }. The ${\n this.#protocolVersion > PROTOCOL_VERSION ? 'server' : 'client'\n } must be updated to a newer release.`,\n origin: ErrorOrigin.ZeroCache,\n });\n } else {\n const connectedMessage: ConnectedMessage = [\n 'connected',\n {wsid: this.#wsID, timestamp: Date.now()},\n ];\n this.send(connectedMessage, 'ignore-backpressure');\n return true;\n }\n return false;\n }\n\n close(reason: string, ...args: unknown[]) {\n if (this.#closed) {\n return;\n }\n this.#closed = true;\n this.#lc.info?.(`closing connection: ${reason}`, ...args);\n this.#ws.removeEventListener('close', this.#handleClose);\n this.#ws.removeEventListener('error', this.#handleError);\n this.#viewSyncerOutboundStream?.cancel();\n this.#viewSyncerOutboundStream = undefined;\n this.#pusherOutboundStream?.cancel();\n this.#pusherOutboundStream = undefined;\n this.#onClose();\n if (this.#ws.readyState !== this.#ws.CLOSED) {\n this.#ws.close();\n }\n clearTimeout(this.#downstreamMsgTimer);\n\n // spin down services if we have\n // no more client connections for the client group?\n }\n\n handleInitConnection(initConnectionMsg: string) {\n return this.#handleMessage({data: initConnectionMsg});\n }\n\n #handleMessage = async (event: {data: Data}) => {\n const data = event.data.toString();\n if (this.#closed) {\n this.#lc.debug?.('Ignoring message received after closed', data);\n return;\n }\n\n let msg;\n try {\n const value = JSON.parse(data);\n msg = valita.parse(value, upstreamSchema);\n } catch (e) {\n const errorBody = {\n kind: ErrorKind.InvalidMessage,\n message: String(e),\n origin: ErrorOrigin.ZeroCache,\n } as const;\n this.#closeWithError(\n errorBody,\n new ProtocolErrorWithLevel(errorBody, 'warn'),\n );\n return;\n }\n\n try {\n const msgType = msg[0];\n if (msgType === 'ping') {\n this.send(['pong', {}], 'ignore-backpressure');\n return;\n }\n\n const result = await this.#messageHandler.handleMessage(msg);\n for (const r of result) {\n this.#handleMessageResult(r);\n }\n } catch (e) {\n this.#closeWithThrown(e);\n }\n };\n\n #handleMessageResult(result: HandlerResult): void {\n switch (result.type) {\n case 'fatal':\n this.#closeWithError(result.error);\n break;\n case 'ok':\n break;\n case 'stream': {\n switch (result.source) {\n case 'viewSyncer':\n assert(\n this.#viewSyncerOutboundStream === undefined,\n 'Outbound stream already set for this connection!',\n );\n this.#viewSyncerOutboundStream = result.stream;\n break;\n case 'pusher':\n assert(\n this.#pusherOutboundStream === undefined,\n 'Outbound stream already set for this connection!',\n );\n this.#pusherOutboundStream = result.stream;\n break;\n }\n this.#proxyOutbound(result.stream);\n break;\n }\n case 'transient': {\n for (const error of result.errors) {\n this.sendError(error);\n }\n }\n }\n }\n\n #handleClose = (e: CloseEvent) => {\n const {code, reason, wasClean} = e;\n this.close('WebSocket close event', {code, reason, wasClean});\n };\n\n #handleError = (e: ErrorEvent) => {\n this.#lc.error?.('WebSocket error event', e.message, e.error);\n };\n\n #proxyInbound() {\n pipeline(\n createWebSocketStream(this.#ws),\n new Writable({\n write: (data, _encoding, callback) => {\n this.#handleMessage({data}).then(() => callback(), callback);\n },\n }),\n // The done callback is not used, as #handleClose and #handleError,\n // configured on the underlying WebSocket, provide more complete\n // information.\n () => {},\n );\n }\n\n #proxyOutbound(outboundStream: Source<Downstream>) {\n // Note: createWebSocketStream() is avoided here in order to control\n // exception handling with #closeWithThrown(). If the Writable\n // from createWebSocketStream() were instead used, exceptions\n // from the outboundStream result in the Writable closing the\n // the websocket before the error message can be sent.\n pipeline(\n Readable.from(outboundStream),\n new Writable({\n objectMode: true,\n write: (downstream: Downstream, _encoding, callback) =>\n this.send(downstream, callback),\n }),\n e =>\n e\n ? this.#closeWithThrown(e)\n : this.close(`downstream closed by ViewSyncer`),\n );\n }\n\n #closeWithThrown(e: unknown) {\n const errorBody =\n findProtocolError(e)?.errorBody ?? wrapWithProtocolError(e).errorBody;\n\n this.#closeWithError(errorBody, e);\n }\n\n #closeWithError(errorBody: ErrorBody, thrown?: unknown) {\n this.sendError(errorBody, thrown);\n this.close(\n `${errorBody.kind} (${errorBody.origin}): ${errorBody.message}`,\n errorBody,\n );\n }\n\n #lastDownstreamMsgTime = Date.now();\n\n #maybeSendPong = () => {\n if (Date.now() - this.#lastDownstreamMsgTime > DOWNSTREAM_MSG_INTERVAL_MS) {\n this.#lc.debug?.('manually sending pong');\n this.send(['pong', {}], 'ignore-backpressure');\n }\n };\n\n send(\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n ) {\n this.#lastDownstreamMsgTime = Date.now();\n return send(this.#lc, this.#ws, data, callback);\n }\n\n sendError(errorBody: ErrorBody, thrown?: unknown) {\n sendError(this.#lc, this.#ws, errorBody, thrown);\n }\n}\n\nexport type WebSocketLike = Pick<WebSocket, 'readyState'> & {\n send(data: string, cb?: (err?: Error) => void): void;\n};\n\n// Exported for testing purposes.\nexport function send(\n lc: LogContext,\n ws: WebSocketLike,\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n) {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(\n JSON.stringify(data),\n callback === 'ignore-backpressure' ? undefined : callback,\n );\n } else {\n lc.debug?.(`Dropping outbound message on ws (state: ${ws.readyState})`, {\n dropped: data,\n });\n if (callback !== 'ignore-backpressure') {\n callback(\n new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: 'WebSocket closed',\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n ),\n );\n }\n }\n}\n\nexport function sendError(\n lc: LogContext,\n ws: WebSocket,\n errorBody: ErrorBody,\n thrown?: unknown,\n) {\n lc = lc.withContext('errorKind', errorBody.kind);\n\n let logLevel: LogLevel;\n\n // If the thrown error is a ProtocolErrorWithLevel, its explicit logLevel takes precedence\n if (thrown instanceof ProtocolErrorWithLevel) {\n logLevel = thrown.logLevel;\n }\n // Errors with errno or transient socket codes are low-level, transient I/O issues\n // (e.g., EPIPE, ECONNRESET) and should be warnings, not errors\n else if (\n hasErrno(thrown) ||\n hasTransientSocketCode(thrown) ||\n isTransientSocketMessage(errorBody.message)\n ) {\n logLevel = 'warn';\n }\n // Fallback: check errorBody.kind for errors that weren't thrown as ProtocolErrorWithLevel\n else if (\n errorBody.kind === ErrorKind.ClientNotFound ||\n errorBody.kind === ErrorKind.TransformFailed\n ) {\n logLevel = 'warn';\n } else {\n logLevel = thrown ? getLogLevel(thrown) : 'info';\n }\n\n lc[logLevel]?.('Sending error on WebSocket', errorBody, thrown ?? '');\n send(lc, ws, ['error', errorBody], 'ignore-backpressure');\n}\n\nexport function findProtocolError(error: unknown): ProtocolError | undefined {\n if (isProtocolError(error)) {\n return error;\n }\n if (error instanceof Error && error.cause) {\n return findProtocolError(error.cause);\n }\n return undefined;\n}\n\nfunction hasErrno(error: unknown): boolean {\n return Boolean(\n error &&\n typeof error === 'object' &&\n 'errno' in error &&\n typeof (error as {errno: unknown}).errno !== 'undefined',\n );\n}\n\n// System error codes that indicate transient socket conditions.\n// These are checked via the `code` property on errors.\nconst TRANSIENT_SOCKET_ERROR_CODES = new Set([\n 'EPIPE',\n 'ECONNRESET',\n 'ECANCELED',\n]);\n\n// Error messages that indicate transient socket conditions but don't have\n// standard error codes (e.g., WebSocket library errors).\nconst TRANSIENT_SOCKET_MESSAGE_PATTERNS = [\n 'socket was closed while data was being compressed',\n];\n\nfunction hasTransientSocketCode(error: unknown): boolean {\n if (!error || typeof error !== 'object') {\n return false;\n }\n const maybeCode =\n 'code' in error ? String((error as {code?: unknown}).code) : undefined;\n return Boolean(\n maybeCode && TRANSIENT_SOCKET_ERROR_CODES.has(maybeCode.toUpperCase()),\n );\n}\n\nfunction isTransientSocketMessage(message: string | undefined): boolean {\n if (!message) {\n return false;\n }\n const lower = message.toLowerCase();\n return TRANSIENT_SOCKET_MESSAGE_PATTERNS.some(pattern =>\n lower.includes(pattern),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"connection.js","names":["#ws","#wsID","#protocolVersion","#lc","#onClose","#messageHandler","#downstreamMsgTimer","#handleClose","#handleError","#proxyInbound","#maybeSendPong","#closeWithError","#closed","#viewSyncerOutboundStream","#pusherOutboundStream","#handleMessage","#handleMessageResult","#closeWithThrown","#proxyOutbound","#lastDownstreamMsgTime"],"sources":["../../../../../zero-cache/src/workers/connection.ts"],"sourcesContent":["import {pipeline, Readable, Writable} from 'node:stream';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport type {CloseEvent, Data, ErrorEvent} from 'ws';\nimport WebSocket, {createWebSocketStream} from 'ws';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport * as valita from '../../../shared/src/valita.ts';\nimport type {ConnectedMessage} from '../../../zero-protocol/src/connect.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 type {ErrorBody} from '../../../zero-protocol/src/error.ts';\nimport {\n isProtocolError,\n type ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {\n MIN_SERVER_SUPPORTED_SYNC_PROTOCOL,\n PROTOCOL_VERSION,\n} from '../../../zero-protocol/src/protocol-version.ts';\nimport {upstreamSchema, type Upstream} from '../../../zero-protocol/src/up.ts';\nimport {\n ProtocolErrorWithLevel,\n getLogLevel,\n wrapWithProtocolError,\n} from '../types/error-with-level.ts';\nimport type {Source} from '../types/streams.ts';\nimport type {ConnectParams} from './connect-params.ts';\n\nexport type HandlerResult =\n | {\n type: 'ok';\n }\n | {\n type: 'fatal';\n error: ErrorBody;\n }\n | {\n type: 'transient';\n errors: ErrorBody[];\n }\n | StreamResult;\n\nexport type StreamResult = {\n type: 'stream';\n source: 'viewSyncer' | 'pusher';\n stream: Source<Downstream>;\n};\n\nexport interface MessageHandler {\n handleMessage(msg: Upstream): Promise<HandlerResult[]>;\n}\n\n// Ensures that a downstream message is sent at least every interval, sending a\n// 'pong' if necessary. This is set to be slightly longer than the client-side\n// PING_INTERVAL of 5 seconds, so that in the common case, 'pong's are sent in\n// response to client-initiated 'ping's. However, if the inbound stream is\n// backed up because a command is taking a long time to process, the pings\n// will be stuck in the queue (i.e. back-pressured), in which case pongs will\n// be manually sent to notify the client of server liveness.\n//\n// This is equivalent to what is done for Postgres keepalives on the\n// replication stream (which can similarly be back-pressured):\n// https://github.com/rocicorp/mono/blob/f98cb369a2dbb15650328859c732db358f187ef0/packages/zero-cache/src/services/change-source/pg/logical-replication/stream.ts#L21\nconst DOWNSTREAM_MSG_INTERVAL_MS = 6_000;\n\n/**\n * Represents a connection between the client and server.\n *\n * Handles incoming messages on the connection and dispatches\n * them to the correct service.\n *\n * Listens to the ViewSyncer and sends messages to the client.\n */\nexport class Connection {\n readonly #ws: WebSocket;\n readonly #wsID: string;\n readonly #protocolVersion: number;\n readonly #lc: LogContext;\n readonly #onClose: () => void;\n readonly #messageHandler: MessageHandler;\n readonly #downstreamMsgTimer: NodeJS.Timeout | undefined;\n\n #viewSyncerOutboundStream: Source<Downstream> | undefined;\n #pusherOutboundStream: Source<Downstream> | undefined;\n #closed = false;\n\n constructor(\n lc: LogContext,\n connectParams: ConnectParams,\n ws: WebSocket,\n messageHandler: MessageHandler,\n onClose: () => void,\n ) {\n const {clientGroupID, clientID, wsID, protocolVersion} = connectParams;\n this.#messageHandler = messageHandler;\n\n this.#ws = ws;\n this.#wsID = wsID;\n this.#protocolVersion = protocolVersion;\n\n this.#lc = lc\n .withContext('connection')\n .withContext('clientID', clientID)\n .withContext('clientGroupID', clientGroupID)\n .withContext('wsID', wsID);\n this.#lc.debug?.('new connection');\n this.#onClose = onClose;\n\n this.#ws.addEventListener('close', this.#handleClose);\n this.#ws.addEventListener('error', this.#handleError);\n\n this.#proxyInbound();\n this.#downstreamMsgTimer = setInterval(\n this.#maybeSendPong,\n DOWNSTREAM_MSG_INTERVAL_MS / 2,\n );\n }\n\n /**\n * Checks the protocol version and errors for unsupported protocols,\n * sending the initial `connected` response on success.\n *\n * This is early in the connection lifecycle because {@link #handleMessage}\n * will only parse messages with schema(s) of supported protocol versions.\n */\n init(): boolean {\n if (\n this.#protocolVersion > PROTOCOL_VERSION ||\n this.#protocolVersion < MIN_SERVER_SUPPORTED_SYNC_PROTOCOL\n ) {\n this.#closeWithError({\n kind: ErrorKind.VersionNotSupported,\n message: `server is at sync protocol v${PROTOCOL_VERSION} and does not support v${\n this.#protocolVersion\n }. The ${\n this.#protocolVersion > PROTOCOL_VERSION ? 'server' : 'client'\n } must be updated to a newer release.`,\n origin: ErrorOrigin.ZeroCache,\n });\n } else {\n const connectedMessage: ConnectedMessage = [\n 'connected',\n {wsid: this.#wsID, timestamp: Date.now()},\n ];\n this.send(connectedMessage, 'ignore-backpressure');\n return true;\n }\n return false;\n }\n\n close(reason: string, ...args: unknown[]) {\n if (this.#closed) {\n return;\n }\n this.#closed = true;\n this.#lc.info?.(`closing connection: ${reason}`, ...args);\n this.#ws.removeEventListener('close', this.#handleClose);\n this.#ws.removeEventListener('error', this.#handleError);\n this.#viewSyncerOutboundStream?.cancel();\n this.#viewSyncerOutboundStream = undefined;\n this.#pusherOutboundStream?.cancel();\n this.#pusherOutboundStream = undefined;\n this.#onClose();\n if (this.#ws.readyState !== this.#ws.CLOSED) {\n this.#ws.close();\n }\n clearTimeout(this.#downstreamMsgTimer);\n\n // spin down services if we have\n // no more client connections for the client group?\n }\n\n handleInitConnection(initConnectionMsg: string) {\n return this.#handleMessage({data: initConnectionMsg});\n }\n\n #handleMessage = async (event: {data: Data}) => {\n const data = event.data.toString();\n if (this.#closed) {\n this.#lc.debug?.('Ignoring message received after closed', data);\n return;\n }\n\n let msg;\n try {\n const value = JSON.parse(data);\n msg = valita.parse(value, upstreamSchema);\n } catch (e) {\n const errorBody = {\n kind: ErrorKind.InvalidMessage,\n message: String(e),\n origin: ErrorOrigin.ZeroCache,\n } as const;\n this.#closeWithError(\n errorBody,\n new ProtocolErrorWithLevel(errorBody, 'warn'),\n );\n return;\n }\n\n try {\n const msgType = msg[0];\n if (msgType === 'ping') {\n this.send(['pong', {}], 'ignore-backpressure');\n return;\n }\n\n const result = await this.#messageHandler.handleMessage(msg);\n for (const r of result) {\n this.#handleMessageResult(r);\n }\n } catch (e) {\n this.#closeWithThrown(e);\n }\n };\n\n #handleMessageResult(result: HandlerResult): void {\n switch (result.type) {\n case 'fatal':\n this.#closeWithError(result.error);\n break;\n case 'ok':\n break;\n case 'stream': {\n switch (result.source) {\n case 'viewSyncer':\n assert(\n this.#viewSyncerOutboundStream === undefined,\n 'Outbound stream already set for this connection!',\n );\n this.#viewSyncerOutboundStream = result.stream;\n break;\n case 'pusher':\n assert(\n this.#pusherOutboundStream === undefined,\n 'Outbound stream already set for this connection!',\n );\n this.#pusherOutboundStream = result.stream;\n break;\n }\n this.#proxyOutbound(result.stream);\n break;\n }\n case 'transient': {\n for (const error of result.errors) {\n this.sendError(error);\n }\n }\n }\n }\n\n #handleClose = (e: CloseEvent) => {\n const {code, reason, wasClean} = e;\n this.close('WebSocket close event', {code, reason, wasClean});\n };\n\n #handleError = (e: ErrorEvent) => {\n this.#lc.error?.('WebSocket error event', e.message, e.error);\n };\n\n #proxyInbound() {\n pipeline(\n createWebSocketStream(this.#ws),\n new Writable({\n write: (data, _encoding, callback) => {\n this.#handleMessage({data}).then(() => callback(), callback);\n },\n }),\n // The done callback is not used, as #handleClose and #handleError,\n // configured on the underlying WebSocket, provide more complete\n // information.\n () => {},\n );\n }\n\n #proxyOutbound(outboundStream: Source<Downstream>) {\n // Note: createWebSocketStream() is avoided here in order to control\n // exception handling with #closeWithThrown(). If the Writable\n // from createWebSocketStream() were instead used, exceptions\n // from the outboundStream result in the Writable closing the\n // the websocket before the error message can be sent.\n pipeline(\n Readable.from(outboundStream),\n new Writable({\n objectMode: true,\n write: (downstream: Downstream, _encoding, callback) =>\n this.send(downstream, callback),\n }),\n e =>\n e\n ? this.#closeWithThrown(e)\n : this.close(`downstream closed by ViewSyncer`),\n );\n }\n\n #closeWithThrown(e: unknown) {\n const errorBody =\n findProtocolError(e)?.errorBody ?? wrapWithProtocolError(e).errorBody;\n\n this.#closeWithError(errorBody, e);\n }\n\n #closeWithError(errorBody: ErrorBody, thrown?: unknown) {\n this.sendError(errorBody, thrown);\n this.close(\n `${errorBody.kind} (${errorBody.origin}): ${errorBody.message}`,\n errorBody,\n );\n }\n\n #lastDownstreamMsgTime = Date.now();\n\n #maybeSendPong = () => {\n if (Date.now() - this.#lastDownstreamMsgTime > DOWNSTREAM_MSG_INTERVAL_MS) {\n this.#lc.debug?.('manually sending pong');\n this.send(['pong', {}], 'ignore-backpressure');\n }\n };\n\n send(\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n ) {\n this.#lastDownstreamMsgTime = Date.now();\n return send(this.#lc, this.#ws, data, callback);\n }\n\n sendError(errorBody: ErrorBody, thrown?: unknown) {\n sendError(this.#lc, this.#ws, errorBody, thrown);\n }\n}\n\nexport type WebSocketLike = Pick<WebSocket, 'readyState'> & {\n send(data: string, cb?: (err?: Error) => void): void;\n};\n\n// Exported for testing purposes.\nexport function send(\n lc: LogContext,\n ws: WebSocketLike,\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n) {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(\n JSON.stringify(data),\n callback === 'ignore-backpressure' ? undefined : callback,\n );\n } else {\n lc.debug?.(`Dropping outbound message on ws (state: ${ws.readyState})`, {\n dropped: data,\n });\n if (callback !== 'ignore-backpressure') {\n callback(\n new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: 'WebSocket closed',\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n ),\n );\n }\n }\n}\n\nexport function sendError(\n lc: LogContext,\n ws: WebSocket,\n errorBody: ErrorBody,\n thrown?: unknown,\n) {\n lc = lc.withContext('errorKind', errorBody.kind);\n\n let logLevel: LogLevel;\n\n // If the thrown error is a ProtocolErrorWithLevel, its explicit logLevel takes precedence\n if (thrown instanceof ProtocolErrorWithLevel) {\n logLevel = thrown.logLevel;\n }\n // Errors with errno or transient socket codes are low-level, transient I/O issues\n // (e.g., EPIPE, ECONNRESET) and should be warnings, not errors\n else if (\n hasErrno(thrown) ||\n hasTransientSocketCode(thrown) ||\n isTransientSocketMessage(errorBody.message)\n ) {\n logLevel = 'warn';\n }\n // Fallback: check errorBody.kind for errors that weren't thrown as ProtocolErrorWithLevel\n else if (\n errorBody.kind === ErrorKind.ClientNotFound ||\n errorBody.kind === ErrorKind.TransformFailed\n ) {\n logLevel = 'warn';\n } else {\n logLevel = thrown ? getLogLevel(thrown) : 'info';\n }\n\n lc[logLevel]?.('Sending error on WebSocket', errorBody, thrown ?? '');\n send(lc, ws, ['error', errorBody], 'ignore-backpressure');\n}\n\nexport function findProtocolError(error: unknown): ProtocolError | undefined {\n if (isProtocolError(error)) {\n return error;\n }\n if (error instanceof Error && error.cause) {\n return findProtocolError(error.cause);\n }\n return undefined;\n}\n\nfunction hasErrno(error: unknown): boolean {\n return Boolean(\n error &&\n typeof error === 'object' &&\n 'errno' in error &&\n typeof (error as {errno: unknown}).errno !== 'undefined',\n );\n}\n\n// System error codes that indicate transient socket conditions.\n// These are checked via the `code` property on errors.\nconst TRANSIENT_SOCKET_ERROR_CODES = new Set([\n 'EPIPE',\n 'ECONNRESET',\n 'ECANCELED',\n]);\n\n// Error messages that indicate transient socket conditions but don't have\n// standard error codes (e.g., WebSocket library errors).\nconst TRANSIENT_SOCKET_MESSAGE_PATTERNS = [\n 'socket was closed while data was being compressed',\n];\n\nfunction hasTransientSocketCode(error: unknown): boolean {\n if (!error || typeof error !== 'object') {\n return false;\n }\n const maybeCode =\n 'code' in error ? String((error as {code?: unknown}).code) : undefined;\n return Boolean(\n maybeCode && TRANSIENT_SOCKET_ERROR_CODES.has(maybeCode.toUpperCase()),\n );\n}\n\nfunction isTransientSocketMessage(message: string | undefined): boolean {\n if (!message) {\n return false;\n }\n const lower = message.toLowerCase();\n return TRANSIENT_SOCKET_MESSAGE_PATTERNS.some(pattern =>\n lower.includes(pattern),\n );\n}\n"],"mappings":"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"}
|
|
@@ -5,8 +5,8 @@ export type { ServerColumnSchema, ServerSchema, ServerTableSchema, } from '../..
|
|
|
5
5
|
export type { AnyTransaction, ClientTransaction, DBConnection, DBTransaction, Location, MutateCRUD, Row, ServerTransaction, Transaction, TransactionBase, TransactionReason, } from '../../zql/src/mutate/custom.ts';
|
|
6
6
|
export { CRUDMutatorFactory, makeSchemaCRUD, type CustomMutatorDefs, } from './custom.ts';
|
|
7
7
|
export { executePostgresQuery } from './pg-query-executor.ts';
|
|
8
|
-
export { getMutation, handleMutateRequest, handleMutationRequest, OutOfOrderMutation, type Database, type ExtractTransactionType, type
|
|
8
|
+
export { getMutation, handleMutateRequest, handleMutationRequest, OutOfOrderMutation, type Database, type ExtractTransactionType, type MutateRequestHandler, type Params, type Parsed, type TransactFn, type TransactFnCallback, type TransactionProviderHooks, type TransactionProviderInput, } from './process-mutations.ts';
|
|
9
9
|
export { PushProcessor } from './push-processor.ts';
|
|
10
|
-
export { handleGetQueriesRequest, handleQueryRequest, handleTransformRequest, type
|
|
10
|
+
export { handleGetQueriesRequest, handleQueryRequest, handleTransformRequest, type QueryRequestHandler, type TransformQueryFunction, } from './queries/process-queries.ts';
|
|
11
11
|
export { ZQLDatabase } from './zql-database.ts';
|
|
12
12
|
//# sourceMappingURL=mod.d.ts.map
|
|
@@ -39,46 +39,55 @@ export type Parsed<D extends Database<ExtractTransactionType<D>>> = {
|
|
|
39
39
|
* @deprecated Use {@linkcode handleMutateRequest} instead.
|
|
40
40
|
*/
|
|
41
41
|
export declare const handleMutationRequest: typeof handleMutateRequest;
|
|
42
|
-
type QueryParams = URLSearchParams | Record<string, string>;
|
|
43
|
-
export type MutateRequestOptions = {
|
|
44
|
-
userID?: string | null | undefined;
|
|
45
|
-
logLevel?: LogLevel | undefined;
|
|
46
|
-
};
|
|
47
42
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* @param dbProvider - Database used to run transactions and store mutation
|
|
51
|
-
* results.
|
|
52
|
-
* @param cb - Runs once per custom mutation. Use `transact` for DB work.
|
|
53
|
-
* Errors before `transact` are saved; errors after commit are logged.
|
|
54
|
-
* @param request - Request to read query params and JSON body from.
|
|
55
|
-
* @param logLevelOrOptions - Either a log level or additional request
|
|
56
|
-
* options.
|
|
57
|
-
* @returns A `MutateResponse`. Success returns `userID: options.userID ?? null`
|
|
58
|
-
* when `options.userID` is provided; invalid requests return the `PushFailed`
|
|
59
|
-
* branch.
|
|
43
|
+
* Parsed query params accepted by {@linkcode handleMutateRequest} when the
|
|
44
|
+
* incoming request URL has already been handled by your framework.
|
|
60
45
|
*/
|
|
61
|
-
export
|
|
46
|
+
export type MutateSearchParams = URLSearchParams | Record<string, string>;
|
|
62
47
|
/**
|
|
63
|
-
*
|
|
48
|
+
* Runs once per custom mutation in a `/mutate` request.
|
|
64
49
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
50
|
+
* Call `transact` to execute database work for the mutation. Throwing before
|
|
51
|
+
* `transact` persists an application error; throwing after `transact` resolves
|
|
52
|
+
* only affects server-side logging because the mutation is already committed.
|
|
53
|
+
*/
|
|
54
|
+
export type MutateRequestHandler<D extends Database<ExtractTransactionType<D>>> = (transact: TransactFn<D>, mutation: CustomMutation) => Promise<MutationResponse>;
|
|
55
|
+
export type HandleMutateRequestArgs<D extends Database<ExtractTransactionType<D>>> = {
|
|
56
|
+
/** Database used to run transactions and store mutation results. */
|
|
57
|
+
dbProvider: D;
|
|
58
|
+
handler: MutateRequestHandler<D>;
|
|
59
|
+
/**
|
|
60
|
+
* Authenticated user ID. Null or undefined means the user is logged out.
|
|
61
|
+
*/
|
|
62
|
+
userID: string | null | undefined;
|
|
63
|
+
/** Optional log level for request parsing and execution. */
|
|
64
|
+
logLevel?: LogLevel | undefined;
|
|
65
|
+
} & ({
|
|
66
|
+
/** Fetch request containing both query params and the JSON body. */
|
|
67
|
+
request: Request;
|
|
68
|
+
} | {
|
|
69
|
+
/** Parsed query params from the `/mutate` request URL. */
|
|
70
|
+
query: MutateSearchParams;
|
|
71
|
+
/** Parsed JSON body from the `/mutate` request. */
|
|
72
|
+
body: ReadonlyJSONValue;
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Process a `/mutate` request from a Fetch `Request`.
|
|
76
|
+
*/
|
|
77
|
+
export declare function handleMutateRequest<D extends Database<ExtractTransactionType<D>>>(input: HandleMutateRequestArgs<D>): Promise<MutateResponse>;
|
|
78
|
+
/**
|
|
79
|
+
* @deprecated Pass a single object instead:
|
|
80
|
+
* `handleMutateRequest({dbProvider, handler, query, body, userID, logLevel})`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function handleMutateRequest<D extends Database<ExtractTransactionType<D>>>(dbProvider: D, handler: MutateRequestHandler<D>, query: MutateSearchParams, body: ReadonlyJSONValue, logLevel?: LogLevel): Promise<MutateResponse>;
|
|
83
|
+
/**
|
|
84
|
+
* @deprecated Pass a single object instead:
|
|
85
|
+
* `handleMutateRequest({dbProvider, handler, request, userID, logLevel})`.
|
|
76
86
|
*/
|
|
77
|
-
export declare function handleMutateRequest<D extends Database<ExtractTransactionType<D>>>(dbProvider: D,
|
|
87
|
+
export declare function handleMutateRequest<D extends Database<ExtractTransactionType<D>>>(dbProvider: D, handler: MutateRequestHandler<D>, request: Request, logLevel?: LogLevel): Promise<MutateResponse>;
|
|
78
88
|
export declare class OutOfOrderMutation extends Error {
|
|
79
89
|
constructor(clientID: string, receivedMutationID: number, lastMutationID: number | bigint);
|
|
80
90
|
}
|
|
81
91
|
/** @deprecated Use getMutator instead */
|
|
82
92
|
export declare function getMutation(mutators: AnyMutatorRegistry | CustomMutatorDefs<any>, name: string): CustomMutatorImpl<any>;
|
|
83
|
-
export {};
|
|
84
93
|
//# sourceMappingURL=process-mutations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-mutations.d.ts","sourceRoot":"","sources":["../../../../zero-server/src/process-mutations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,QAAQ,EAAC,MAAM,kBAAkB,CAAC;AAG3D,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,0BAA0B,CAAC;AAEhE,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,CAAC,MAAM,4BAA4B,CAAC;AAWhD,OAAO,EACL,kBAAkB,EAClB,KAAK,cAAc,EACpB,MAAM,0CAA0C,CAAC;AAElD,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAEpB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAEL,KAAK,gBAAgB,EAEtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,0CAA0C,CAAC;AAEjF,OAAO,KAAK,EAAC,iBAAiB,EAAE,iBAAiB,EAAC,MAAM,aAAa,CAAC;AAItE,MAAM,WAAW,wBAAwB;IACvC,sBAAsB,EAAE,MAAM,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAAA;KAAC,CAAC,CAAC;IACzE,mBAAmB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,qBAAqB,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,wBAAwB;IACvC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,WAAW,EAAE,CAAC,CAAC,EACb,QAAQ,EAAE,CACR,EAAE,EAAE,CAAC,EACL,gBAAgB,EAAE,wBAAwB,KACvC,YAAY,CAAC,CAAC,CAAC,EACpB,gBAAgB,CAAC,EAAE,wBAAwB,KACxC,OAAO,CAAC,CAAC,CAAC,CAAC;CACjB;AAED,MAAM,MAAM,sBAAsB,CAAC,CAAC,IAAI,CAAC,SAAS,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAChF,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAExD,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CACtE,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC,KACtB,OAAO,CAAC,gBAAgB,CAAC,CAAC;AAE/B,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAC1E,CACE,EAAE,EAAE,sBAAsB,CAAC,CAAC,CAAC,EAC7B,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,iBAAiB,GAAG,SAAS,KACvC,OAAO,CAAC,IAAI,CAAC,CAAC;AAErB,MAAM,MAAM,MAAM,CAAC,CAAC,SAAS,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI;IAClE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IACxB,SAAS,EAAE,cAAc,EAAE,CAAC;CAC7B,CAAC;AAqBF;;GAEG;AACH,eAAO,MAAM,qBAAqB,4BAAsB,CAAC;AAEzD,
|
|
1
|
+
{"version":3,"file":"process-mutations.d.ts","sourceRoot":"","sources":["../../../../zero-server/src/process-mutations.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,30 +26,12 @@ var applicationErrorWrapper = async (fn) => {
|
|
|
26
26
|
* @deprecated Use {@linkcode handleMutateRequest} instead.
|
|
27
27
|
*/
|
|
28
28
|
var handleMutationRequest = handleMutateRequest;
|
|
29
|
-
async function handleMutateRequest(
|
|
30
|
-
|
|
31
|
-
let queryParams;
|
|
32
|
-
let optionsArg;
|
|
33
|
-
if (requestOrQueryParams instanceof Request) {
|
|
34
|
-
requestOrJsonBody = requestOrQueryParams;
|
|
35
|
-
queryParams = new URL(requestOrQueryParams.url).searchParams;
|
|
36
|
-
optionsArg = jsonBodyOrLogLevelOrOptions;
|
|
37
|
-
} else {
|
|
38
|
-
requestOrJsonBody = jsonBodyOrLogLevelOrOptions;
|
|
39
|
-
queryParams = requestOrQueryParams;
|
|
40
|
-
optionsArg = logLevelOrOptions;
|
|
41
|
-
}
|
|
42
|
-
const options = normalizeMutateRequestOptions(optionsArg);
|
|
43
|
-
const normalized = {
|
|
44
|
-
requestOrJsonBody,
|
|
45
|
-
userID: "userID" in options ? options.userID ?? null : void 0,
|
|
46
|
-
queryParams,
|
|
47
|
-
logLevel: options.logLevel ?? "info"
|
|
48
|
-
};
|
|
29
|
+
async function handleMutateRequest(inputOrDbProvider, maybeHandler, requestOrQuery, bodyOrLogLevel, logLevel) {
|
|
30
|
+
const normalized = typeof inputOrDbProvider === "object" && "handler" in inputOrDbProvider ? normalizeMutateRequestInput(inputOrDbProvider) : normalizeLegacyMutateRequestArgs(inputOrDbProvider, maybeHandler, requestOrQuery, bodyOrLogLevel, logLevel);
|
|
49
31
|
const lc = createLogContext(normalized.logLevel).withContext("PushProcessor");
|
|
50
32
|
let jsonBody;
|
|
51
|
-
if (normalized.
|
|
52
|
-
jsonBody = await normalized.
|
|
33
|
+
if (normalized.type === "request") try {
|
|
34
|
+
jsonBody = await normalized.request.json();
|
|
53
35
|
} catch (error) {
|
|
54
36
|
lc.error?.("Failed to parse push body", error);
|
|
55
37
|
const message = `Failed to parse push body: ${getErrorMessage(error)}`;
|
|
@@ -63,7 +45,7 @@ async function handleMutateRequest(dbProvider, cb, requestOrQueryParams, jsonBod
|
|
|
63
45
|
...details ? { details } : {}
|
|
64
46
|
};
|
|
65
47
|
}
|
|
66
|
-
else jsonBody = normalized.
|
|
48
|
+
else jsonBody = normalized.jsonBody;
|
|
67
49
|
let mutationIDs = [];
|
|
68
50
|
let pushBody;
|
|
69
51
|
try {
|
|
@@ -87,7 +69,7 @@ async function handleMutateRequest(dbProvider, cb, requestOrQueryParams, jsonBod
|
|
|
87
69
|
}
|
|
88
70
|
let parsedQueryParams;
|
|
89
71
|
try {
|
|
90
|
-
parsedQueryParams = parse(normalized.
|
|
72
|
+
parsedQueryParams = parse(normalized.type === "request" ? Object.fromEntries(new URL(normalized.request.url).searchParams) : normalized.queryParams, mutateParamsSchema, "passthrough");
|
|
91
73
|
} catch (error) {
|
|
92
74
|
lc.error?.("Failed to parse push query parameters", error);
|
|
93
75
|
const message = `Failed to parse push query parameters: ${getErrorMessage(error)}`;
|
|
@@ -111,12 +93,12 @@ async function handleMutateRequest(dbProvider, cb, requestOrQueryParams, jsonBod
|
|
|
111
93
|
const responses = [];
|
|
112
94
|
let processedCount = 0;
|
|
113
95
|
try {
|
|
114
|
-
const transactor = new Transactor(dbProvider, pushBody, parsedQueryParams, lc);
|
|
96
|
+
const transactor = new Transactor(normalized.dbProvider, pushBody, parsedQueryParams, lc);
|
|
115
97
|
for (const m of pushBody.mutations) {
|
|
116
98
|
if (m.type === "custom" && m.name === "_zero_cleanupResults") {
|
|
117
99
|
lc.debug?.(`Processing internal mutation '${m.name}' (clientID=${m.clientID})`);
|
|
118
100
|
try {
|
|
119
|
-
await processCleanupResultsMutation(dbProvider, m, parsedQueryParams, lc);
|
|
101
|
+
await processCleanupResultsMutation(normalized.dbProvider, m, parsedQueryParams, lc);
|
|
120
102
|
processedCount++;
|
|
121
103
|
} catch (error) {
|
|
122
104
|
lc.warn?.(`Failed to process cleanup mutation for client ${m.clientID}`, error);
|
|
@@ -134,7 +116,7 @@ async function handleMutateRequest(dbProvider, cb, requestOrQueryParams, jsonBod
|
|
|
134
116
|
return result;
|
|
135
117
|
};
|
|
136
118
|
try {
|
|
137
|
-
const res = await applicationErrorWrapper(() =>
|
|
119
|
+
const res = await applicationErrorWrapper(() => normalized.handler(transactProxy, m));
|
|
138
120
|
responses.push(res);
|
|
139
121
|
lc.debug?.(`Mutation '${m.name}' (id=${m.id}) completed successfully`);
|
|
140
122
|
processedCount++;
|
|
@@ -167,8 +149,46 @@ async function handleMutateRequest(dbProvider, cb, requestOrQueryParams, jsonBod
|
|
|
167
149
|
};
|
|
168
150
|
}
|
|
169
151
|
}
|
|
170
|
-
function
|
|
171
|
-
|
|
152
|
+
function normalizeMutateRequestInput(input) {
|
|
153
|
+
if ("request" in input) return {
|
|
154
|
+
type: "request",
|
|
155
|
+
dbProvider: input.dbProvider,
|
|
156
|
+
handler: input.handler,
|
|
157
|
+
request: input.request,
|
|
158
|
+
userID: input.userID ?? null,
|
|
159
|
+
logLevel: input.logLevel ?? "info"
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
type: "body",
|
|
163
|
+
dbProvider: input.dbProvider,
|
|
164
|
+
handler: input.handler,
|
|
165
|
+
jsonBody: input.body,
|
|
166
|
+
userID: input.userID ?? null,
|
|
167
|
+
queryParams: input.query instanceof URLSearchParams ? Object.fromEntries(input.query) : input.query,
|
|
168
|
+
logLevel: input.logLevel ?? "info"
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function normalizeLegacyMutateRequestArgs(dbProvider, handler, requestOrQuery, bodyOrLogLevel, logLevel) {
|
|
172
|
+
assert(typeof handler === "function", "Handler function is required");
|
|
173
|
+
assert(typeof requestOrQuery !== "undefined", "Request or query parameters are required");
|
|
174
|
+
if (requestOrQuery instanceof Request) return {
|
|
175
|
+
type: "request",
|
|
176
|
+
dbProvider,
|
|
177
|
+
handler,
|
|
178
|
+
request: requestOrQuery,
|
|
179
|
+
userID: void 0,
|
|
180
|
+
logLevel: bodyOrLogLevel ?? "info"
|
|
181
|
+
};
|
|
182
|
+
assert(typeof bodyOrLogLevel !== "undefined", "JSON body cannot be undefined");
|
|
183
|
+
return {
|
|
184
|
+
type: "body",
|
|
185
|
+
dbProvider,
|
|
186
|
+
handler,
|
|
187
|
+
jsonBody: bodyOrLogLevel,
|
|
188
|
+
userID: void 0,
|
|
189
|
+
queryParams: requestOrQuery instanceof URLSearchParams ? Object.fromEntries(requestOrQuery) : requestOrQuery,
|
|
190
|
+
logLevel: logLevel ?? "info"
|
|
191
|
+
};
|
|
172
192
|
}
|
|
173
193
|
var Transactor = class {
|
|
174
194
|
#dbProvider;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-mutations.js","names":["#dbProvider","#req","#params","#lc","#transactImpl","#checkAndIncrementLastMutationID","#getTransactionInput"],"sources":["../../../../zero-server/src/process-mutations.ts"],"sourcesContent":["import type {LogContext, LogLevel} from '@rocicorp/logger';\nimport {assert} from '../../shared/src/asserts.ts';\nimport {getErrorDetails, getErrorMessage} from '../../shared/src/error.ts';\nimport type {ReadonlyJSONValue} from '../../shared/src/json.ts';\nimport {promiseVoid} from '../../shared/src/resolved-promises.ts';\nimport type {MaybePromise} from '../../shared/src/types.ts';\nimport * as v from '../../shared/src/valita.ts';\nimport {MutationAlreadyProcessedError} from '../../zero-cache/src/services/mutagen/error.ts';\nimport type {ApplicationError} from '../../zero-protocol/src/application-error.ts';\nimport {\n isApplicationError,\n wrapWithApplicationError,\n} from '../../zero-protocol/src/application-error.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 type {PushFailedBody} from '../../zero-protocol/src/error.ts';\nimport {\n mutateParamsSchema,\n type MutateResponse,\n} from '../../zero-protocol/src/mutate-server.ts';\nimport type {MutationID} from '../../zero-protocol/src/mutation-id.ts';\nimport {\n CLEANUP_RESULTS_MUTATION_NAME,\n cleanupResultsArgSchema,\n type CleanupResultsArg,\n type CustomMutation,\n type Mutation,\n} from '../../zero-protocol/src/mutation.ts';\nimport {\n pushBodySchema,\n type MutationResponse,\n type PushBody,\n} from '../../zero-protocol/src/push.ts';\nimport type {AnyMutatorRegistry} from '../../zql/src/mutate/mutator-registry.ts';\nimport {isMutator} from '../../zql/src/mutate/mutator.ts';\nimport type {CustomMutatorDefs, CustomMutatorImpl} from './custom.ts';\nimport {createLogContext} from './logging.ts';\nimport {separatorRe} from './push-processor.ts';\n\nexport interface TransactionProviderHooks {\n updateClientMutationID: () => Promise<{lastMutationID: number | bigint}>;\n writeMutationResult: (result: MutationResponse) => Promise<void>;\n deleteMutationResults: (args: CleanupResultsArg) => Promise<void>;\n}\n\nexport interface TransactionProviderInput {\n upstreamSchema: string;\n clientGroupID: string;\n clientID: string;\n mutationID: number;\n}\n\n/**\n * Defines the abstract interface for a database that PushProcessor can execute\n * transactions against.\n */\nexport interface Database<T> {\n transaction: <R>(\n callback: (\n tx: T,\n transactionHooks: TransactionProviderHooks,\n ) => MaybePromise<R>,\n transactionInput?: TransactionProviderInput,\n ) => Promise<R>;\n}\n\nexport type ExtractTransactionType<D> = D extends Database<infer T> ? T : never;\nexport type Params = v.Infer<typeof mutateParamsSchema>;\n\nexport type TransactFn<D extends Database<ExtractTransactionType<D>>> = (\n cb: TransactFnCallback<D>,\n) => Promise<MutationResponse>;\n\nexport type TransactFnCallback<D extends Database<ExtractTransactionType<D>>> =\n (\n tx: ExtractTransactionType<D>,\n mutatorName: string,\n mutatorArgs: ReadonlyJSONValue | undefined,\n ) => Promise<void>;\n\nexport type Parsed<D extends Database<ExtractTransactionType<D>>> = {\n transact: TransactFn<D>;\n mutations: CustomMutation[];\n};\n\ntype MutationPhase = 'preTransaction' | 'transactionPending' | 'postCommit';\n\nconst applicationErrorWrapper = async <T>(fn: () => Promise<T>): Promise<T> => {\n try {\n return await fn();\n } catch (error) {\n if (\n error instanceof DatabaseTransactionError ||\n error instanceof OutOfOrderMutation ||\n error instanceof MutationAlreadyProcessedError ||\n isApplicationError(error)\n ) {\n throw error;\n }\n\n throw wrapWithApplicationError(error);\n }\n};\n\n/**\n * @deprecated Use {@linkcode handleMutateRequest} instead.\n */\nexport const handleMutationRequest = handleMutateRequest;\n\ntype QueryParams = URLSearchParams | Record<string, string>;\n\nexport type MutateRequestOptions = {\n userID?: string | null | undefined;\n logLevel?: LogLevel | undefined;\n};\n\ntype NormalizedMutateRequestArgs = {\n readonly requestOrJsonBody: Request | ReadonlyJSONValue;\n readonly userID: string | null | undefined;\n readonly queryParams: QueryParams;\n readonly logLevel: LogLevel;\n};\n\n/**\n * Process a `/mutate` request from a `Request`.\n *\n * @param dbProvider - Database used to run transactions and store mutation\n * results.\n * @param cb - Runs once per custom mutation. Use `transact` for DB work.\n * Errors before `transact` are saved; errors after commit are logged.\n * @param request - Request to read query params and JSON body from.\n * @param logLevelOrOptions - Either a log level or additional request\n * options.\n * @returns A `MutateResponse`. Success returns `userID: options.userID ?? null`\n * when `options.userID` is provided; invalid requests return the `PushFailed`\n * branch.\n */\nexport function handleMutateRequest<\n D extends Database<ExtractTransactionType<D>>,\n>(\n dbProvider: D,\n cb: (\n transact: TransactFn<D>,\n mutation: CustomMutation,\n ) => Promise<MutationResponse>,\n request: Request,\n logLevelOrOptions?: LogLevel | MutateRequestOptions,\n): Promise<MutateResponse>;\n\n/**\n * Process a `/mutate` request from parsed query params and a parsed JSON body.\n *\n * @param dbProvider - Database used to run transactions and store mutation\n * results.\n * @param cb - Runs once per custom mutation. Use `transact` for DB work.\n * Errors before `transact` are saved; errors after commit are logged.\n * @param queryParams - Parsed query params.\n * @param jsonBody - Parsed JSON body.\n * @param logLevelOrOptions - Either a log level or additional request\n * options.\n * @returns A `MutateResponse`. Success returns `userID: options.userID ?? null`\n * when `options.userID` is provided; invalid requests return the `PushFailed`\n * branch.\n */\nexport function handleMutateRequest<\n D extends Database<ExtractTransactionType<D>>,\n>(\n dbProvider: D,\n cb: (\n transact: TransactFn<D>,\n mutation: CustomMutation,\n ) => Promise<MutationResponse>,\n queryParams: QueryParams,\n jsonBody: ReadonlyJSONValue,\n logLevelOrOptions?: LogLevel | MutateRequestOptions,\n): Promise<MutateResponse>;\n\nexport async function handleMutateRequest<\n D extends Database<ExtractTransactionType<D>>,\n>(\n dbProvider: D,\n cb: (\n transact: TransactFn<D>,\n mutation: CustomMutation,\n ) => Promise<MutationResponse>,\n requestOrQueryParams: Request | QueryParams,\n jsonBodyOrLogLevelOrOptions?:\n | ReadonlyJSONValue\n | LogLevel\n | MutateRequestOptions,\n logLevelOrOptions?: LogLevel | MutateRequestOptions,\n): Promise<MutateResponse> {\n let requestOrJsonBody: Request | ReadonlyJSONValue;\n let queryParams: QueryParams;\n let optionsArg: LogLevel | MutateRequestOptions | undefined;\n\n if (requestOrQueryParams instanceof Request) {\n requestOrJsonBody = requestOrQueryParams;\n queryParams = new URL(requestOrQueryParams.url).searchParams;\n optionsArg = jsonBodyOrLogLevelOrOptions as\n | LogLevel\n | MutateRequestOptions\n | undefined;\n } else {\n requestOrJsonBody = jsonBodyOrLogLevelOrOptions as ReadonlyJSONValue;\n queryParams = requestOrQueryParams;\n optionsArg = logLevelOrOptions;\n }\n\n const options = normalizeMutateRequestOptions(optionsArg);\n const normalized: NormalizedMutateRequestArgs = {\n requestOrJsonBody,\n userID: 'userID' in options ? (options.userID ?? null) : undefined,\n queryParams,\n logLevel: options.logLevel ?? 'info',\n };\n const lc = createLogContext(normalized.logLevel).withContext('PushProcessor');\n let jsonBody: unknown;\n\n if (normalized.requestOrJsonBody instanceof Request) {\n try {\n jsonBody = await normalized.requestOrJsonBody.json();\n } catch (error) {\n lc.error?.('Failed to parse push body', error);\n const message = `Failed to parse push body: ${getErrorMessage(error)}`;\n const details = getErrorDetails(error);\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Parse,\n message,\n mutationIDs: [],\n ...(details ? {details} : {}),\n } as const satisfies PushFailedBody;\n }\n } else {\n jsonBody = normalized.requestOrJsonBody;\n }\n\n let mutationIDs: MutationID[] = [];\n\n let pushBody: PushBody;\n try {\n pushBody = v.parse(jsonBody, pushBodySchema, 'passthrough');\n mutationIDs = pushBody.mutations.map(m => ({\n id: m.id,\n clientID: m.clientID,\n }));\n } catch (error) {\n lc.error?.('Failed to parse push body', error);\n const message = `Failed to parse push body: ${getErrorMessage(error)}`;\n const details = getErrorDetails(error);\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Parse,\n message,\n mutationIDs,\n ...(details ? {details} : {}),\n } as const satisfies PushFailedBody;\n }\n\n let parsedQueryParams: Params;\n try {\n const rawQueryParams =\n normalized.queryParams instanceof URLSearchParams\n ? Object.fromEntries(normalized.queryParams)\n : normalized.queryParams;\n parsedQueryParams = v.parse(\n rawQueryParams,\n mutateParamsSchema,\n 'passthrough',\n );\n } catch (error) {\n lc.error?.('Failed to parse push query parameters', error);\n const message = `Failed to parse push query parameters: ${getErrorMessage(error)}`;\n const details = getErrorDetails(error);\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Parse,\n message,\n mutationIDs,\n ...(details ? {details} : {}),\n } as const satisfies PushFailedBody;\n }\n\n if (pushBody.pushVersion !== 1) {\n const response = {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.UnsupportedPushVersion,\n mutationIDs,\n message: `Unsupported push version: ${pushBody.pushVersion}`,\n } as const satisfies PushFailedBody;\n return response;\n }\n\n const responses: MutationResponse[] = [];\n let processedCount = 0;\n\n try {\n const transactor = new Transactor(\n dbProvider,\n pushBody,\n parsedQueryParams,\n lc,\n );\n\n // Each mutation goes through three phases:\n // 1. Pre-transaction: user logic that runs before `transact` is called. If\n // this throws we still advance LMID and persist the failure result.\n // 2. Transaction: the callback passed to `transact`, which can be retried\n // if it fails with an ApplicationError.\n // 3. Post-commit: any logic that runs after `transact` resolves. Failures\n // here are logged but the mutation remains committed.\n for (const m of pushBody.mutations) {\n // Handle internal mutations (like cleanup) directly without user dispatch\n if (m.type === 'custom' && m.name === CLEANUP_RESULTS_MUTATION_NAME) {\n lc.debug?.(\n `Processing internal mutation '${m.name}' (clientID=${m.clientID})`,\n );\n try {\n await processCleanupResultsMutation(\n dbProvider,\n m,\n parsedQueryParams,\n lc,\n );\n // No response added - this is fire-and-forget\n processedCount++;\n } catch (error) {\n lc.warn?.(\n `Failed to process cleanup mutation for client ${m.clientID}`,\n error,\n );\n // Don't fail the whole push for cleanup errors\n processedCount++;\n }\n continue;\n }\n\n assert(m.type === 'custom', 'Expected custom mutation');\n lc.debug?.(\n `Processing mutation '${m.name}' (id=${m.id}, clientID=${m.clientID})`,\n );\n\n let mutationPhase: MutationPhase = 'preTransaction';\n\n const transactProxy: TransactFn<D> = async innerCb => {\n mutationPhase = 'transactionPending';\n const result = await transactor.transact(m, innerCb);\n mutationPhase = 'postCommit';\n return result;\n };\n\n try {\n const res = await applicationErrorWrapper(() => cb(transactProxy, m));\n responses.push(res);\n lc.debug?.(`Mutation '${m.name}' (id=${m.id}) completed successfully`);\n\n processedCount++;\n } catch (error) {\n if (!isApplicationError(error)) {\n throw error;\n }\n\n if (mutationPhase === 'preTransaction') {\n // Pre-transaction\n await transactor.persistPreTransactionFailure(m, error);\n } else if (mutationPhase === 'postCommit') {\n // Post-commit\n lc.error?.(\n `Post-commit mutation handler failed for mutation ${m.id} for client ${m.clientID}`,\n error,\n );\n }\n\n lc.warn?.(\n `Application error processing mutation ${m.id} for client ${m.clientID}`,\n error,\n );\n responses.push(makeAppErrorResponse(m, error));\n\n processedCount++;\n }\n }\n\n return {\n kind: 'MutateResponse',\n mutations: responses,\n ...(typeof normalized.userID !== 'undefined'\n ? {userID: normalized.userID}\n : {}),\n } as const satisfies MutateResponse;\n } catch (error) {\n lc.error?.('Failed to process push request', error);\n // only include mutationIDs for mutations that were not processed\n const unprocessedMutationIDs = mutationIDs.slice(processedCount);\n\n const message = getErrorMessage(error);\n const details = getErrorDetails(error);\n\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason:\n error instanceof OutOfOrderMutation\n ? ErrorReason.OutOfOrderMutation\n : error instanceof DatabaseTransactionError\n ? ErrorReason.Database\n : ErrorReason.Internal,\n message,\n mutationIDs: unprocessedMutationIDs,\n ...(details ? {details} : {}),\n };\n }\n}\n\nfunction normalizeMutateRequestOptions(\n logLevelOrOptions: LogLevel | MutateRequestOptions | undefined,\n): MutateRequestOptions {\n return typeof logLevelOrOptions === 'string'\n ? {logLevel: logLevelOrOptions}\n : (logLevelOrOptions ?? {});\n}\n\nclass Transactor<D extends Database<ExtractTransactionType<D>>> {\n readonly #dbProvider: D;\n readonly #req: PushBody;\n readonly #params: Params;\n readonly #lc: LogContext;\n\n constructor(dbProvider: D, req: PushBody, params: Params, lc: LogContext) {\n this.#dbProvider = dbProvider;\n this.#req = req;\n this.#params = params;\n this.#lc = lc;\n }\n\n transact = async (\n mutation: CustomMutation,\n cb: TransactFnCallback<D>,\n ): Promise<MutationResponse> => {\n let appError: ApplicationError | undefined = undefined;\n for (;;) {\n try {\n const ret = await this.#transactImpl(mutation, cb, appError);\n if (appError !== undefined) {\n this.#lc.warn?.(\n `Mutation ${mutation.id} for client ${mutation.clientID} was retried after an error`,\n appError,\n );\n return makeAppErrorResponse(mutation, appError);\n }\n\n return ret;\n } catch (error) {\n if (error instanceof OutOfOrderMutation) {\n this.#lc.error?.(error);\n throw error;\n }\n\n if (error instanceof MutationAlreadyProcessedError) {\n this.#lc.warn?.(error);\n return {\n id: {\n clientID: mutation.clientID,\n id: mutation.id,\n },\n result: {\n error: 'alreadyProcessed',\n details: error.message,\n },\n };\n }\n\n if (appError !== undefined) {\n // Retry also failed → internal error, cannot skip mutation\n this.#lc.error?.(\n `Retry also failed for mutation ${mutation.id} for client ${mutation.clientID}`,\n error,\n );\n throw error;\n }\n\n // First attempt failed → store error and retry without mutator\n const originalError =\n error instanceof DatabaseTransactionError\n ? (error.cause ?? error)\n : error;\n appError = wrapWithApplicationError(originalError);\n this.#lc.warn?.(\n `Error processing mutation ${mutation.id} for client ${mutation.clientID}, retrying without mutator`,\n appError,\n );\n continue;\n }\n }\n };\n\n async persistPreTransactionFailure(\n mutation: CustomMutation,\n appError: ApplicationError<ReadonlyJSONValue | undefined>,\n ): Promise<MutationResponse> {\n // User-land code threw before calling `transact`. We still need to bump the\n // LMID for this mutation and persist the error so that the client knows it failed.\n const ret = await this.#transactImpl(\n mutation,\n // noop callback since there's no transaction to execute\n () => promiseVoid,\n appError,\n );\n return ret;\n }\n\n async #transactImpl(\n mutation: CustomMutation,\n cb: TransactFnCallback<D>,\n appError: ApplicationError | undefined,\n ): Promise<MutationResponse> {\n let transactionPhase: DatabaseTransactionPhase = 'open';\n\n try {\n const ret = await this.#dbProvider.transaction(\n async (dbTx, transactionHooks) => {\n // update the transaction phase to 'execute' after the transaction is opened\n transactionPhase = 'execute';\n\n await this.#checkAndIncrementLastMutationID(\n transactionHooks,\n mutation.clientID,\n mutation.id,\n );\n\n if (appError === undefined) {\n this.#lc.debug?.(\n `Executing mutator '${mutation.name}' (id=${mutation.id})`,\n );\n await cb(dbTx, mutation.name, mutation.args[0]);\n } else {\n const mutationResult = makeAppErrorResponse(mutation, appError);\n await transactionHooks.writeMutationResult(mutationResult);\n }\n\n return {\n id: {\n clientID: mutation.clientID,\n id: mutation.id,\n },\n result: {},\n };\n },\n this.#getTransactionInput(mutation),\n );\n\n return ret;\n } catch (error) {\n if (\n isApplicationError(error) ||\n error instanceof OutOfOrderMutation ||\n error instanceof MutationAlreadyProcessedError\n ) {\n throw error;\n }\n\n throw new DatabaseTransactionError(transactionPhase, {cause: error});\n }\n }\n\n #getTransactionInput(mutation: CustomMutation): TransactionProviderInput {\n return {\n upstreamSchema: this.#params.schema,\n clientGroupID: this.#req.clientGroupID,\n clientID: mutation.clientID,\n mutationID: mutation.id,\n };\n }\n\n async #checkAndIncrementLastMutationID(\n transactionHooks: TransactionProviderHooks,\n clientID: string,\n receivedMutationID: number,\n ) {\n const {lastMutationID} = await transactionHooks.updateClientMutationID();\n\n if (receivedMutationID < lastMutationID) {\n throw new MutationAlreadyProcessedError(\n clientID,\n receivedMutationID,\n lastMutationID,\n );\n } else if (receivedMutationID > lastMutationID) {\n throw new OutOfOrderMutation(\n clientID,\n receivedMutationID,\n lastMutationID,\n );\n }\n }\n}\n\nexport class OutOfOrderMutation extends Error {\n constructor(\n clientID: string,\n receivedMutationID: number,\n lastMutationID: number | bigint,\n ) {\n super(\n `Client ${clientID} sent mutation ID ${receivedMutationID} but expected ${lastMutationID}`,\n );\n }\n}\n\nfunction makeAppErrorResponse(\n m: Mutation,\n error: ApplicationError<ReadonlyJSONValue | undefined>,\n): MutationResponse {\n return {\n id: {\n clientID: m.clientID,\n id: m.id,\n },\n result: {\n error: 'app',\n message: error.message,\n ...(error.details ? {details: error.details} : {}),\n },\n };\n}\n\n/** @deprecated Use getMutator instead */\nexport function getMutation(\n // oxlint-disable-next-line no-explicit-any\n mutators: AnyMutatorRegistry | CustomMutatorDefs<any>,\n name: string,\n // oxlint-disable-next-line no-explicit-any\n): CustomMutatorImpl<any> {\n const path = name.split(separatorRe);\n const mutator = getObjectAtPath(mutators, path);\n assert(typeof mutator === 'function', `could not find mutator ${name}`);\n\n if (isMutator(mutator)) {\n // mutator needs to be called with {tx, args, ctx}\n // CustomMutatorImpl is called with (tx, args, ctx)\n return (tx, args, ctx) => mutator.fn({args, ctx, tx});\n }\n\n // oxlint-disable-next-line no-explicit-any\n return mutator as CustomMutatorImpl<any>;\n}\n\nfunction getObjectAtPath(\n obj: Record<string, unknown>,\n path: string[],\n): unknown {\n let current: unknown = obj;\n for (const part of path) {\n if (typeof current !== 'object' || current === null || !(part in current)) {\n return undefined;\n }\n current = (current as Record<string, unknown>)[part];\n }\n return current;\n}\n\n/**\n * Processes internal cleanup mutation that deletes acknowledged mutation results\n * from the upstream database. This runs without LMID tracking since it's an\n * internal operation.\n */\nasync function processCleanupResultsMutation<\n D extends Database<ExtractTransactionType<D>>,\n>(\n dbProvider: D,\n mutation: CustomMutation,\n queryParams: Params,\n lc: LogContext,\n): Promise<void> {\n const parseResult = v.test(mutation.args[0], cleanupResultsArgSchema);\n if (!parseResult.ok) {\n lc.warn?.('Cleanup mutation has invalid args', parseResult.error);\n return;\n }\n const args: CleanupResultsArg = parseResult.value;\n\n // Determine clientID for transaction input based on cleanup type\n // Note: legacy format without type field is treated as single\n const clientID =\n 'type' in args && args.type === 'bulk' ? args.clientIDs[0] : args.clientID;\n\n // Run in a transaction, using the hook for DB-specific operation.\n // Note: only upstreamSchema is used by deleteMutationResults; the other\n // fields are required by the interface but ignored for this operation.\n await dbProvider.transaction(\n async (_, hooks) => {\n await hooks.deleteMutationResults(args);\n },\n {\n upstreamSchema: queryParams.schema,\n clientGroupID: args.clientGroupID,\n clientID,\n mutationID: 0,\n },\n );\n}\n\ntype DatabaseTransactionPhase = 'open' | 'execute';\nclass DatabaseTransactionError extends Error {\n constructor(phase: DatabaseTransactionPhase, options?: ErrorOptions) {\n super(\n phase === 'open'\n ? `Failed to open database transaction: ${getErrorMessage(options?.cause)}`\n : `Database transaction failed after opening: ${getErrorMessage(options?.cause)}`,\n options,\n );\n this.name = 'DatabaseTransactionError';\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"process-mutations.js","names":["#dbProvider","#req","#params","#lc","#transactImpl","#checkAndIncrementLastMutationID","#getTransactionInput"],"sources":["../../../../zero-server/src/process-mutations.ts"],"sourcesContent":["import type {LogContext, LogLevel} from '@rocicorp/logger';\nimport {assert} from '../../shared/src/asserts.ts';\nimport {getErrorDetails, getErrorMessage} from '../../shared/src/error.ts';\nimport type {ReadonlyJSONValue} from '../../shared/src/json.ts';\nimport {promiseVoid} from '../../shared/src/resolved-promises.ts';\nimport type {MaybePromise} from '../../shared/src/types.ts';\nimport * as v from '../../shared/src/valita.ts';\nimport {MutationAlreadyProcessedError} from '../../zero-cache/src/services/mutagen/error.ts';\nimport type {ApplicationError} from '../../zero-protocol/src/application-error.ts';\nimport {\n isApplicationError,\n wrapWithApplicationError,\n} from '../../zero-protocol/src/application-error.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 type {PushFailedBody} from '../../zero-protocol/src/error.ts';\nimport {\n mutateParamsSchema,\n type MutateResponse,\n} from '../../zero-protocol/src/mutate-server.ts';\nimport type {MutationID} from '../../zero-protocol/src/mutation-id.ts';\nimport {\n CLEANUP_RESULTS_MUTATION_NAME,\n cleanupResultsArgSchema,\n type CleanupResultsArg,\n type CustomMutation,\n type Mutation,\n} from '../../zero-protocol/src/mutation.ts';\nimport {\n pushBodySchema,\n type MutationResponse,\n type PushBody,\n} from '../../zero-protocol/src/push.ts';\nimport type {AnyMutatorRegistry} from '../../zql/src/mutate/mutator-registry.ts';\nimport {isMutator} from '../../zql/src/mutate/mutator.ts';\nimport type {CustomMutatorDefs, CustomMutatorImpl} from './custom.ts';\nimport {createLogContext} from './logging.ts';\nimport {separatorRe} from './push-processor.ts';\n\nexport interface TransactionProviderHooks {\n updateClientMutationID: () => Promise<{lastMutationID: number | bigint}>;\n writeMutationResult: (result: MutationResponse) => Promise<void>;\n deleteMutationResults: (args: CleanupResultsArg) => Promise<void>;\n}\n\nexport interface TransactionProviderInput {\n upstreamSchema: string;\n clientGroupID: string;\n clientID: string;\n mutationID: number;\n}\n\n/**\n * Defines the abstract interface for a database that PushProcessor can execute\n * transactions against.\n */\nexport interface Database<T> {\n transaction: <R>(\n callback: (\n tx: T,\n transactionHooks: TransactionProviderHooks,\n ) => MaybePromise<R>,\n transactionInput?: TransactionProviderInput,\n ) => Promise<R>;\n}\n\nexport type ExtractTransactionType<D> = D extends Database<infer T> ? T : never;\nexport type Params = v.Infer<typeof mutateParamsSchema>;\n\nexport type TransactFn<D extends Database<ExtractTransactionType<D>>> = (\n cb: TransactFnCallback<D>,\n) => Promise<MutationResponse>;\n\nexport type TransactFnCallback<D extends Database<ExtractTransactionType<D>>> =\n (\n tx: ExtractTransactionType<D>,\n mutatorName: string,\n mutatorArgs: ReadonlyJSONValue | undefined,\n ) => Promise<void>;\n\nexport type Parsed<D extends Database<ExtractTransactionType<D>>> = {\n transact: TransactFn<D>;\n mutations: CustomMutation[];\n};\n\ntype MutationPhase = 'preTransaction' | 'transactionPending' | 'postCommit';\n\nconst applicationErrorWrapper = async <T>(fn: () => Promise<T>): Promise<T> => {\n try {\n return await fn();\n } catch (error) {\n if (\n error instanceof DatabaseTransactionError ||\n error instanceof OutOfOrderMutation ||\n error instanceof MutationAlreadyProcessedError ||\n isApplicationError(error)\n ) {\n throw error;\n }\n\n throw wrapWithApplicationError(error);\n }\n};\n\n/**\n * @deprecated Use {@linkcode handleMutateRequest} instead.\n */\nexport const handleMutationRequest = handleMutateRequest;\n\n/**\n * Parsed query params accepted by {@linkcode handleMutateRequest} when the\n * incoming request URL has already been handled by your framework.\n */\nexport type MutateSearchParams = URLSearchParams | Record<string, string>;\n\n/**\n * Runs once per custom mutation in a `/mutate` request.\n *\n * Call `transact` to execute database work for the mutation. Throwing before\n * `transact` persists an application error; throwing after `transact` resolves\n * only affects server-side logging because the mutation is already committed.\n */\nexport type MutateRequestHandler<\n D extends Database<ExtractTransactionType<D>>,\n> = (\n transact: TransactFn<D>,\n mutation: CustomMutation,\n) => Promise<MutationResponse>;\n\nexport type HandleMutateRequestArgs<\n D extends Database<ExtractTransactionType<D>>,\n> = {\n /** Database used to run transactions and store mutation results. */\n dbProvider: D;\n handler: MutateRequestHandler<D>;\n /**\n * Authenticated user ID. Null or undefined means the user is logged out.\n */\n userID: string | null | undefined;\n /** Optional log level for request parsing and execution. */\n logLevel?: LogLevel | undefined;\n} & (\n | {\n /** Fetch request containing both query params and the JSON body. */\n request: Request;\n }\n | {\n /** Parsed query params from the `/mutate` request URL. */\n query: MutateSearchParams;\n /** Parsed JSON body from the `/mutate` request. */\n body: ReadonlyJSONValue;\n }\n);\n\ntype NormalizedMutateRequestArgs<\n D extends Database<ExtractTransactionType<D>>,\n> = {\n readonly dbProvider: D;\n readonly handler: MutateRequestHandler<D>;\n // Note: semantics of undefined differ from HandleMutateRequestArgs.userID.\n // Here, undefined means the app didn't provide a user ID - we do not know if\n // the user is logged in or not. This is legacy behavior needed to support\n // deprecated signatures of handleMutateRequest which did not receive userID\n // from app.\n readonly userID: string | null | undefined;\n readonly logLevel: LogLevel;\n} & (\n | {\n readonly type: 'request';\n readonly request: Request;\n }\n | {\n readonly type: 'body';\n readonly queryParams: Record<string, string>;\n readonly jsonBody: ReadonlyJSONValue;\n }\n);\n\n/**\n * Process a `/mutate` request from a Fetch `Request`.\n */\nexport function handleMutateRequest<\n D extends Database<ExtractTransactionType<D>>,\n>(input: HandleMutateRequestArgs<D>): Promise<MutateResponse>;\n\n/**\n * @deprecated Pass a single object instead:\n * `handleMutateRequest({dbProvider, handler, query, body, userID, logLevel})`.\n */\nexport function handleMutateRequest<\n D extends Database<ExtractTransactionType<D>>,\n>(\n dbProvider: D,\n handler: MutateRequestHandler<D>,\n query: MutateSearchParams,\n body: ReadonlyJSONValue,\n logLevel?: LogLevel,\n): Promise<MutateResponse>;\n\n/**\n * @deprecated Pass a single object instead:\n * `handleMutateRequest({dbProvider, handler, request, userID, logLevel})`.\n */\nexport function handleMutateRequest<\n D extends Database<ExtractTransactionType<D>>,\n>(\n dbProvider: D,\n handler: MutateRequestHandler<D>,\n request: Request,\n logLevel?: LogLevel,\n): Promise<MutateResponse>;\n\nexport async function handleMutateRequest<\n D extends Database<ExtractTransactionType<D>>,\n>(\n inputOrDbProvider: HandleMutateRequestArgs<D> | D,\n maybeHandler?: MutateRequestHandler<D> | undefined,\n requestOrQuery?: Request | MutateSearchParams | undefined,\n bodyOrLogLevel?: ReadonlyJSONValue | LogLevel | undefined,\n logLevel?: LogLevel | undefined,\n): Promise<MutateResponse> {\n const normalized =\n typeof inputOrDbProvider === 'object' && 'handler' in inputOrDbProvider\n ? normalizeMutateRequestInput(inputOrDbProvider)\n : normalizeLegacyMutateRequestArgs(\n inputOrDbProvider,\n maybeHandler,\n requestOrQuery,\n bodyOrLogLevel,\n logLevel,\n );\n\n const lc = createLogContext(normalized.logLevel).withContext('PushProcessor');\n let jsonBody: unknown;\n\n if (normalized.type === 'request') {\n try {\n jsonBody = await normalized.request.json();\n } catch (error) {\n lc.error?.('Failed to parse push body', error);\n const message = `Failed to parse push body: ${getErrorMessage(error)}`;\n const details = getErrorDetails(error);\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Parse,\n message,\n mutationIDs: [],\n ...(details ? {details} : {}),\n } as const satisfies PushFailedBody;\n }\n } else {\n jsonBody = normalized.jsonBody;\n }\n\n let mutationIDs: MutationID[] = [];\n\n let pushBody: PushBody;\n try {\n pushBody = v.parse(jsonBody, pushBodySchema, 'passthrough');\n mutationIDs = pushBody.mutations.map(m => ({\n id: m.id,\n clientID: m.clientID,\n }));\n } catch (error) {\n lc.error?.('Failed to parse push body', error);\n const message = `Failed to parse push body: ${getErrorMessage(error)}`;\n const details = getErrorDetails(error);\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Parse,\n message,\n mutationIDs,\n ...(details ? {details} : {}),\n } as const satisfies PushFailedBody;\n }\n\n let parsedQueryParams: Params;\n try {\n parsedQueryParams = v.parse(\n normalized.type === 'request'\n ? Object.fromEntries(new URL(normalized.request.url).searchParams)\n : normalized.queryParams,\n mutateParamsSchema,\n 'passthrough',\n );\n } catch (error) {\n lc.error?.('Failed to parse push query parameters', error);\n const message = `Failed to parse push query parameters: ${getErrorMessage(error)}`;\n const details = getErrorDetails(error);\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Parse,\n message,\n mutationIDs,\n ...(details ? {details} : {}),\n } as const satisfies PushFailedBody;\n }\n\n if (pushBody.pushVersion !== 1) {\n const response = {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.UnsupportedPushVersion,\n mutationIDs,\n message: `Unsupported push version: ${pushBody.pushVersion}`,\n } as const satisfies PushFailedBody;\n return response;\n }\n\n const responses: MutationResponse[] = [];\n let processedCount = 0;\n\n try {\n const transactor = new Transactor(\n normalized.dbProvider,\n pushBody,\n parsedQueryParams,\n lc,\n );\n\n // Each mutation goes through three phases:\n // 1. Pre-transaction: user logic that runs before `transact` is called. If\n // this throws we still advance LMID and persist the failure result.\n // 2. Transaction: the callback passed to `transact`, which can be retried\n // if it fails with an ApplicationError.\n // 3. Post-commit: any logic that runs after `transact` resolves. Failures\n // here are logged but the mutation remains committed.\n for (const m of pushBody.mutations) {\n // Handle internal mutations (like cleanup) directly without user dispatch\n if (m.type === 'custom' && m.name === CLEANUP_RESULTS_MUTATION_NAME) {\n lc.debug?.(\n `Processing internal mutation '${m.name}' (clientID=${m.clientID})`,\n );\n try {\n await processCleanupResultsMutation(\n normalized.dbProvider,\n m,\n parsedQueryParams,\n lc,\n );\n // No response added - this is fire-and-forget\n processedCount++;\n } catch (error) {\n lc.warn?.(\n `Failed to process cleanup mutation for client ${m.clientID}`,\n error,\n );\n // Don't fail the whole push for cleanup errors\n processedCount++;\n }\n continue;\n }\n\n assert(m.type === 'custom', 'Expected custom mutation');\n lc.debug?.(\n `Processing mutation '${m.name}' (id=${m.id}, clientID=${m.clientID})`,\n );\n\n let mutationPhase: MutationPhase = 'preTransaction';\n\n const transactProxy: TransactFn<D> = async innerCb => {\n mutationPhase = 'transactionPending';\n const result = await transactor.transact(m, innerCb);\n mutationPhase = 'postCommit';\n return result;\n };\n\n try {\n const res = await applicationErrorWrapper(() =>\n normalized.handler(transactProxy, m),\n );\n responses.push(res);\n lc.debug?.(`Mutation '${m.name}' (id=${m.id}) completed successfully`);\n\n processedCount++;\n } catch (error) {\n if (!isApplicationError(error)) {\n throw error;\n }\n\n if (mutationPhase === 'preTransaction') {\n // Pre-transaction\n await transactor.persistPreTransactionFailure(m, error);\n } else if (mutationPhase === 'postCommit') {\n // Post-commit\n lc.error?.(\n `Post-commit mutation handler failed for mutation ${m.id} for client ${m.clientID}`,\n error,\n );\n }\n\n lc.warn?.(\n `Application error processing mutation ${m.id} for client ${m.clientID}`,\n error,\n );\n responses.push(makeAppErrorResponse(m, error));\n\n processedCount++;\n }\n }\n\n return {\n kind: 'MutateResponse',\n mutations: responses,\n ...(typeof normalized.userID !== 'undefined'\n ? {userID: normalized.userID}\n : {}),\n } as const satisfies MutateResponse;\n } catch (error) {\n lc.error?.('Failed to process push request', error);\n // only include mutationIDs for mutations that were not processed\n const unprocessedMutationIDs = mutationIDs.slice(processedCount);\n\n const message = getErrorMessage(error);\n const details = getErrorDetails(error);\n\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason:\n error instanceof OutOfOrderMutation\n ? ErrorReason.OutOfOrderMutation\n : error instanceof DatabaseTransactionError\n ? ErrorReason.Database\n : ErrorReason.Internal,\n message,\n mutationIDs: unprocessedMutationIDs,\n ...(details ? {details} : {}),\n };\n }\n}\n\nfunction normalizeMutateRequestInput<\n D extends Database<ExtractTransactionType<D>>,\n>(input: HandleMutateRequestArgs<D>): NormalizedMutateRequestArgs<D> {\n if ('request' in input) {\n return {\n type: 'request',\n dbProvider: input.dbProvider,\n handler: input.handler,\n request: input.request,\n userID: input.userID ?? null,\n logLevel: input.logLevel ?? 'info',\n };\n }\n\n return {\n type: 'body',\n dbProvider: input.dbProvider,\n handler: input.handler,\n jsonBody: input.body,\n userID: input.userID ?? null,\n queryParams:\n input.query instanceof URLSearchParams\n ? Object.fromEntries(input.query)\n : input.query,\n logLevel: input.logLevel ?? 'info',\n };\n}\n\nfunction normalizeLegacyMutateRequestArgs<\n D extends Database<ExtractTransactionType<D>>,\n>(\n dbProvider: D,\n handler: MutateRequestHandler<D> | undefined,\n requestOrQuery: Request | MutateSearchParams | undefined,\n bodyOrLogLevel: ReadonlyJSONValue | LogLevel | undefined,\n logLevel: LogLevel | undefined,\n): NormalizedMutateRequestArgs<D> {\n assert(typeof handler === 'function', 'Handler function is required');\n assert(\n typeof requestOrQuery !== 'undefined',\n 'Request or query parameters are required',\n );\n\n if (requestOrQuery instanceof Request) {\n return {\n type: 'request',\n dbProvider,\n handler,\n request: requestOrQuery,\n userID: undefined,\n logLevel: (bodyOrLogLevel as LogLevel | undefined) ?? 'info',\n };\n }\n\n assert(\n typeof bodyOrLogLevel !== 'undefined',\n 'JSON body cannot be undefined',\n );\n\n return {\n type: 'body',\n dbProvider,\n handler,\n jsonBody: bodyOrLogLevel,\n userID: undefined,\n queryParams:\n requestOrQuery instanceof URLSearchParams\n ? Object.fromEntries(requestOrQuery)\n : requestOrQuery,\n logLevel: logLevel ?? 'info',\n };\n}\n\nclass Transactor<D extends Database<ExtractTransactionType<D>>> {\n readonly #dbProvider: D;\n readonly #req: PushBody;\n readonly #params: Params;\n readonly #lc: LogContext;\n\n constructor(dbProvider: D, req: PushBody, params: Params, lc: LogContext) {\n this.#dbProvider = dbProvider;\n this.#req = req;\n this.#params = params;\n this.#lc = lc;\n }\n\n transact = async (\n mutation: CustomMutation,\n cb: TransactFnCallback<D>,\n ): Promise<MutationResponse> => {\n let appError: ApplicationError | undefined = undefined;\n for (;;) {\n try {\n const ret = await this.#transactImpl(mutation, cb, appError);\n if (appError !== undefined) {\n this.#lc.warn?.(\n `Mutation ${mutation.id} for client ${mutation.clientID} was retried after an error`,\n appError,\n );\n return makeAppErrorResponse(mutation, appError);\n }\n\n return ret;\n } catch (error) {\n if (error instanceof OutOfOrderMutation) {\n this.#lc.error?.(error);\n throw error;\n }\n\n if (error instanceof MutationAlreadyProcessedError) {\n this.#lc.warn?.(error);\n return {\n id: {\n clientID: mutation.clientID,\n id: mutation.id,\n },\n result: {\n error: 'alreadyProcessed',\n details: error.message,\n },\n };\n }\n\n if (appError !== undefined) {\n // Retry also failed → internal error, cannot skip mutation\n this.#lc.error?.(\n `Retry also failed for mutation ${mutation.id} for client ${mutation.clientID}`,\n error,\n );\n throw error;\n }\n\n // First attempt failed → store error and retry without mutator\n const originalError =\n error instanceof DatabaseTransactionError\n ? (error.cause ?? error)\n : error;\n appError = wrapWithApplicationError(originalError);\n this.#lc.warn?.(\n `Error processing mutation ${mutation.id} for client ${mutation.clientID}, retrying without mutator`,\n appError,\n );\n continue;\n }\n }\n };\n\n async persistPreTransactionFailure(\n mutation: CustomMutation,\n appError: ApplicationError<ReadonlyJSONValue | undefined>,\n ): Promise<MutationResponse> {\n // User-land code threw before calling `transact`. We still need to bump the\n // LMID for this mutation and persist the error so that the client knows it failed.\n const ret = await this.#transactImpl(\n mutation,\n // noop callback since there's no transaction to execute\n () => promiseVoid,\n appError,\n );\n return ret;\n }\n\n async #transactImpl(\n mutation: CustomMutation,\n cb: TransactFnCallback<D>,\n appError: ApplicationError | undefined,\n ): Promise<MutationResponse> {\n let transactionPhase: DatabaseTransactionPhase = 'open';\n\n try {\n const ret = await this.#dbProvider.transaction(\n async (dbTx, transactionHooks) => {\n // update the transaction phase to 'execute' after the transaction is opened\n transactionPhase = 'execute';\n\n await this.#checkAndIncrementLastMutationID(\n transactionHooks,\n mutation.clientID,\n mutation.id,\n );\n\n if (appError === undefined) {\n this.#lc.debug?.(\n `Executing mutator '${mutation.name}' (id=${mutation.id})`,\n );\n await cb(dbTx, mutation.name, mutation.args[0]);\n } else {\n const mutationResult = makeAppErrorResponse(mutation, appError);\n await transactionHooks.writeMutationResult(mutationResult);\n }\n\n return {\n id: {\n clientID: mutation.clientID,\n id: mutation.id,\n },\n result: {},\n };\n },\n this.#getTransactionInput(mutation),\n );\n\n return ret;\n } catch (error) {\n if (\n isApplicationError(error) ||\n error instanceof OutOfOrderMutation ||\n error instanceof MutationAlreadyProcessedError\n ) {\n throw error;\n }\n\n throw new DatabaseTransactionError(transactionPhase, {cause: error});\n }\n }\n\n #getTransactionInput(mutation: CustomMutation): TransactionProviderInput {\n return {\n upstreamSchema: this.#params.schema,\n clientGroupID: this.#req.clientGroupID,\n clientID: mutation.clientID,\n mutationID: mutation.id,\n };\n }\n\n async #checkAndIncrementLastMutationID(\n transactionHooks: TransactionProviderHooks,\n clientID: string,\n receivedMutationID: number,\n ) {\n const {lastMutationID} = await transactionHooks.updateClientMutationID();\n\n if (receivedMutationID < lastMutationID) {\n throw new MutationAlreadyProcessedError(\n clientID,\n receivedMutationID,\n lastMutationID,\n );\n } else if (receivedMutationID > lastMutationID) {\n throw new OutOfOrderMutation(\n clientID,\n receivedMutationID,\n lastMutationID,\n );\n }\n }\n}\n\nexport class OutOfOrderMutation extends Error {\n constructor(\n clientID: string,\n receivedMutationID: number,\n lastMutationID: number | bigint,\n ) {\n super(\n `Client ${clientID} sent mutation ID ${receivedMutationID} but expected ${lastMutationID}`,\n );\n }\n}\n\nfunction makeAppErrorResponse(\n m: Mutation,\n error: ApplicationError<ReadonlyJSONValue | undefined>,\n): MutationResponse {\n return {\n id: {\n clientID: m.clientID,\n id: m.id,\n },\n result: {\n error: 'app',\n message: error.message,\n ...(error.details ? {details: error.details} : {}),\n },\n };\n}\n\n/** @deprecated Use getMutator instead */\nexport function getMutation(\n // oxlint-disable-next-line no-explicit-any\n mutators: AnyMutatorRegistry | CustomMutatorDefs<any>,\n name: string,\n // oxlint-disable-next-line no-explicit-any\n): CustomMutatorImpl<any> {\n const path = name.split(separatorRe);\n const mutator = getObjectAtPath(mutators, path);\n assert(typeof mutator === 'function', `could not find mutator ${name}`);\n\n if (isMutator(mutator)) {\n // mutator needs to be called with {tx, args, ctx}\n // CustomMutatorImpl is called with (tx, args, ctx)\n return (tx, args, ctx) => mutator.fn({args, ctx, tx});\n }\n\n // oxlint-disable-next-line no-explicit-any\n return mutator as CustomMutatorImpl<any>;\n}\n\nfunction getObjectAtPath(\n obj: Record<string, unknown>,\n path: string[],\n): unknown {\n let current: unknown = obj;\n for (const part of path) {\n if (typeof current !== 'object' || current === null || !(part in current)) {\n return undefined;\n }\n current = (current as Record<string, unknown>)[part];\n }\n return current;\n}\n\n/**\n * Processes internal cleanup mutation that deletes acknowledged mutation results\n * from the upstream database. This runs without LMID tracking since it's an\n * internal operation.\n */\nasync function processCleanupResultsMutation<\n D extends Database<ExtractTransactionType<D>>,\n>(\n dbProvider: D,\n mutation: CustomMutation,\n queryParams: Params,\n lc: LogContext,\n): Promise<void> {\n const parseResult = v.test(mutation.args[0], cleanupResultsArgSchema);\n if (!parseResult.ok) {\n lc.warn?.('Cleanup mutation has invalid args', parseResult.error);\n return;\n }\n const args: CleanupResultsArg = parseResult.value;\n\n // Determine clientID for transaction input based on cleanup type\n // Note: legacy format without type field is treated as single\n const clientID =\n 'type' in args && args.type === 'bulk' ? args.clientIDs[0] : args.clientID;\n\n // Run in a transaction, using the hook for DB-specific operation.\n // Note: only upstreamSchema is used by deleteMutationResults; the other\n // fields are required by the interface but ignored for this operation.\n await dbProvider.transaction(\n async (_, hooks) => {\n await hooks.deleteMutationResults(args);\n },\n {\n upstreamSchema: queryParams.schema,\n clientGroupID: args.clientGroupID,\n clientID,\n mutationID: 0,\n },\n );\n}\n\ntype DatabaseTransactionPhase = 'open' | 'execute';\nclass DatabaseTransactionError extends Error {\n constructor(phase: DatabaseTransactionPhase, options?: ErrorOptions) {\n super(\n phase === 'open'\n ? `Failed to open database transaction: ${getErrorMessage(options?.cause)}`\n : `Database transaction failed after opening: ${getErrorMessage(options?.cause)}`,\n options,\n );\n this.name = 'DatabaseTransactionError';\n }\n}\n"],"mappings":"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"}
|
|
@@ -28,28 +28,45 @@ export declare function handleGetQueriesRequest<S extends Schema>(cb: (name: str
|
|
|
28
28
|
export declare function handleTransformRequest<S extends Schema>(cb: (name: string, args: readonly ReadonlyJSONValue[]) => MaybePromise<{
|
|
29
29
|
query: AnyQuery;
|
|
30
30
|
} | AnyQuery>, schema: S, requestOrJsonBody: Request | ReadonlyJSONValue, logLevel?: LogLevel): Promise<QueryResponse>;
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Parsed query params accepted by {@linkcode handleQueryRequest} when the
|
|
33
|
+
* incoming request URL has already been handled by your framework.
|
|
34
|
+
*/
|
|
35
|
+
export type QuerySearchParams = URLSearchParams | Record<string, string>;
|
|
36
|
+
export type HandleQueryRequestArgs<S extends Schema> = {
|
|
37
|
+
/** Callback that transforms each requested query into a `Query`. */
|
|
38
|
+
handler: QueryRequestHandler;
|
|
39
|
+
/** Schema used when building the returned ASTs. */
|
|
40
|
+
schema: S;
|
|
41
|
+
/**
|
|
42
|
+
* Authenticated user ID. Null or undefined means the user is logged out.
|
|
43
|
+
*/
|
|
44
|
+
userID: string | null | undefined;
|
|
45
|
+
/** Optional log level for request parsing and execution. */
|
|
33
46
|
logLevel?: LogLevel | undefined;
|
|
34
|
-
}
|
|
47
|
+
} & ({
|
|
48
|
+
/** Fetch request containing the `/query` JSON body. */
|
|
49
|
+
request: Request;
|
|
50
|
+
} | {
|
|
51
|
+
/** Parsed query params from the `/query` request URL. */
|
|
52
|
+
query: QuerySearchParams;
|
|
53
|
+
/** Parsed JSON body from the `/query` request. */
|
|
54
|
+
body: ReadonlyJSONValue;
|
|
55
|
+
});
|
|
35
56
|
/**
|
|
36
|
-
* Process a `/query` request
|
|
37
|
-
*
|
|
38
|
-
* @param transformQuery - Runs once per requested query with the query name
|
|
39
|
-
* and first JSON argument. Returns a `Query`.
|
|
40
|
-
* @param schema - Schema used when building the returned ASTs.
|
|
41
|
-
* @param request - A Fetch `Request`.
|
|
42
|
-
* @param logLevelOrOptions - Either a log level or additional request
|
|
43
|
-
* options.
|
|
44
|
-
* @returns A `QueryResponse`. Success returns `userID: options.userID ?? null`
|
|
45
|
-
* when `options.userID` is provided. Per-query errors stay in `queries`;
|
|
46
|
-
* malformed requests return `TransformFailed`.
|
|
57
|
+
* Process a `/query` request from a Fetch `Request`.
|
|
47
58
|
*/
|
|
48
|
-
export declare function handleQueryRequest<S extends Schema>(
|
|
59
|
+
export declare function handleQueryRequest<S extends Schema>(input: HandleQueryRequestArgs<S>): Promise<QueryResponse>;
|
|
49
60
|
/**
|
|
50
|
-
*
|
|
61
|
+
* @deprecated Pass a single object instead:
|
|
62
|
+
* `handleQueryRequest({handler, schema, request, userID, logLevel})`.
|
|
51
63
|
*/
|
|
52
|
-
export declare function handleQueryRequest<S extends Schema>(transformQuery:
|
|
64
|
+
export declare function handleQueryRequest<S extends Schema>(transformQuery: QueryRequestHandler, schema: S, request: Request, logLevel?: LogLevel): Promise<QueryResponse>;
|
|
65
|
+
/**
|
|
66
|
+
* @deprecated Pass a single object instead:
|
|
67
|
+
* `handleQueryRequest({handler, schema, body, userID, logLevel})`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function handleQueryRequest<S extends Schema>(transformQuery: QueryRequestHandler, schema: S, jsonBody: ReadonlyJSONValue, logLevel?: LogLevel): Promise<QueryResponse>;
|
|
53
70
|
/**
|
|
54
71
|
* A function that transforms a query by name and arguments into a Query object.
|
|
55
72
|
*
|
|
@@ -57,5 +74,10 @@ export declare function handleQueryRequest<S extends Schema>(transformQuery: Tra
|
|
|
57
74
|
* @param args - The arguments to pass to the query (can be undefined)
|
|
58
75
|
* @returns A Query object
|
|
59
76
|
*/
|
|
60
|
-
export type
|
|
77
|
+
export type QueryRequestHandler = (name: string, args: ReadonlyJSONValue | undefined) => AnyQuery;
|
|
78
|
+
/** @deprecated Use `QueryRequestHandler` instead. */
|
|
79
|
+
export type TransformQueryFunction = QueryRequestHandler;
|
|
80
|
+
export type LegacyQueryRequestHandler = (name: string, args: readonly ReadonlyJSONValue[]) => MaybePromise<{
|
|
81
|
+
query: AnyQuery;
|
|
82
|
+
} | AnyQuery>;
|
|
61
83
|
//# sourceMappingURL=process-queries.d.ts.map
|