@rocicorp/zero 1.4.0 → 1.5.0-canary.1
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/auth/auth.d.ts +1 -1
- package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
- package/out/zero-cache/src/auth/auth.js +1 -1
- package/out/zero-cache/src/auth/auth.js.map +1 -1
- package/out/zero-cache/src/auth/write-authorizer.d.ts +1 -1
- package/out/zero-cache/src/auth/write-authorizer.d.ts.map +1 -1
- package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
- package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
- package/out/zero-cache/src/config/normalize.js +8 -0
- package/out/zero-cache/src/config/normalize.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +8 -4
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +28 -6
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/custom/fetch.d.ts +1 -1
- package/out/zero-cache/src/custom/fetch.d.ts.map +1 -1
- package/out/zero-cache/src/custom/fetch.js +2 -2
- package/out/zero-cache/src/custom/fetch.js.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.d.ts +21 -7
- package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.js +26 -9
- package/out/zero-cache/src/custom-queries/transform-query.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 +2 -1
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.d.ts.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.js +5 -2
- package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
- package/out/zero-cache/src/server/syncer.js +3 -3
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +24 -20
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +258 -45
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +119 -83
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js +2 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +1 -0
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +3 -3
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/http-service.d.ts +1 -0
- package/out/zero-cache/src/services/http-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/http-service.js +5 -4
- package/out/zero-cache/src/services/http-service.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +1 -2
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.d.ts +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.d.ts +4 -3
- package/out/zero-cache/src/services/mutagen/pusher.d.ts.map +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js +57 -38
- package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
- package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js +2 -1
- package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +41 -27
- 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 +147 -104
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts +6 -0
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +8 -0
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +3 -3
- 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 +119 -86
- 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-cache/src/workers/syncer-ws-message-handler.d.ts +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js +7 -7
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +11 -10
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/connection.d.ts +15 -7
- package/out/zero-client/src/client/connection.d.ts.map +1 -1
- package/out/zero-client/src/client/connection.js.map +1 -1
- package/out/zero-client/src/client/crud-impl.d.ts +1 -1
- package/out/zero-client/src/client/crud-impl.d.ts.map +1 -1
- package/out/zero-client/src/client/crud-impl.js +1 -1
- package/out/zero-client/src/client/crud-impl.js.map +1 -1
- package/out/zero-client/src/client/crud.d.ts +1 -1
- package/out/zero-client/src/client/crud.d.ts.map +1 -1
- package/out/zero-client/src/client/crud.js +1 -1
- package/out/zero-client/src/client/crud.js.map +1 -1
- package/out/zero-client/src/client/keys.d.ts +1 -1
- package/out/zero-client/src/client/keys.d.ts.map +1 -1
- package/out/zero-client/src/client/keys.js.map +1 -1
- package/out/zero-client/src/client/make-replicache-mutators.js +1 -1
- package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
- package/out/zero-client/src/client/mutation-tracker.d.ts +2 -1
- package/out/zero-client/src/client/mutation-tracker.d.ts.map +1 -1
- package/out/zero-client/src/client/mutation-tracker.js +3 -3
- package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +2 -2
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-client/src/types/client-state.d.ts +1 -1
- package/out/zero-client/src/types/client-state.d.ts.map +1 -1
- package/out/zero-protocol/src/custom-queries.js +1 -1
- package/out/zero-protocol/src/down.js +1 -1
- package/out/zero-protocol/src/error-kind-enum.d.ts +1 -2
- package/out/zero-protocol/src/error-kind-enum.d.ts.map +1 -1
- package/out/zero-protocol/src/error-kind-enum.js.map +1 -1
- package/out/zero-protocol/src/mutate-server.d.ts +165 -0
- package/out/zero-protocol/src/mutate-server.d.ts.map +1 -0
- package/out/zero-protocol/src/mutate-server.js +24 -0
- package/out/zero-protocol/src/mutate-server.js.map +1 -0
- package/out/zero-protocol/src/mutation.d.ts +229 -0
- package/out/zero-protocol/src/mutation.d.ts.map +1 -0
- package/out/zero-protocol/src/mutation.js +112 -0
- package/out/zero-protocol/src/mutation.js.map +1 -0
- package/out/zero-protocol/src/mutations-patch.js +1 -1
- package/out/zero-protocol/src/mutations-patch.js.map +1 -1
- package/out/zero-protocol/src/push.d.ts +3 -234
- package/out/zero-protocol/src/push.d.ts.map +1 -1
- package/out/zero-protocol/src/push.js +3 -114
- package/out/zero-protocol/src/push.js.map +1 -1
- package/out/zero-protocol/src/query-server.d.ts +150 -0
- package/out/zero-protocol/src/query-server.d.ts.map +1 -0
- package/out/zero-protocol/src/query-server.js +16 -0
- package/out/zero-protocol/src/query-server.js.map +1 -0
- package/out/zero-protocol/src/up.js +1 -1
- package/out/zero-server/src/mod.d.ts +4 -2
- package/out/zero-server/src/mod.d.ts.map +1 -1
- package/out/zero-server/src/process-mutations.d.ts +50 -4
- package/out/zero-server/src/process-mutations.d.ts.map +1 -1
- package/out/zero-server/src/process-mutations.js +73 -36
- package/out/zero-server/src/process-mutations.js.map +1 -1
- package/out/zero-server/src/push-processor.d.ts +3 -3
- package/out/zero-server/src/push-processor.d.ts.map +1 -1
- package/out/zero-server/src/push-processor.js.map +1 -1
- package/out/zero-server/src/queries/process-queries.d.ts +45 -53
- package/out/zero-server/src/queries/process-queries.d.ts.map +1 -1
- package/out/zero-server/src/queries/process-queries.js +72 -53
- package/out/zero-server/src/queries/process-queries.js.map +1 -1
- package/out/zero-server/src/zql-database.js.map +1 -1
- package/out/zero-types/src/default-types.d.ts +1 -0
- package/out/zero-types/src/default-types.d.ts.map +1 -1
- package/out/zql/src/builder/builder.d.ts.map +1 -1
- package/out/zql/src/builder/builder.js +17 -7
- package/out/zql/src/builder/builder.js.map +1 -1
- package/out/zql/src/ivm/cap.d.ts +32 -0
- package/out/zql/src/ivm/cap.d.ts.map +1 -0
- package/out/zql/src/ivm/cap.js +205 -0
- package/out/zql/src/ivm/cap.js.map +1 -0
- package/out/zql/src/ivm/constraint.js +1 -1
- package/out/zql/src/ivm/flipped-join.d.ts.map +1 -1
- package/out/zql/src/ivm/flipped-join.js +61 -15
- package/out/zql/src/ivm/flipped-join.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +3 -4
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/schema.d.ts +8 -0
- package/out/zql/src/ivm/schema.d.ts.map +1 -1
- package/out/zql/src/ivm/take.js +2 -2
- package/out/zql/src/mutate/mutator-registry.js.map +1 -1
- package/out/zql/src/mutate/mutator.d.ts +11 -2
- package/out/zql/src/mutate/mutator.d.ts.map +1 -1
- package/out/zql/src/mutate/mutator.js.map +1 -1
- package/out/zql/src/query/query-registry.d.ts +9 -2
- package/out/zql/src/query/query-registry.d.ts.map +1 -1
- package/out/zql/src/query/query-registry.js.map +1 -1
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +4 -1
- package/out/zqlite/src/table-source.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, { createWebSocketStream } from "ws";
|
|
9
|
+
import WebSocket$1, { 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.OPEN) ws.send(JSON.stringify(data), callback === "ignore-backpressure" ? void 0 : callback);
|
|
183
|
+
if (ws.readyState === WebSocket$1.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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"}
|
|
@@ -8,7 +8,7 @@ import type { ConnectParams } from './connect-params.ts';
|
|
|
8
8
|
import type { HandlerResult, MessageHandler } from './connection.ts';
|
|
9
9
|
export declare class SyncerWsMessageHandler implements MessageHandler {
|
|
10
10
|
#private;
|
|
11
|
-
constructor(lc: LogContext, connectParams: ConnectParams,
|
|
11
|
+
constructor(lc: LogContext, connectParams: ConnectParams, connContextManager: ConnectionContextManager, viewSyncer: ViewSyncer, mutagen: Mutagen | undefined, pusher: Pusher | undefined);
|
|
12
12
|
handleMessage(msg: Upstream): Promise<HandlerResult[]>;
|
|
13
13
|
}
|
|
14
14
|
//# sourceMappingURL=syncer-ws-message-handler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncer-ws-message-handler.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/workers/syncer-ws-message-handler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAOjD,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,kCAAkC,CAAC;AAC/D,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,gCAAgC,CAAC;AAC5D,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EACL,KAAK,wBAAwB,EAE9B,MAAM,uDAAuD,CAAC;AAC/D,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,wCAAwC,CAAC;AACvE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAC,aAAa,EAAE,cAAc,EAAC,MAAM,iBAAiB,CAAC;AAiBnE,qBAAa,sBAAuB,YAAW,cAAc;;gBAWzD,EAAE,EAAE,UAAU,EACd,aAAa,EAAE,aAAa,EAC5B,
|
|
1
|
+
{"version":3,"file":"syncer-ws-message-handler.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/workers/syncer-ws-message-handler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAOjD,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,kCAAkC,CAAC;AAC/D,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,gCAAgC,CAAC;AAC5D,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EACL,KAAK,wBAAwB,EAE9B,MAAM,uDAAuD,CAAC;AAC/D,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,wCAAwC,CAAC;AACvE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAC,aAAa,EAAE,cAAc,EAAC,MAAM,iBAAiB,CAAC;AAiBnE,qBAAa,sBAAuB,YAAW,cAAc;;gBAWzD,EAAE,EAAE,UAAU,EACd,aAAa,EAAE,aAAa,EAC5B,kBAAkB,EAAE,wBAAwB,EAC5C,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,OAAO,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,GAAG,SAAS;IAoBtB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;CAmN7D"}
|
|
@@ -26,13 +26,13 @@ var SyncerWsMessageHandler = class {
|
|
|
26
26
|
#lc;
|
|
27
27
|
#clientGroupID;
|
|
28
28
|
#connectionSelector;
|
|
29
|
-
#
|
|
29
|
+
#connContextManager;
|
|
30
30
|
#pusher;
|
|
31
|
-
constructor(lc, connectParams,
|
|
31
|
+
constructor(lc, connectParams, connContextManager, viewSyncer, mutagen, pusher) {
|
|
32
32
|
const { clientGroupID, clientID, wsID } = connectParams;
|
|
33
33
|
this.#viewSyncer = viewSyncer;
|
|
34
34
|
this.#mutagen = mutagen;
|
|
35
|
-
this.#
|
|
35
|
+
this.#connContextManager = connContextManager;
|
|
36
36
|
this.#mutationLock = new Lock();
|
|
37
37
|
this.#lc = lc.withContext("connection").withContext("clientID", clientID).withContext("clientGroupID", clientGroupID).withContext("wsID", wsID);
|
|
38
38
|
this.#clientGroupID = clientGroupID;
|
|
@@ -84,7 +84,7 @@ var SyncerWsMessageHandler = class {
|
|
|
84
84
|
origin: ZeroCache
|
|
85
85
|
}
|
|
86
86
|
}];
|
|
87
|
-
const auth = this.#
|
|
87
|
+
const auth = this.#connContextManager.mustGetConnectionContext(this.#connectionSelector).auth;
|
|
88
88
|
assert(auth?.type !== "opaque", "Only JWT auth is supported for CRUD mutations");
|
|
89
89
|
return [await this.#mutationLock.withLock(async () => {
|
|
90
90
|
const errors = [];
|
|
@@ -108,8 +108,8 @@ var SyncerWsMessageHandler = class {
|
|
|
108
108
|
break;
|
|
109
109
|
case "updateAuth":
|
|
110
110
|
await startAsyncSpan(tracer, "connection.updateAuth", async () => {
|
|
111
|
-
const
|
|
112
|
-
const authRevisionChanged = (await this.#
|
|
111
|
+
const initialConnCtx = this.#connContextManager.mustGetConnectionContext(this.#connectionSelector);
|
|
112
|
+
const authRevisionChanged = (await this.#connContextManager.updateAuth(this.#connectionSelector, msg[1])).revision !== initialConnCtx.revision;
|
|
113
113
|
await viewSyncer.updateAuth(this.#connectionSelector, msg, authRevisionChanged);
|
|
114
114
|
});
|
|
115
115
|
break;
|
|
@@ -119,7 +119,7 @@ var SyncerWsMessageHandler = class {
|
|
|
119
119
|
break;
|
|
120
120
|
}
|
|
121
121
|
case "initConnection":
|
|
122
|
-
this.#
|
|
122
|
+
this.#connContextManager.initConnection(this.#connectionSelector, msg[1]);
|
|
123
123
|
return withTraceparent(msg[1].traceparent, () => {
|
|
124
124
|
const ret = [{
|
|
125
125
|
type: "stream",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncer-ws-message-handler.js","names":["#viewSyncer","#mutagen","#mutationLock","#lc","#clientGroupID","#connectionSelector","#contextManager","#pusher"],"sources":["../../../../../zero-cache/src/workers/syncer-ws-message-handler.ts"],"sourcesContent":["import {ROOT_CONTEXT, context, propagation, trace} from '@opentelemetry/api';\nimport {Lock} from '@rocicorp/lock';\nimport type {LogContext} from '@rocicorp/logger';\nimport {startAsyncSpan, startSpan} from '../../../otel/src/span.ts';\nimport {version} from '../../../otel/src/version.ts';\nimport {assert, unreachable} from '../../../shared/src/asserts.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 type {Upstream} from '../../../zero-protocol/src/up.ts';\nimport type {Mutagen} from '../services/mutagen/mutagen.ts';\nimport type {Pusher} from '../services/mutagen/pusher.ts';\nimport {\n type ConnectionContextManager,\n type ConnectionSelector,\n} from '../services/view-syncer/connection-context-manager.ts';\nimport {type ViewSyncer} from '../services/view-syncer/view-syncer.ts';\nimport type {ConnectParams} from './connect-params.ts';\nimport type {HandlerResult, MessageHandler} from './connection.ts';\n\nconst tracer = trace.getTracer('syncer-ws-server', version);\n\n/**\n * Wraps a function in an OTEL context extracted from a W3C traceparent header.\n * This enables distributed tracing from the client through zero-cache to\n * the user's API server.\n */\nfunction withTraceparent<T>(traceparent: string | undefined, fn: () => T): T {\n if (!traceparent) {\n return fn();\n }\n const extracted = propagation.extract(ROOT_CONTEXT, {traceparent});\n return context.with(extracted, fn);\n}\n\nexport class SyncerWsMessageHandler implements MessageHandler {\n readonly #viewSyncer: ViewSyncer;\n readonly #mutagen: Mutagen | undefined;\n readonly #mutationLock: Lock;\n readonly #lc: LogContext;\n readonly #clientGroupID: string;\n readonly #connectionSelector: ConnectionSelector;\n readonly #contextManager: ConnectionContextManager;\n readonly #pusher: Pusher | undefined;\n\n constructor(\n lc: LogContext,\n connectParams: ConnectParams,\n contextManager: ConnectionContextManager,\n viewSyncer: ViewSyncer,\n mutagen: Mutagen | undefined,\n pusher: Pusher | undefined,\n ) {\n const {clientGroupID, clientID, wsID} = connectParams;\n this.#viewSyncer = viewSyncer;\n this.#mutagen = mutagen;\n this.#contextManager = contextManager;\n this.#mutationLock = new Lock();\n this.#lc = lc\n .withContext('connection')\n .withContext('clientID', clientID)\n .withContext('clientGroupID', clientGroupID)\n .withContext('wsID', wsID);\n this.#clientGroupID = clientGroupID;\n this.#pusher = pusher;\n this.#connectionSelector = {\n clientID,\n wsID,\n };\n }\n\n async handleMessage(msg: Upstream): Promise<HandlerResult[]> {\n const lc = this.#lc;\n const msgType = msg[0];\n const viewSyncer = this.#viewSyncer;\n switch (msgType) {\n case 'ping':\n lc.error?.('Ping is not supported at this layer by Zero');\n break;\n case 'pull':\n lc.error?.('Pull is not supported by Zero');\n break;\n case 'push': {\n return withTraceparent(msg[1].traceparent, () =>\n startAsyncSpan<HandlerResult[]>(\n tracer,\n 'connection.push',\n async () => {\n const {clientGroupID, mutations} = msg[1];\n if (clientGroupID !== this.#clientGroupID) {\n return [\n {\n type: 'fatal',\n error: {\n kind: ErrorKind.InvalidPush,\n message:\n `clientGroupID in mutation \"${clientGroupID}\" does not match ` +\n `clientGroupID of connection \"${this.#clientGroupID}`,\n origin: ErrorOrigin.ZeroCache,\n },\n } satisfies HandlerResult,\n ];\n }\n\n if (mutations.length === 0) {\n return [\n {\n type: 'ok',\n },\n ];\n }\n\n // The client only ever sends 1 mutation per push.\n // #pusher will throw if it sees a CRUD mutation.\n // #mutagen will throw if it see a custom mutation.\n if (mutations[0].type === 'custom') {\n if (!this.#pusher) {\n return [\n {\n type: 'fatal',\n error: {\n kind: ErrorKind.InvalidPush,\n message:\n 'A ZERO_MUTATE_URL must be set in order to process custom mutations.',\n origin: ErrorOrigin.ZeroCache,\n },\n } satisfies HandlerResult,\n ];\n }\n return [\n this.#pusher.enqueuePush(this.#connectionSelector, msg[1]),\n ];\n }\n\n const mutagen = this.#mutagen;\n if (!mutagen) {\n return [\n {\n type: 'fatal',\n error: {\n kind: ErrorKind.InvalidPush,\n message: `Support for legacy CRUD mutations is disabled`,\n origin: ErrorOrigin.ZeroCache,\n },\n } satisfies HandlerResult,\n ];\n }\n\n const auth = this.#contextManager.mustGetConnectionContext(\n this.#connectionSelector,\n ).auth;\n assert(\n auth?.type !== 'opaque',\n 'Only JWT auth is supported for CRUD mutations',\n );\n\n // Hold a connection-level lock while processing mutations so that:\n // 1. Mutations are processed in the order in which they are received and\n // 2. A single view syncer connection cannot hog multiple upstream connections.\n const ret = await this.#mutationLock.withLock(async () => {\n const errors: ErrorBody[] = [];\n for (const mutation of mutations) {\n const maybeError = await mutagen.processMutation(\n mutation,\n auth?.decoded,\n this.#pusher !== undefined,\n );\n if (maybeError !== undefined) {\n errors.push({\n kind: maybeError[0],\n message: maybeError[1],\n origin: ErrorOrigin.ZeroCache,\n });\n }\n }\n if (errors.length > 0) {\n return {type: 'transient', errors} satisfies HandlerResult;\n }\n return {type: 'ok'} satisfies HandlerResult;\n });\n return [ret];\n },\n ),\n );\n }\n case 'changeDesiredQueries':\n await withTraceparent(msg[1].traceparent, () =>\n startAsyncSpan(tracer, 'connection.changeDesiredQueries', () =>\n viewSyncer.changeDesiredQueries(this.#connectionSelector, msg),\n ),\n );\n break;\n case 'updateAuth':\n await startAsyncSpan(tracer, 'connection.updateAuth', async () => {\n const initialConnection =\n this.#contextManager.mustGetConnectionContext(\n this.#connectionSelector,\n );\n const updatedConnection = await this.#contextManager.updateAuth(\n this.#connectionSelector,\n msg[1],\n );\n const authRevisionChanged =\n updatedConnection.revision !== initialConnection.revision;\n\n await viewSyncer.updateAuth(\n this.#connectionSelector,\n msg,\n authRevisionChanged,\n );\n });\n break;\n case 'deleteClients': {\n const deletedClientIDs = await startAsyncSpan(\n tracer,\n 'connection.deleteClients',\n () => viewSyncer.deleteClients(this.#connectionSelector, msg),\n );\n if (this.#pusher && deletedClientIDs.length > 0) {\n await this.#pusher.deleteClientMutations(\n this.#connectionSelector,\n deletedClientIDs,\n );\n }\n break;\n }\n case 'initConnection': {\n this.#contextManager.initConnection(this.#connectionSelector, msg[1]);\n return withTraceparent(msg[1].traceparent, () => {\n const ret: HandlerResult[] = [\n {\n type: 'stream',\n source: 'viewSyncer',\n stream: startSpan(tracer, 'connection.initConnection', () =>\n viewSyncer.initConnection(this.#connectionSelector, msg),\n ),\n },\n ];\n\n // Given we support both CRUD and Custom mutators,\n // we do not initialize the `pusher` unless the user has opted\n // into custom mutations. We detect that by checking\n // if the pushURL has been set.\n if (this.#pusher) {\n ret.push({\n type: 'stream',\n source: 'pusher',\n stream: this.#pusher.initConnection(this.#connectionSelector),\n });\n }\n\n return ret;\n });\n }\n case 'closeConnection':\n // This message is deprecated and no longer used.\n break;\n\n case 'inspect':\n await startAsyncSpan(tracer, 'connection.inspect', () =>\n viewSyncer.inspect(this.#connectionSelector, msg),\n );\n break;\n\n case 'ackMutationResponses':\n if (this.#pusher) {\n await this.#pusher.ackMutationResponses(\n this.#connectionSelector,\n msg[1],\n );\n }\n break;\n\n default:\n unreachable(msgType);\n }\n\n return [{type: 'ok'}];\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"syncer-ws-message-handler.js","names":["#viewSyncer","#mutagen","#mutationLock","#lc","#clientGroupID","#connectionSelector","#connContextManager","#pusher"],"sources":["../../../../../zero-cache/src/workers/syncer-ws-message-handler.ts"],"sourcesContent":["import {ROOT_CONTEXT, context, propagation, trace} from '@opentelemetry/api';\nimport {Lock} from '@rocicorp/lock';\nimport type {LogContext} from '@rocicorp/logger';\nimport {startAsyncSpan, startSpan} from '../../../otel/src/span.ts';\nimport {version} from '../../../otel/src/version.ts';\nimport {assert, unreachable} from '../../../shared/src/asserts.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 type {Upstream} from '../../../zero-protocol/src/up.ts';\nimport type {Mutagen} from '../services/mutagen/mutagen.ts';\nimport type {Pusher} from '../services/mutagen/pusher.ts';\nimport {\n type ConnectionContextManager,\n type ConnectionSelector,\n} from '../services/view-syncer/connection-context-manager.ts';\nimport {type ViewSyncer} from '../services/view-syncer/view-syncer.ts';\nimport type {ConnectParams} from './connect-params.ts';\nimport type {HandlerResult, MessageHandler} from './connection.ts';\n\nconst tracer = trace.getTracer('syncer-ws-server', version);\n\n/**\n * Wraps a function in an OTEL context extracted from a W3C traceparent header.\n * This enables distributed tracing from the client through zero-cache to\n * the user's API server.\n */\nfunction withTraceparent<T>(traceparent: string | undefined, fn: () => T): T {\n if (!traceparent) {\n return fn();\n }\n const extracted = propagation.extract(ROOT_CONTEXT, {traceparent});\n return context.with(extracted, fn);\n}\n\nexport class SyncerWsMessageHandler implements MessageHandler {\n readonly #viewSyncer: ViewSyncer;\n readonly #mutagen: Mutagen | undefined;\n readonly #mutationLock: Lock;\n readonly #lc: LogContext;\n readonly #clientGroupID: string;\n readonly #connectionSelector: ConnectionSelector;\n readonly #connContextManager: ConnectionContextManager;\n readonly #pusher: Pusher | undefined;\n\n constructor(\n lc: LogContext,\n connectParams: ConnectParams,\n connContextManager: ConnectionContextManager,\n viewSyncer: ViewSyncer,\n mutagen: Mutagen | undefined,\n pusher: Pusher | undefined,\n ) {\n const {clientGroupID, clientID, wsID} = connectParams;\n this.#viewSyncer = viewSyncer;\n this.#mutagen = mutagen;\n this.#connContextManager = connContextManager;\n this.#mutationLock = new Lock();\n this.#lc = lc\n .withContext('connection')\n .withContext('clientID', clientID)\n .withContext('clientGroupID', clientGroupID)\n .withContext('wsID', wsID);\n this.#clientGroupID = clientGroupID;\n this.#pusher = pusher;\n this.#connectionSelector = {\n clientID,\n wsID,\n };\n }\n\n async handleMessage(msg: Upstream): Promise<HandlerResult[]> {\n const lc = this.#lc;\n const msgType = msg[0];\n const viewSyncer = this.#viewSyncer;\n switch (msgType) {\n case 'ping':\n lc.error?.('Ping is not supported at this layer by Zero');\n break;\n case 'pull':\n lc.error?.('Pull is not supported by Zero');\n break;\n case 'push': {\n return withTraceparent(msg[1].traceparent, () =>\n startAsyncSpan<HandlerResult[]>(\n tracer,\n 'connection.push',\n async () => {\n const {clientGroupID, mutations} = msg[1];\n if (clientGroupID !== this.#clientGroupID) {\n return [\n {\n type: 'fatal',\n error: {\n kind: ErrorKind.InvalidPush,\n message:\n `clientGroupID in mutation \"${clientGroupID}\" does not match ` +\n `clientGroupID of connection \"${this.#clientGroupID}`,\n origin: ErrorOrigin.ZeroCache,\n },\n } satisfies HandlerResult,\n ];\n }\n\n if (mutations.length === 0) {\n return [\n {\n type: 'ok',\n },\n ];\n }\n\n // The client only ever sends 1 mutation per push.\n // #pusher will throw if it sees a CRUD mutation.\n // #mutagen will throw if it see a custom mutation.\n if (mutations[0].type === 'custom') {\n if (!this.#pusher) {\n return [\n {\n type: 'fatal',\n error: {\n kind: ErrorKind.InvalidPush,\n message:\n 'A ZERO_MUTATE_URL must be set in order to process custom mutations.',\n origin: ErrorOrigin.ZeroCache,\n },\n } satisfies HandlerResult,\n ];\n }\n return [\n this.#pusher.enqueuePush(this.#connectionSelector, msg[1]),\n ];\n }\n\n const mutagen = this.#mutagen;\n if (!mutagen) {\n return [\n {\n type: 'fatal',\n error: {\n kind: ErrorKind.InvalidPush,\n message: `Support for legacy CRUD mutations is disabled`,\n origin: ErrorOrigin.ZeroCache,\n },\n } satisfies HandlerResult,\n ];\n }\n\n const auth = this.#connContextManager.mustGetConnectionContext(\n this.#connectionSelector,\n ).auth;\n assert(\n auth?.type !== 'opaque',\n 'Only JWT auth is supported for CRUD mutations',\n );\n\n // Hold a connection-level lock while processing mutations so that:\n // 1. Mutations are processed in the order in which they are received and\n // 2. A single view syncer connection cannot hog multiple upstream connections.\n const ret = await this.#mutationLock.withLock(async () => {\n const errors: ErrorBody[] = [];\n for (const mutation of mutations) {\n const maybeError = await mutagen.processMutation(\n mutation,\n auth?.decoded,\n this.#pusher !== undefined,\n );\n if (maybeError !== undefined) {\n errors.push({\n kind: maybeError[0],\n message: maybeError[1],\n origin: ErrorOrigin.ZeroCache,\n });\n }\n }\n if (errors.length > 0) {\n return {type: 'transient', errors} satisfies HandlerResult;\n }\n return {type: 'ok'} satisfies HandlerResult;\n });\n return [ret];\n },\n ),\n );\n }\n case 'changeDesiredQueries':\n await withTraceparent(msg[1].traceparent, () =>\n startAsyncSpan(tracer, 'connection.changeDesiredQueries', () =>\n viewSyncer.changeDesiredQueries(this.#connectionSelector, msg),\n ),\n );\n break;\n case 'updateAuth':\n await startAsyncSpan(tracer, 'connection.updateAuth', async () => {\n const initialConnCtx =\n this.#connContextManager.mustGetConnectionContext(\n this.#connectionSelector,\n );\n const updatedConnCtx = await this.#connContextManager.updateAuth(\n this.#connectionSelector,\n msg[1],\n );\n const authRevisionChanged =\n updatedConnCtx.revision !== initialConnCtx.revision;\n\n await viewSyncer.updateAuth(\n this.#connectionSelector,\n msg,\n authRevisionChanged,\n );\n });\n break;\n case 'deleteClients': {\n const deletedClientIDs = await startAsyncSpan(\n tracer,\n 'connection.deleteClients',\n () => viewSyncer.deleteClients(this.#connectionSelector, msg),\n );\n if (this.#pusher && deletedClientIDs.length > 0) {\n await this.#pusher.deleteClientMutations(\n this.#connectionSelector,\n deletedClientIDs,\n );\n }\n break;\n }\n case 'initConnection': {\n this.#connContextManager.initConnection(\n this.#connectionSelector,\n msg[1],\n );\n return withTraceparent(msg[1].traceparent, () => {\n const ret: HandlerResult[] = [\n {\n type: 'stream',\n source: 'viewSyncer',\n stream: startSpan(tracer, 'connection.initConnection', () =>\n viewSyncer.initConnection(this.#connectionSelector, msg),\n ),\n },\n ];\n\n // Given we support both CRUD and Custom mutators,\n // we do not initialize the `pusher` unless the user has opted\n // into custom mutations. We detect that by checking\n // if the pushURL has been set.\n if (this.#pusher) {\n ret.push({\n type: 'stream',\n source: 'pusher',\n stream: this.#pusher.initConnection(this.#connectionSelector),\n });\n }\n\n return ret;\n });\n }\n case 'closeConnection':\n // This message is deprecated and no longer used.\n break;\n\n case 'inspect':\n await startAsyncSpan(tracer, 'connection.inspect', () =>\n viewSyncer.inspect(this.#connectionSelector, msg),\n );\n break;\n\n case 'ackMutationResponses':\n if (this.#pusher) {\n await this.#pusher.ackMutationResponses(\n this.#connectionSelector,\n msg[1],\n );\n }\n break;\n\n default:\n unreachable(msgType);\n }\n\n return [{type: 'ok'}];\n }\n}\n"],"mappings":"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"}
|
|
@@ -24,7 +24,7 @@ export type SyncerWorkerData = {
|
|
|
24
24
|
export declare class Syncer implements SingletonService {
|
|
25
25
|
#private;
|
|
26
26
|
readonly id: string;
|
|
27
|
-
constructor(lc: LogContext, config: ZeroConfig, viewSyncerFactory: (id: string, sub: Subscription<ReplicaState>, drainCoordinator: DrainCoordinator) => ViewSyncer & ActivityBasedService, mutagenFactory: ((id: string) => Mutagen & Service) | undefined, pusherFactory: ((id: string,
|
|
27
|
+
constructor(lc: LogContext, config: ZeroConfig, viewSyncerFactory: (id: string, sub: Subscription<ReplicaState>, drainCoordinator: DrainCoordinator) => ViewSyncer & ActivityBasedService, mutagenFactory: ((id: string) => Mutagen & Service) | undefined, pusherFactory: ((id: string, connContextManager: ConnectionContextManager) => Pusher & Service) | undefined, parent: Worker, validateLegacyJWT: ValidateLegacyJWT | undefined);
|
|
28
28
|
run(): Promise<void>;
|
|
29
29
|
/**
|
|
30
30
|
* Graceful shutdown involves shutting down view syncers one at a time, pausing
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/workers/syncer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAUjD,OAAO,EAAyB,KAAK,iBAAiB,EAAC,MAAM,iBAAiB,CAAC;AAE/E,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,0BAA0B,CAAC;AAOzD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,gCAAgC,CAAC;AAC5D,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,+BAA+B,CAAC;AAC1D,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,sCAAsC,CAAC;AAEvE,OAAO,KAAK,EACV,oBAAoB,EACpB,OAAO,EACP,gBAAgB,EACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,uDAAuD,CAAC;AACpG,OAAO,EAAC,gBAAgB,EAAC,MAAM,8CAA8C,CAAC;AAC9E,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,wCAAwC,CAAC;AACvE,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,0BAA0B,CAAC;AAO3D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,cAAc,EAAE,WAAW,CAAC;CAC7B,CAAC;AA4BF;;;;;;GAMG;AACH,qBAAa,MAAO,YAAW,gBAAgB;;IAC7C,QAAQ,CAAC,EAAE,SAAmB;gBAc5B,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,UAAU,EAClB,iBAAiB,EAAE,CACjB,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,YAAY,CAAC,YAAY,CAAC,EAC/B,gBAAgB,EAAE,gBAAgB,KAC/B,UAAU,GAAG,oBAAoB,EACtC,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,GAAG,SAAS,EAC/D,aAAa,EACT,CAAC,CACC,EAAE,EAAE,MAAM,EACV,
|
|
1
|
+
{"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/workers/syncer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAUjD,OAAO,EAAyB,KAAK,iBAAiB,EAAC,MAAM,iBAAiB,CAAC;AAE/E,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,0BAA0B,CAAC;AAOzD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,gCAAgC,CAAC;AAC5D,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,+BAA+B,CAAC;AAC1D,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,sCAAsC,CAAC;AAEvE,OAAO,KAAK,EACV,oBAAoB,EACpB,OAAO,EACP,gBAAgB,EACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,uDAAuD,CAAC;AACpG,OAAO,EAAC,gBAAgB,EAAC,MAAM,8CAA8C,CAAC;AAC9E,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,wCAAwC,CAAC;AACvE,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,0BAA0B,CAAC;AAO3D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,cAAc,EAAE,WAAW,CAAC;CAC7B,CAAC;AA4BF;;;;;;GAMG;AACH,qBAAa,MAAO,YAAW,gBAAgB;;IAC7C,QAAQ,CAAC,EAAE,SAAmB;gBAc5B,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,UAAU,EAClB,iBAAiB,EAAE,CACjB,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,YAAY,CAAC,YAAY,CAAC,EAC/B,gBAAgB,EAAE,gBAAgB,KAC/B,UAAU,GAAG,oBAAoB,EACtC,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,GAAG,SAAS,EAC/D,aAAa,EACT,CAAC,CACC,EAAE,EAAE,MAAM,EACV,kBAAkB,EAAE,wBAAwB,KACzC,MAAM,GAAG,OAAO,CAAC,GACtB,SAAS,EACb,MAAM,EAAE,MAAM,EACd,iBAAiB,EAAE,iBAAiB,GAAG,SAAS;IA6OlD,GAAG;IAIH;;;;;OAKG;IACG,KAAK;IAqBX,IAAI;CAKL"}
|
|
@@ -60,7 +60,7 @@ var Syncer = class {
|
|
|
60
60
|
this.#lc = lc;
|
|
61
61
|
this.#viewSyncers = new ServiceRunner(lc, (id) => viewSyncerFactory(id, notifier.subscribe(), this.#drainCoordinator), (v) => v.keepalive());
|
|
62
62
|
if (mutagenFactory) this.#mutagens = new ServiceRunner(lc, mutagenFactory, (m) => m.hasRefs());
|
|
63
|
-
if (pusherFactory) this.#pushers = new ServiceRunner(lc, (id) => pusherFactory(id, this.#viewSyncers.getService(id).
|
|
63
|
+
if (pusherFactory) this.#pushers = new ServiceRunner(lc, (id) => pusherFactory(id, this.#viewSyncers.getService(id).connContextManager), (p) => p.hasRefs());
|
|
64
64
|
this.#parent = parent;
|
|
65
65
|
this.#wss = new WebSocketServer(getWebSocketServerOptions(config));
|
|
66
66
|
installWebSocketReceiver(lc, this.#wss, this.#createConnection, this.#parent);
|
|
@@ -82,6 +82,7 @@ var Syncer = class {
|
|
|
82
82
|
recordConnectionAttempted();
|
|
83
83
|
const { clientID, clientGroupID, auth, userID } = params;
|
|
84
84
|
const hasProvidedAuth = auth !== void 0 && auth !== "";
|
|
85
|
+
const incomingUserID = userID ?? null;
|
|
85
86
|
if (hasProvidedAuth) {
|
|
86
87
|
const tokenOptions = tokenConfigOptions(this.#config.auth ?? {});
|
|
87
88
|
const hasPushOrMutate = this.#config?.push?.url !== void 0 || this.#config?.mutate?.url !== void 0;
|
|
@@ -90,13 +91,13 @@ var Syncer = class {
|
|
|
90
91
|
}
|
|
91
92
|
let initialAuth;
|
|
92
93
|
try {
|
|
93
|
-
initialAuth = await resolveAuth(this.#lc.withContext("clientGroupID", clientGroupID).withContext("clientID", clientID), void 0,
|
|
94
|
+
initialAuth = await resolveAuth(this.#lc.withContext("clientGroupID", clientGroupID).withContext("clientID", clientID), void 0, incomingUserID, auth, this.#validateLegacyJWT);
|
|
94
95
|
} catch (e) {
|
|
95
96
|
if (isProtocolError(e)) {
|
|
96
97
|
this.#lc.warn?.("Rejecting sync connection during initial auth resolution", {
|
|
97
98
|
clientGroupID,
|
|
98
99
|
clientID,
|
|
99
|
-
|
|
100
|
+
incomingUserID,
|
|
100
101
|
hasProvidedAuth,
|
|
101
102
|
errorKind: e.message
|
|
102
103
|
});
|
|
@@ -107,9 +108,9 @@ var Syncer = class {
|
|
|
107
108
|
throw e;
|
|
108
109
|
}
|
|
109
110
|
const viewSyncer = this.#viewSyncers.getService(clientGroupID);
|
|
110
|
-
const
|
|
111
|
-
const group =
|
|
112
|
-
if (group.
|
|
111
|
+
const connContextManager = viewSyncer.connContextManager;
|
|
112
|
+
const group = connContextManager.getGroupState();
|
|
113
|
+
if (group.pinnedUser !== void 0 && group.pinnedUser.id !== incomingUserID) {
|
|
113
114
|
const error = new ProtocolError({
|
|
114
115
|
kind: Unauthorized,
|
|
115
116
|
message: "Client groups are pinned to a single userID. Connection userID does not match existing client group userID.",
|
|
@@ -124,7 +125,7 @@ var Syncer = class {
|
|
|
124
125
|
this.#lc.debug?.(`client ${clientID} already connected, closing existing connection`);
|
|
125
126
|
existing.close(`replaced by ${params.wsID}`);
|
|
126
127
|
}
|
|
127
|
-
|
|
128
|
+
connContextManager.registerConnection({
|
|
128
129
|
clientID,
|
|
129
130
|
wsID: params.wsID
|
|
130
131
|
}, params, initialAuth);
|
|
@@ -134,8 +135,8 @@ var Syncer = class {
|
|
|
134
135
|
pusher?.ref();
|
|
135
136
|
let connection;
|
|
136
137
|
try {
|
|
137
|
-
connection = new Connection(this.#lc, params, ws, new SyncerWsMessageHandler(this.#lc, params,
|
|
138
|
-
|
|
138
|
+
connection = new Connection(this.#lc, params, ws, new SyncerWsMessageHandler(this.#lc, params, connContextManager, viewSyncer, mutagen, pusher), () => {
|
|
139
|
+
connContextManager.closeConnection({
|
|
139
140
|
clientID,
|
|
140
141
|
wsID: params.wsID
|
|
141
142
|
});
|
|
@@ -144,7 +145,7 @@ var Syncer = class {
|
|
|
144
145
|
pusher?.unref();
|
|
145
146
|
});
|
|
146
147
|
} catch (e) {
|
|
147
|
-
|
|
148
|
+
connContextManager.closeConnection({
|
|
148
149
|
clientID,
|
|
149
150
|
wsID: params.wsID
|
|
150
151
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncer.js","names":["#lc","#viewSyncers","#mutagens","#pushers","#connections","#drainCoordinator","#parent","#wss","#stopped","#config","#validateLegacyJWT","#createConnection"],"sources":["../../../../../zero-cache/src/workers/syncer.ts"],"sourcesContent":["import {pid} from 'node:process';\nimport type {MessagePort} from 'node:worker_threads';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {WebSocketServer, type ServerOptions, type WebSocket} from 'ws';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport {\n isProtocolError,\n ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {resolveAuth, type Auth, type ValidateLegacyJWT} from '../auth/auth.ts';\nimport {tokenConfigOptions} from '../auth/jwt.ts';\nimport {type ZeroConfig} from '../config/zero-config.ts';\nimport {getOrCreateGauge} from '../observability/metrics.ts';\nimport {\n recordConnectionAttempted,\n recordConnectionSuccess,\n setActiveClientGroupsGetter,\n} from '../server/anonymous-otel-start.ts';\nimport type {Mutagen} from '../services/mutagen/mutagen.ts';\nimport type {Pusher} from '../services/mutagen/pusher.ts';\nimport type {ReplicaState} from '../services/replicator/replicator.ts';\nimport {ServiceRunner} from '../services/runner.ts';\nimport type {\n ActivityBasedService,\n Service,\n SingletonService,\n} from '../services/service.ts';\nimport type {ConnectionContextManager} from '../services/view-syncer/connection-context-manager.ts';\nimport {DrainCoordinator} from '../services/view-syncer/drain-coordinator.ts';\nimport type {ViewSyncer} from '../services/view-syncer/view-syncer.ts';\nimport type {Worker} from '../types/processes.ts';\nimport type {Subscription} from '../types/subscription.ts';\nimport {installWebSocketReceiver} from '../types/websocket-handoff.ts';\nimport type {ConnectParams} from './connect-params.ts';\nimport {Connection, sendError} from './connection.ts';\nimport {createNotifierFrom, subscribeTo} from './replicator.ts';\nimport {SyncerWsMessageHandler} from './syncer-ws-message-handler.ts';\n\nexport type SyncerWorkerData = {\n replicatorPort: MessagePort;\n};\n\nfunction getWebSocketServerOptions(config: ZeroConfig): ServerOptions {\n const options: ServerOptions = {\n noServer: true,\n maxPayload: config.websocketMaxPayloadBytes,\n };\n\n if (config.websocketCompression) {\n options.perMessageDeflate = true;\n\n if (config.websocketCompressionOptions) {\n try {\n const compressionOptions = JSON.parse(\n config.websocketCompressionOptions,\n );\n options.perMessageDeflate = compressionOptions;\n } catch (e) {\n throw new Error(\n `Failed to parse ZERO_WEBSOCKET_COMPRESSION_OPTIONS: ${String(e)}. Expected valid JSON.`,\n );\n }\n }\n }\n\n return options;\n}\n\n/**\n * The Syncer worker receives websocket handoffs for \"/sync\" connections\n * from the Dispatcher in the main thread, and creates websocket\n * {@link Connection}s with a corresponding {@link ViewSyncer}, {@link Mutagen},\n * and {@link Subscription} to version notifications from the Replicator\n * worker.\n */\nexport class Syncer implements SingletonService {\n readonly id = `syncer-${pid}`;\n readonly #lc: LogContext;\n readonly #viewSyncers: ServiceRunner<ViewSyncer & ActivityBasedService>;\n readonly #mutagens: ServiceRunner<Mutagen & Service> | undefined;\n readonly #pushers: ServiceRunner<Pusher & Service> | undefined;\n readonly #connections = new Map<string, Connection>();\n readonly #drainCoordinator = new DrainCoordinator();\n readonly #parent: Worker;\n readonly #wss: WebSocketServer;\n readonly #stopped = resolver();\n readonly #config: ZeroConfig;\n readonly #validateLegacyJWT: ValidateLegacyJWT | undefined;\n\n constructor(\n lc: LogContext,\n config: ZeroConfig,\n viewSyncerFactory: (\n id: string,\n sub: Subscription<ReplicaState>,\n drainCoordinator: DrainCoordinator,\n ) => ViewSyncer & ActivityBasedService,\n mutagenFactory: ((id: string) => Mutagen & Service) | undefined,\n pusherFactory:\n | ((\n id: string,\n contextManager: ConnectionContextManager,\n ) => Pusher & Service)\n | undefined,\n parent: Worker,\n validateLegacyJWT: ValidateLegacyJWT | undefined,\n ) {\n this.#config = config;\n this.#validateLegacyJWT = validateLegacyJWT;\n // Relays notifications from the parent thread subscription\n // to ViewSyncers within this thread.\n const notifier = createNotifierFrom(lc, parent);\n subscribeTo(lc, parent);\n\n this.#lc = lc;\n this.#viewSyncers = new ServiceRunner(\n lc,\n id => viewSyncerFactory(id, notifier.subscribe(), this.#drainCoordinator),\n v => v.keepalive(),\n );\n if (mutagenFactory) {\n this.#mutagens = new ServiceRunner(lc, mutagenFactory, m => m.hasRefs());\n }\n if (pusherFactory) {\n this.#pushers = new ServiceRunner(\n lc,\n id =>\n pusherFactory(id, this.#viewSyncers.getService(id).contextManager),\n p => p.hasRefs(),\n );\n }\n this.#parent = parent;\n this.#wss = new WebSocketServer(getWebSocketServerOptions(config));\n\n installWebSocketReceiver(\n lc,\n this.#wss,\n this.#createConnection,\n this.#parent,\n );\n\n setActiveClientGroupsGetter(() => this.#viewSyncers.size);\n\n getOrCreateGauge(\n 'sync',\n 'active-client-groups',\n 'Number of active client groups',\n ).addCallback(result => result.observe(this.#viewSyncers.size));\n\n getOrCreateGauge(\n 'sync',\n 'queries',\n 'Active queries (pipelines) across all client groups',\n ).addCallback(result => {\n let total = 0;\n for (const vs of this.#viewSyncers.getServices()) {\n total += vs.queryCount;\n }\n result.observe(total);\n });\n\n getOrCreateGauge(\n 'sync',\n 'rows',\n 'Tracked rows across all client groups',\n ).addCallback(result => {\n let total = 0;\n for (const vs of this.#viewSyncers.getServices()) {\n total += vs.rowCount;\n }\n result.observe(total);\n });\n }\n\n readonly #createConnection = async (ws: WebSocket, params: ConnectParams) => {\n this.#lc.debug?.(\n 'creating connection',\n params.clientGroupID,\n params.clientID,\n );\n recordConnectionAttempted();\n const {clientID, clientGroupID, auth, userID} = params;\n const hasProvidedAuth = auth !== undefined && auth !== '';\n\n if (hasProvidedAuth) {\n const tokenOptions = tokenConfigOptions(this.#config.auth ?? {});\n\n const hasPushOrMutate =\n this.#config?.push?.url !== undefined ||\n this.#config?.mutate?.url !== undefined;\n const hasQueries =\n this.#config?.query?.url !== undefined ||\n this.#config?.getQueries?.url !== undefined;\n\n // must either have one of the token options set or have custom mutations & queries enabled\n const hasExactlyOneTokenOption = tokenOptions.length === 1;\n const hasCustomEndpoints = hasPushOrMutate && hasQueries;\n if (!hasExactlyOneTokenOption && !hasCustomEndpoints) {\n throw new Error(\n 'Exactly one of jwk, secret, or jwksUrl must be set in order to verify tokens but actually the following were set: ' +\n JSON.stringify(tokenOptions) +\n '. You may also set both ZERO_MUTATE_URL and ZERO_QUERY_URL to enable custom mutations and queries without passing token verification options.',\n );\n }\n }\n\n let initialAuth: Auth | undefined;\n\n // Verify JWT BEFORE touching existing connections - prevents unauthenticated\n // attackers from force-disconnecting legitimate users via DoS.\n try {\n initialAuth = await resolveAuth(\n this.#lc\n .withContext('clientGroupID', clientGroupID)\n .withContext('clientID', clientID),\n // no previous auth, since this is a new connection, and resolveAuth is\n // connection scoped, not client group scoped\n undefined,\n userID,\n auth,\n this.#validateLegacyJWT,\n );\n } catch (e) {\n if (isProtocolError(e)) {\n this.#lc.warn?.(\n 'Rejecting sync connection during initial auth resolution',\n {\n clientGroupID,\n clientID,\n userID,\n hasProvidedAuth,\n errorKind: e.message,\n },\n );\n sendError(this.#lc, ws, e.errorBody);\n ws.close(3000, e.errorBody.message);\n return;\n }\n throw e;\n }\n\n const viewSyncer = this.#viewSyncers.getService(clientGroupID);\n const contextManager = viewSyncer.contextManager;\n const group = contextManager.getGroupState();\n\n // TODO(0xcadams): we only check for user ID mismatch here if the group is\n // already validated. This prevents wrong-user reconnects from evicting a\n // healthy connection, but it does not protect against same-user reconnects\n // with an invalid opaque token. The long-term fix is to keep the replacement\n // connection pending until its auth is fully validated, and only then replace\n // the existing socket.\n if (group.validated && group.userID !== userID) {\n const error = new ProtocolError({\n kind: ErrorKind.Unauthorized,\n message:\n 'Client groups are pinned to a single userID. Connection userID does not match existing client group userID.',\n origin: ErrorOrigin.ZeroCache,\n });\n sendError(this.#lc, ws, error.errorBody);\n ws.close(3000, error.message);\n return;\n }\n\n // Check for and close existing connections AFTER auth is validated\n const existing = this.#connections.get(clientID);\n if (existing) {\n this.#lc.debug?.(\n `client ${clientID} already connected, closing existing connection`,\n );\n existing.close(`replaced by ${params.wsID}`);\n }\n\n contextManager.registerConnection(\n {clientID, wsID: params.wsID},\n params,\n initialAuth,\n );\n\n const mutagen = this.#mutagens?.getService(clientGroupID);\n const pusher = this.#pushers?.getService(clientGroupID);\n // a new connection is using the mutagen and pusher. Bump their ref counts.\n mutagen?.ref();\n pusher?.ref();\n\n let connection: Connection;\n try {\n connection = new Connection(\n this.#lc,\n params,\n ws,\n new SyncerWsMessageHandler(\n this.#lc,\n params,\n contextManager,\n viewSyncer,\n mutagen,\n pusher,\n ),\n () => {\n contextManager.closeConnection({\n clientID,\n wsID: params.wsID,\n });\n if (this.#connections.get(clientID) === connection) {\n this.#connections.delete(clientID);\n }\n // Connection is closed. We can unref the mutagen and pusher.\n // If their ref counts are zero, they will stop themselves and set themselves invalid.\n mutagen?.unref();\n pusher?.unref();\n },\n );\n } catch (e) {\n contextManager.closeConnection({clientID, wsID: params.wsID});\n mutagen?.unref();\n pusher?.unref();\n throw e;\n }\n\n this.#connections.set(clientID, connection);\n\n connection.init() && recordConnectionSuccess();\n\n if (params.initConnectionMsg) {\n this.#lc.debug?.(\n 'handling init connection message from sec header',\n params.clientGroupID,\n params.clientID,\n );\n await connection.handleInitConnection(\n JSON.stringify(params.initConnectionMsg),\n );\n }\n };\n\n run() {\n return this.#stopped.promise;\n }\n\n /**\n * Graceful shutdown involves shutting down view syncers one at a time, pausing\n * for the duration of view syncer's hydration between each one. This paces the\n * disconnects to avoid creating a backlog of hydrations in the receiving server\n * when the clients reconnect.\n */\n async drain() {\n const start = Date.now();\n this.#lc.info?.(`draining ${this.#viewSyncers.size} view-syncers`);\n\n this.#drainCoordinator.drainNextIn(0);\n\n while (this.#viewSyncers.size) {\n await this.#drainCoordinator.forceDrainTimeout;\n\n // Pick an arbitrary view syncer to force drain.\n for (const vs of this.#viewSyncers.getServices()) {\n this.#lc.debug?.(`draining view-syncer ${vs.id} (forced)`);\n // When this drain or an elective drain completes, the forceDrainTimeout will\n // resolve after the next drain interval.\n void vs.stop();\n break;\n }\n }\n this.#lc.info?.(`finished draining (${Date.now() - start} ms)`);\n }\n\n stop() {\n this.#wss.close();\n this.#stopped.resolve();\n return promiseVoid;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"syncer.js","names":["#lc","#viewSyncers","#mutagens","#pushers","#connections","#drainCoordinator","#parent","#wss","#stopped","#config","#validateLegacyJWT","#createConnection"],"sources":["../../../../../zero-cache/src/workers/syncer.ts"],"sourcesContent":["import {pid} from 'node:process';\nimport type {MessagePort} from 'node:worker_threads';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {WebSocketServer, type ServerOptions, type WebSocket} from 'ws';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport {\n isProtocolError,\n ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {resolveAuth, type Auth, type ValidateLegacyJWT} from '../auth/auth.ts';\nimport {tokenConfigOptions} from '../auth/jwt.ts';\nimport {type ZeroConfig} from '../config/zero-config.ts';\nimport {getOrCreateGauge} from '../observability/metrics.ts';\nimport {\n recordConnectionAttempted,\n recordConnectionSuccess,\n setActiveClientGroupsGetter,\n} from '../server/anonymous-otel-start.ts';\nimport type {Mutagen} from '../services/mutagen/mutagen.ts';\nimport type {Pusher} from '../services/mutagen/pusher.ts';\nimport type {ReplicaState} from '../services/replicator/replicator.ts';\nimport {ServiceRunner} from '../services/runner.ts';\nimport type {\n ActivityBasedService,\n Service,\n SingletonService,\n} from '../services/service.ts';\nimport type {ConnectionContextManager} from '../services/view-syncer/connection-context-manager.ts';\nimport {DrainCoordinator} from '../services/view-syncer/drain-coordinator.ts';\nimport type {ViewSyncer} from '../services/view-syncer/view-syncer.ts';\nimport type {Worker} from '../types/processes.ts';\nimport type {Subscription} from '../types/subscription.ts';\nimport {installWebSocketReceiver} from '../types/websocket-handoff.ts';\nimport type {ConnectParams} from './connect-params.ts';\nimport {Connection, sendError} from './connection.ts';\nimport {createNotifierFrom, subscribeTo} from './replicator.ts';\nimport {SyncerWsMessageHandler} from './syncer-ws-message-handler.ts';\n\nexport type SyncerWorkerData = {\n replicatorPort: MessagePort;\n};\n\nfunction getWebSocketServerOptions(config: ZeroConfig): ServerOptions {\n const options: ServerOptions = {\n noServer: true,\n maxPayload: config.websocketMaxPayloadBytes,\n };\n\n if (config.websocketCompression) {\n options.perMessageDeflate = true;\n\n if (config.websocketCompressionOptions) {\n try {\n const compressionOptions = JSON.parse(\n config.websocketCompressionOptions,\n );\n options.perMessageDeflate = compressionOptions;\n } catch (e) {\n throw new Error(\n `Failed to parse ZERO_WEBSOCKET_COMPRESSION_OPTIONS: ${String(e)}. Expected valid JSON.`,\n );\n }\n }\n }\n\n return options;\n}\n\n/**\n * The Syncer worker receives websocket handoffs for \"/sync\" connections\n * from the Dispatcher in the main thread, and creates websocket\n * {@link Connection}s with a corresponding {@link ViewSyncer}, {@link Mutagen},\n * and {@link Subscription} to version notifications from the Replicator\n * worker.\n */\nexport class Syncer implements SingletonService {\n readonly id = `syncer-${pid}`;\n readonly #lc: LogContext;\n readonly #viewSyncers: ServiceRunner<ViewSyncer & ActivityBasedService>;\n readonly #mutagens: ServiceRunner<Mutagen & Service> | undefined;\n readonly #pushers: ServiceRunner<Pusher & Service> | undefined;\n readonly #connections = new Map<string, Connection>();\n readonly #drainCoordinator = new DrainCoordinator();\n readonly #parent: Worker;\n readonly #wss: WebSocketServer;\n readonly #stopped = resolver();\n readonly #config: ZeroConfig;\n readonly #validateLegacyJWT: ValidateLegacyJWT | undefined;\n\n constructor(\n lc: LogContext,\n config: ZeroConfig,\n viewSyncerFactory: (\n id: string,\n sub: Subscription<ReplicaState>,\n drainCoordinator: DrainCoordinator,\n ) => ViewSyncer & ActivityBasedService,\n mutagenFactory: ((id: string) => Mutagen & Service) | undefined,\n pusherFactory:\n | ((\n id: string,\n connContextManager: ConnectionContextManager,\n ) => Pusher & Service)\n | undefined,\n parent: Worker,\n validateLegacyJWT: ValidateLegacyJWT | undefined,\n ) {\n this.#config = config;\n this.#validateLegacyJWT = validateLegacyJWT;\n // Relays notifications from the parent thread subscription\n // to ViewSyncers within this thread.\n const notifier = createNotifierFrom(lc, parent);\n subscribeTo(lc, parent);\n\n this.#lc = lc;\n this.#viewSyncers = new ServiceRunner(\n lc,\n id => viewSyncerFactory(id, notifier.subscribe(), this.#drainCoordinator),\n v => v.keepalive(),\n );\n if (mutagenFactory) {\n this.#mutagens = new ServiceRunner(lc, mutagenFactory, m => m.hasRefs());\n }\n if (pusherFactory) {\n this.#pushers = new ServiceRunner(\n lc,\n id =>\n pusherFactory(\n id,\n this.#viewSyncers.getService(id).connContextManager,\n ),\n p => p.hasRefs(),\n );\n }\n this.#parent = parent;\n this.#wss = new WebSocketServer(getWebSocketServerOptions(config));\n\n installWebSocketReceiver(\n lc,\n this.#wss,\n this.#createConnection,\n this.#parent,\n );\n\n setActiveClientGroupsGetter(() => this.#viewSyncers.size);\n\n getOrCreateGauge(\n 'sync',\n 'active-client-groups',\n 'Number of active client groups',\n ).addCallback(result => result.observe(this.#viewSyncers.size));\n\n getOrCreateGauge(\n 'sync',\n 'queries',\n 'Active queries (pipelines) across all client groups',\n ).addCallback(result => {\n let total = 0;\n for (const vs of this.#viewSyncers.getServices()) {\n total += vs.queryCount;\n }\n result.observe(total);\n });\n\n getOrCreateGauge(\n 'sync',\n 'rows',\n 'Tracked rows across all client groups',\n ).addCallback(result => {\n let total = 0;\n for (const vs of this.#viewSyncers.getServices()) {\n total += vs.rowCount;\n }\n result.observe(total);\n });\n }\n\n readonly #createConnection = async (ws: WebSocket, params: ConnectParams) => {\n this.#lc.debug?.(\n 'creating connection',\n params.clientGroupID,\n params.clientID,\n );\n recordConnectionAttempted();\n const {clientID, clientGroupID, auth, userID} = params;\n const hasProvidedAuth = auth !== undefined && auth !== '';\n const incomingUserID = userID ?? null;\n\n if (hasProvidedAuth) {\n const tokenOptions = tokenConfigOptions(this.#config.auth ?? {});\n\n const hasPushOrMutate =\n this.#config?.push?.url !== undefined ||\n this.#config?.mutate?.url !== undefined;\n const hasQueries =\n this.#config?.query?.url !== undefined ||\n this.#config?.getQueries?.url !== undefined;\n\n // must either have one of the token options set or have custom mutations & queries enabled\n const hasExactlyOneTokenOption = tokenOptions.length === 1;\n const hasCustomEndpoints = hasPushOrMutate && hasQueries;\n if (!hasExactlyOneTokenOption && !hasCustomEndpoints) {\n throw new Error(\n 'Exactly one of jwk, secret, or jwksUrl must be set in order to verify tokens but actually the following were set: ' +\n JSON.stringify(tokenOptions) +\n '. You may also set both ZERO_MUTATE_URL and ZERO_QUERY_URL to enable custom mutations and queries without passing token verification options.',\n );\n }\n }\n\n let initialAuth: Auth | undefined;\n\n // Verify JWT BEFORE touching existing connections - prevents unauthenticated\n // attackers from force-disconnecting legitimate users via DoS.\n try {\n initialAuth = await resolveAuth(\n this.#lc\n .withContext('clientGroupID', clientGroupID)\n .withContext('clientID', clientID),\n // no previous auth, since this is a new connection, and resolveAuth is\n // connection scoped, not client group scoped\n undefined,\n incomingUserID,\n auth,\n this.#validateLegacyJWT,\n );\n } catch (e) {\n if (isProtocolError(e)) {\n this.#lc.warn?.(\n 'Rejecting sync connection during initial auth resolution',\n {\n clientGroupID,\n clientID,\n incomingUserID,\n hasProvidedAuth,\n errorKind: e.message,\n },\n );\n sendError(this.#lc, ws, e.errorBody);\n ws.close(3000, e.errorBody.message);\n return;\n }\n throw e;\n }\n\n const viewSyncer = this.#viewSyncers.getService(clientGroupID);\n const connContextManager = viewSyncer.connContextManager;\n const group = connContextManager.getGroupState();\n\n // TODO(0xcadams): we only check for user ID mismatch here if the group is\n // already validated. This prevents wrong-user reconnects from evicting a\n // healthy connection, but it does not protect against same-user reconnects\n // with an invalid opaque token. The long-term fix is to keep the replacement\n // connection pending until its auth is fully validated, and only then replace\n // the existing socket.\n if (\n group.pinnedUser !== undefined &&\n group.pinnedUser.id !== incomingUserID\n ) {\n const error = new ProtocolError({\n kind: ErrorKind.Unauthorized,\n message:\n 'Client groups are pinned to a single userID. Connection userID does not match existing client group userID.',\n origin: ErrorOrigin.ZeroCache,\n });\n sendError(this.#lc, ws, error.errorBody);\n ws.close(3000, error.message);\n return;\n }\n\n // Check for and close existing connections AFTER auth is validated\n const existing = this.#connections.get(clientID);\n if (existing) {\n this.#lc.debug?.(\n `client ${clientID} already connected, closing existing connection`,\n );\n existing.close(`replaced by ${params.wsID}`);\n }\n\n connContextManager.registerConnection(\n {clientID, wsID: params.wsID},\n params,\n initialAuth,\n );\n\n const mutagen = this.#mutagens?.getService(clientGroupID);\n const pusher = this.#pushers?.getService(clientGroupID);\n // a new connection is using the mutagen and pusher. Bump their ref counts.\n mutagen?.ref();\n pusher?.ref();\n\n let connection: Connection;\n try {\n connection = new Connection(\n this.#lc,\n params,\n ws,\n new SyncerWsMessageHandler(\n this.#lc,\n params,\n connContextManager,\n viewSyncer,\n mutagen,\n pusher,\n ),\n () => {\n connContextManager.closeConnection({\n clientID,\n wsID: params.wsID,\n });\n if (this.#connections.get(clientID) === connection) {\n this.#connections.delete(clientID);\n }\n // Connection is closed. We can unref the mutagen and pusher.\n // If their ref counts are zero, they will stop themselves and set themselves invalid.\n mutagen?.unref();\n pusher?.unref();\n },\n );\n } catch (e) {\n connContextManager.closeConnection({clientID, wsID: params.wsID});\n mutagen?.unref();\n pusher?.unref();\n throw e;\n }\n\n this.#connections.set(clientID, connection);\n\n connection.init() && recordConnectionSuccess();\n\n if (params.initConnectionMsg) {\n this.#lc.debug?.(\n 'handling init connection message from sec header',\n params.clientGroupID,\n params.clientID,\n );\n await connection.handleInitConnection(\n JSON.stringify(params.initConnectionMsg),\n );\n }\n };\n\n run() {\n return this.#stopped.promise;\n }\n\n /**\n * Graceful shutdown involves shutting down view syncers one at a time, pausing\n * for the duration of view syncer's hydration between each one. This paces the\n * disconnects to avoid creating a backlog of hydrations in the receiving server\n * when the clients reconnect.\n */\n async drain() {\n const start = Date.now();\n this.#lc.info?.(`draining ${this.#viewSyncers.size} view-syncers`);\n\n this.#drainCoordinator.drainNextIn(0);\n\n while (this.#viewSyncers.size) {\n await this.#drainCoordinator.forceDrainTimeout;\n\n // Pick an arbitrary view syncer to force drain.\n for (const vs of this.#viewSyncers.getServices()) {\n this.#lc.debug?.(`draining view-syncer ${vs.id} (forced)`);\n // When this drain or an elective drain completes, the forceDrainTimeout will\n // resolve after the next drain interval.\n void vs.stop();\n break;\n }\n }\n this.#lc.info?.(`finished draining (${Date.now() - start} ms)`);\n }\n\n stop() {\n this.#wss.close();\n this.#stopped.resolve();\n return promiseVoid;\n }\n}\n"],"mappings":"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"}
|
|
@@ -66,15 +66,23 @@ export interface Connection {
|
|
|
66
66
|
*/
|
|
67
67
|
readonly state: Source<ConnectionState>;
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
69
|
+
* Updates the auth token and, when Zero is paused in `needs-auth` or `error`,
|
|
70
|
+
* resumes connecting.
|
|
70
71
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
72
|
+
* Calling `connect()` without `auth` preserves the current auth token.
|
|
73
|
+
* If Zero is already `connected`, it sends an auth update to the server
|
|
74
|
+
* _without_ reconnecting. In other states, the new token is used the next time
|
|
75
|
+
* Zero connects.
|
|
73
76
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* @
|
|
77
|
+
* This method does not reconnect from `disconnected` or `closed`. To switch
|
|
78
|
+
* to logged-out, create a new Zero instance with `auth` omitted.
|
|
79
|
+
*
|
|
80
|
+
* @param opts - Optional connection options.
|
|
81
|
+
* @param opts.auth - Optional new auth token to store and use for auth refreshes or
|
|
82
|
+
* the next connection.
|
|
83
|
+
* @returns A promise that resolves immediately unless Zero is paused in
|
|
84
|
+
* `needs-auth` or `error`, in which case it resolves after the next
|
|
85
|
+
* connection state change.
|
|
78
86
|
*/
|
|
79
87
|
connect(opts?: {
|
|
80
88
|
auth: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../../../zero-client/src/client/connection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAC,YAAY,EAAC,MAAM,qCAAqC,CAAC;AAGjE,OAAO,KAAK,EACV,iBAAiB,EAElB,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,eAAe,GACvB;IACE,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACD;IACE,IAAI,EAAE,WAAW,CAAC;CACnB,GACD;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,EACF;QACE,IAAI,EAAE,QAAQ,CAAC;QACf,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GACD;QACE,IAAI,EAAE,OAAO,CAAC;QACd,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GACD;QACE,IAAI,EAAE,YAAY,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACP,GACD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEN,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IAEpB;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACrD;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC;IAExC
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../../../zero-client/src/client/connection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAC,YAAY,EAAC,MAAM,qCAAqC,CAAC;AAGjE,OAAO,KAAK,EACV,iBAAiB,EAElB,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,eAAe,GACvB;IACE,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACD;IACE,IAAI,EAAE,WAAW,CAAC;CACnB,GACD;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,EACF;QACE,IAAI,EAAE,QAAQ,CAAC;QACf,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GACD;QACE,IAAI,EAAE,OAAO,CAAC;QACd,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GACD;QACE,IAAI,EAAE,YAAY,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACP,GACD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEN,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IAEpB;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACrD;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC;IAExC;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,IAAI,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,qBAAa,cAAe,YAAW,UAAU;;gBAO7C,iBAAiB,EAAE,iBAAiB,EACpC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;IAQjC,IAAI,KAAK,IAAI,MAAM,CAAC,eAAe,CAAC,CAEnC;IAEK,OAAO,CAAC,IAAI,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAwCpD;AAED,qBAAa,gBACX,SAAQ,YAAY,CAAC,eAAe,CACpC,YAAW,MAAM,CAAC,eAAe,CAAC;;gBAItB,iBAAiB,EAAE,iBAAiB;IAchD,IAAI,OAAO,IAAI,eAAe,CAE7B;CA0DF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","names":["#connectionManager","#lc","#source","#setAuth","#state","#mapConnectionManagerState"],"sources":["../../../../../zero-client/src/client/connection.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {unreachable} from '../../../shared/src/asserts.ts';\nimport {Subscribable} from '../../../shared/src/subscribable.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ClientErrorKind} from './client-error-kind.ts';\nimport type {\n ConnectionManager,\n ConnectionManagerState,\n} from './connection-manager.ts';\nimport {ConnectionStatus} from './connection-status.ts';\n\n/**\n * The current connection state of the Zero instance. One of the following states:\n *\n * - `connecting`: The client is actively trying to connect every 5 seconds.\n * - `disconnected`: The client is now in an \"offline\" state. It will continue\n * to try to connect every 5 seconds.\n * - `connected`: The client has opened a successful connection to the server.\n * - `needs-auth`: Authentication is invalid or expired. No connection retries will be made\n * until the host application calls `connect()`.\n * - `error`: A fatal error occurred. No connection retries will be made until the host\n * application calls `connect()` again.\n * - `closed`: The client was shut down (for example via `zero.close()`). This is\n * a terminal state, and a new Zero instance must be created to reconnect.\n */\nexport type ConnectionState =\n | {\n name: 'disconnected';\n reason: string;\n }\n | {\n name: 'connecting';\n reason?: string;\n }\n | {\n name: 'connected';\n }\n | {\n name: 'needs-auth';\n reason:\n | {\n type: 'mutate';\n status: 401 | 403;\n body?: string;\n }\n | {\n type: 'query';\n status: 401 | 403;\n body?: string;\n }\n | {\n type: 'zero-cache';\n reason: string;\n };\n }\n | {\n name: 'error';\n reason: string;\n }\n | {\n name: 'closed';\n reason: string;\n };\n\nexport interface Source<T> {\n /**\n * The current state value.\n */\n readonly current: T;\n\n /**\n * Subscribe to state changes.\n *\n * @param listener - Called when the state changes with the new state value.\n * @returns A function to unsubscribe from state changes.\n */\n subscribe(listener: (state: T) => void): () => void;\n}\n\n/**\n * Connection API for managing Zero's connection lifecycle.\n */\nexport interface Connection {\n /**\n * The current connection state as a subscribable value.\n */\n readonly state: Source<ConnectionState>;\n\n /**\n *
|
|
1
|
+
{"version":3,"file":"connection.js","names":["#connectionManager","#lc","#source","#setAuth","#state","#mapConnectionManagerState"],"sources":["../../../../../zero-client/src/client/connection.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {unreachable} from '../../../shared/src/asserts.ts';\nimport {Subscribable} from '../../../shared/src/subscribable.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ClientErrorKind} from './client-error-kind.ts';\nimport type {\n ConnectionManager,\n ConnectionManagerState,\n} from './connection-manager.ts';\nimport {ConnectionStatus} from './connection-status.ts';\n\n/**\n * The current connection state of the Zero instance. One of the following states:\n *\n * - `connecting`: The client is actively trying to connect every 5 seconds.\n * - `disconnected`: The client is now in an \"offline\" state. It will continue\n * to try to connect every 5 seconds.\n * - `connected`: The client has opened a successful connection to the server.\n * - `needs-auth`: Authentication is invalid or expired. No connection retries will be made\n * until the host application calls `connect()`.\n * - `error`: A fatal error occurred. No connection retries will be made until the host\n * application calls `connect()` again.\n * - `closed`: The client was shut down (for example via `zero.close()`). This is\n * a terminal state, and a new Zero instance must be created to reconnect.\n */\nexport type ConnectionState =\n | {\n name: 'disconnected';\n reason: string;\n }\n | {\n name: 'connecting';\n reason?: string;\n }\n | {\n name: 'connected';\n }\n | {\n name: 'needs-auth';\n reason:\n | {\n type: 'mutate';\n status: 401 | 403;\n body?: string;\n }\n | {\n type: 'query';\n status: 401 | 403;\n body?: string;\n }\n | {\n type: 'zero-cache';\n reason: string;\n };\n }\n | {\n name: 'error';\n reason: string;\n }\n | {\n name: 'closed';\n reason: string;\n };\n\nexport interface Source<T> {\n /**\n * The current state value.\n */\n readonly current: T;\n\n /**\n * Subscribe to state changes.\n *\n * @param listener - Called when the state changes with the new state value.\n * @returns A function to unsubscribe from state changes.\n */\n subscribe(listener: (state: T) => void): () => void;\n}\n\n/**\n * Connection API for managing Zero's connection lifecycle.\n */\nexport interface Connection {\n /**\n * The current connection state as a subscribable value.\n */\n readonly state: Source<ConnectionState>;\n\n /**\n * Updates the auth token and, when Zero is paused in `needs-auth` or `error`,\n * resumes connecting.\n *\n * Calling `connect()` without `auth` preserves the current auth token.\n * If Zero is already `connected`, it sends an auth update to the server\n * _without_ reconnecting. In other states, the new token is used the next time\n * Zero connects.\n *\n * This method does not reconnect from `disconnected` or `closed`. To switch\n * to logged-out, create a new Zero instance with `auth` omitted.\n *\n * @param opts - Optional connection options.\n * @param opts.auth - Optional new auth token to store and use for auth refreshes or\n * the next connection.\n * @returns A promise that resolves immediately unless Zero is paused in\n * `needs-auth` or `error`, in which case it resolves after the next\n * connection state change.\n */\n connect(opts?: {auth: string}): Promise<void>;\n}\n\nexport class ConnectionImpl implements Connection {\n readonly #connectionManager: ConnectionManager;\n readonly #lc: LogContext;\n readonly #source: ConnectionSource;\n readonly #setAuth: (auth: string) => void;\n\n constructor(\n connectionManager: ConnectionManager,\n lc: LogContext,\n setAuth: (auth: string) => void,\n ) {\n this.#connectionManager = connectionManager;\n this.#lc = lc;\n this.#source = new ConnectionSource(connectionManager);\n this.#setAuth = setAuth;\n }\n\n get state(): Source<ConnectionState> {\n return this.#source;\n }\n\n async connect(opts?: {auth: string}): Promise<void> {\n const lc = this.#lc.withContext('connect');\n\n if (opts && 'auth' in opts) {\n lc.debug?.('Updating auth credential from connect()');\n this.#setAuth(opts.auth);\n }\n\n // if the connection is disconnected due to a missing cacheURL, we don't allow a reconnect\n if (\n this.#connectionManager.state.name === ConnectionStatus.Disconnected &&\n this.#connectionManager.state.reason.kind ===\n ClientErrorKind.NoSocketOrigin\n ) {\n lc.error?.(\n 'connect() called but the connection is disconnected due to a missing cacheURL. No reconnect will be attempted.',\n );\n return;\n }\n\n // only allow connect() to be called from a terminal state\n if (!this.#connectionManager.isInTerminalState()) {\n lc.debug?.(\n 'connect() called but not in a terminal state. Current state:',\n this.#connectionManager.state.name,\n );\n return;\n }\n\n lc.info?.(\n `Resuming connection from state: ${this.#connectionManager.state.name}`,\n );\n\n this.#connectionManager.requestConnect();\n if (this.#connectionManager.state.name === ConnectionStatus.Connecting) {\n return;\n }\n\n await this.#connectionManager.waitForStateChange();\n }\n}\n\nexport class ConnectionSource\n extends Subscribable<ConnectionState>\n implements Source<ConnectionState>\n{\n #state: ConnectionState;\n\n constructor(connectionManager: ConnectionManager) {\n super();\n this.#state = this.#mapConnectionManagerState(connectionManager.state);\n\n // Subscribe to ConnectionManager immediately to keep #state in sync.\n // This ensures `current` always returns the correct state, even if\n // external code hasn't subscribed yet (fixes race condition where\n // connection completes before React subscribes).\n connectionManager.subscribe(state => {\n this.#state = this.#mapConnectionManagerState(state);\n this.notify(this.#state);\n });\n }\n\n get current(): ConnectionState {\n return this.#state;\n }\n\n #mapConnectionManagerState(state: ConnectionManagerState): ConnectionState {\n switch (state.name) {\n case ConnectionStatus.Closed:\n return {\n name: 'closed',\n reason: state.reason.message,\n };\n case ConnectionStatus.Connected:\n return {\n name: 'connected',\n };\n case ConnectionStatus.Connecting:\n return {\n name: 'connecting',\n ...(state.reason?.message ? {reason: state.reason.message} : {}),\n };\n case ConnectionStatus.Disconnected:\n return {\n name: 'disconnected',\n reason: state.reason.message,\n };\n case ConnectionStatus.Error:\n return {\n name: 'error',\n reason: state.reason.message,\n };\n case ConnectionStatus.NeedsAuth:\n return {\n name: 'needs-auth',\n reason:\n state.reason.errorBody.kind === ErrorKind.PushFailed\n ? {\n type: 'mutate',\n status: state.reason.errorBody.status,\n ...(state.reason.errorBody.bodyPreview\n ? {body: state.reason.errorBody.bodyPreview}\n : {}),\n }\n : state.reason.errorBody.kind === ErrorKind.TransformFailed\n ? {\n type: 'query',\n status: state.reason.errorBody.status,\n ...(state.reason.errorBody.bodyPreview\n ? {body: state.reason.errorBody.bodyPreview}\n : {}),\n }\n : {\n type: 'zero-cache',\n reason: state.reason.message,\n },\n };\n\n default:\n unreachable(state);\n }\n }\n}\n"],"mappings":";;;;;;AA8GA,IAAa,iBAAb,MAAkD;CAChD;CACA;CACA;CACA;CAEA,YACE,mBACA,IACA,SACA;AACA,QAAA,oBAA0B;AAC1B,QAAA,KAAW;AACX,QAAA,SAAe,IAAI,iBAAiB,kBAAkB;AACtD,QAAA,UAAgB;;CAGlB,IAAI,QAAiC;AACnC,SAAO,MAAA;;CAGT,MAAM,QAAQ,MAAsC;EAClD,MAAM,KAAK,MAAA,GAAS,YAAY,UAAU;AAE1C,MAAI,QAAQ,UAAU,MAAM;AAC1B,MAAG,QAAQ,0CAA0C;AACrD,SAAA,QAAc,KAAK,KAAK;;AAI1B,MACE,MAAA,kBAAwB,MAAM,SAAS,kBACvC,MAAA,kBAAwB,MAAM,OAAO,SACnC,kBACF;AACA,MAAG,QACD,iHACD;AACD;;AAIF,MAAI,CAAC,MAAA,kBAAwB,mBAAmB,EAAE;AAChD,MAAG,QACD,gEACA,MAAA,kBAAwB,MAAM,KAC/B;AACD;;AAGF,KAAG,OACD,mCAAmC,MAAA,kBAAwB,MAAM,OAClE;AAED,QAAA,kBAAwB,gBAAgB;AACxC,MAAI,MAAA,kBAAwB,MAAM,SAAS,aACzC;AAGF,QAAM,MAAA,kBAAwB,oBAAoB;;;AAItD,IAAa,mBAAb,cACU,aAEV;CACE;CAEA,YAAY,mBAAsC;AAChD,SAAO;AACP,QAAA,QAAc,MAAA,0BAAgC,kBAAkB,MAAM;AAMtE,oBAAkB,WAAU,UAAS;AACnC,SAAA,QAAc,MAAA,0BAAgC,MAAM;AACpD,QAAK,OAAO,MAAA,MAAY;IACxB;;CAGJ,IAAI,UAA2B;AAC7B,SAAO,MAAA;;CAGT,2BAA2B,OAAgD;AACzE,UAAQ,MAAM,MAAd;GACE,KAAK,OACH,QAAO;IACL,MAAM;IACN,QAAQ,MAAM,OAAO;IACtB;GACH,KAAK,UACH,QAAO,EACL,MAAM,aACP;GACH,KAAK,WACH,QAAO;IACL,MAAM;IACN,GAAI,MAAM,QAAQ,UAAU,EAAC,QAAQ,MAAM,OAAO,SAAQ,GAAG,EAAE;IAChE;GACH,KAAK,aACH,QAAO;IACL,MAAM;IACN,QAAQ,MAAM,OAAO;IACtB;GACH,KAAK,MACH,QAAO;IACL,MAAM;IACN,QAAQ,MAAM,OAAO;IACtB;GACH,KAAK,UACH,QAAO;IACL,MAAM;IACN,QACE,MAAM,OAAO,UAAU,SAAS,eAC5B;KACE,MAAM;KACN,QAAQ,MAAM,OAAO,UAAU;KAC/B,GAAI,MAAM,OAAO,UAAU,cACvB,EAAC,MAAM,MAAM,OAAO,UAAU,aAAY,GAC1C,EAAE;KACP,GACD,MAAM,OAAO,UAAU,SAAS,oBAC9B;KACE,MAAM;KACN,QAAQ,MAAM,OAAO,UAAU;KAC/B,GAAI,MAAM,OAAO,UAAU,cACvB,EAAC,MAAM,MAAM,OAAO,UAAU,aAAY,GAC1C,EAAE;KACP,GACD;KACE,MAAM;KACN,QAAQ,MAAM,OAAO;KACtB;IACV;GAEH,QACE,aAAY,MAAM"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DeleteOp, type InsertOp, type UpdateOp, type UpsertOp } from '../../../zero-protocol/src/
|
|
1
|
+
import { type DeleteOp, type InsertOp, type UpdateOp, type UpsertOp } from '../../../zero-protocol/src/mutation.ts';
|
|
2
2
|
import type { Schema } from '../../../zero-types/src/schema.ts';
|
|
3
3
|
import type { IVMSourceBranch } from './ivm-branch.ts';
|
|
4
4
|
import type { WriteTransaction } from './replicache-types.ts';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crud-impl.d.ts","sourceRoot":"","sources":["../../../../../zero-client/src/client/crud-impl.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,
|
|
1
|
+
{"version":3,"file":"crud-impl.d.ts","sourceRoot":"","sources":["../../../../../zero-client/src/client/crud-impl.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,wCAAwC,CAAC;AAEhD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,mCAAmC,CAAC;AAO9D,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,iBAAiB,CAAC;AAErD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAC,YAAY,EAAC,MAAM,iCAAiC,CAAC;AAelE,wBAAsB,MAAM,CAC1B,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,eAAe,GAAG,SAAS,GACrC,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED,wBAAsB,MAAM,CAC1B,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,QAAQ,GAAG,QAAQ,EACxB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,eAAe,GAAG,SAAS,GACrC,OAAO,CAAC,IAAI,CAAC,CAWf;AAED,wBAAsB,MAAM,CAC1B,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,eAAe,GAAG,SAAS,GACrC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,iBAAe,UAAU,CACvB,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,eAAe,GAAG,SAAS,GACrC,OAAO,CAAC,IAAI,CAAC,CAkBf;AAED,OAAO,EAAC,UAAU,IAAI,MAAM,EAAC,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { __exportAll } from "../../../_virtual/_rolldown/runtime.js";
|
|
2
2
|
import { must } from "../../../shared/src/must.js";
|
|
3
|
-
import "../../../zero-protocol/src/
|
|
3
|
+
import "../../../zero-protocol/src/mutation.js";
|
|
4
4
|
import { consume } from "../../../zql/src/ivm/stream.js";
|
|
5
5
|
import { makeSourceChangeAdd, makeSourceChangeEdit, makeSourceChangeRemove } from "../../../zql/src/ivm/source.js";
|
|
6
6
|
import { toPrimaryKeyString } from "./keys.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crud-impl.js","names":[],"sources":["../../../../../zero-client/src/client/crud-impl.ts"],"sourcesContent":["import type {ReadonlyJSONObject} from '../../../shared/src/json.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport type {Row} from '../../../zero-protocol/src/data.ts';\nimport {\n type DeleteOp,\n type InsertOp,\n type UpdateOp,\n type UpsertOp,\n} from '../../../zero-protocol/src/
|
|
1
|
+
{"version":3,"file":"crud-impl.js","names":[],"sources":["../../../../../zero-client/src/client/crud-impl.ts"],"sourcesContent":["import type {ReadonlyJSONObject} from '../../../shared/src/json.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport type {Row} from '../../../zero-protocol/src/data.ts';\nimport {\n type DeleteOp,\n type InsertOp,\n type UpdateOp,\n type UpsertOp,\n} from '../../../zero-protocol/src/mutation.ts';\nimport type {TableSchema} from '../../../zero-schema/src/table-schema.ts';\nimport type {Schema} from '../../../zero-types/src/schema.ts';\nimport {\n makeSourceChangeAdd,\n makeSourceChangeEdit,\n makeSourceChangeRemove,\n} from '../../../zql/src/ivm/source.ts';\nimport {consume} from '../../../zql/src/ivm/stream.ts';\nimport type {IVMSourceBranch} from './ivm-branch.ts';\nimport {toPrimaryKeyString} from './keys.ts';\nimport type {WriteTransaction} from './replicache-types.ts';\nexport type {TableMutator} from '../../../zql/src/mutate/crud.ts';\n\nfunction defaultOptionalFieldsToNull(\n schema: TableSchema,\n value: ReadonlyJSONObject,\n): ReadonlyJSONObject {\n let rv = value;\n for (const name in schema.columns) {\n if (rv[name] === undefined) {\n rv = {...rv, [name]: null};\n }\n }\n return rv;\n}\n\nexport async function insert(\n tx: WriteTransaction,\n arg: InsertOp,\n schema: Schema,\n ivmBranch: IVMSourceBranch | undefined,\n): Promise<void> {\n const key = toPrimaryKeyString(\n arg.tableName,\n schema.tables[arg.tableName].primaryKey,\n arg.value,\n );\n if (!(await tx.has(key))) {\n const val = defaultOptionalFieldsToNull(\n schema.tables[arg.tableName],\n arg.value,\n );\n await tx.set(key, val);\n if (ivmBranch) {\n consume(\n must(ivmBranch.getSource(arg.tableName)).push(\n makeSourceChangeAdd(arg.value),\n ),\n );\n }\n }\n}\n\nexport async function upsert(\n tx: WriteTransaction,\n arg: InsertOp | UpsertOp,\n schema: Schema,\n ivmBranch: IVMSourceBranch | undefined,\n): Promise<void> {\n const key = toPrimaryKeyString(\n arg.tableName,\n schema.tables[arg.tableName].primaryKey,\n arg.value,\n );\n if (await tx.has(key)) {\n await update(tx, {...arg, op: 'update'}, schema, ivmBranch);\n } else {\n await insert(tx, {...arg, op: 'insert'}, schema, ivmBranch);\n }\n}\n\nexport async function update(\n tx: WriteTransaction,\n arg: UpdateOp,\n schema: Schema,\n ivmBranch: IVMSourceBranch | undefined,\n): Promise<void> {\n const key = toPrimaryKeyString(\n arg.tableName,\n schema.tables[arg.tableName].primaryKey,\n arg.value,\n );\n const prev = await tx.get(key);\n if (prev === undefined) {\n return;\n }\n const update = arg.value;\n const next = {...(prev as ReadonlyJSONObject)};\n for (const k in update) {\n if (update[k] !== undefined) {\n next[k] = update[k];\n }\n }\n await tx.set(key, next);\n if (ivmBranch) {\n consume(\n must(ivmBranch.getSource(arg.tableName)).push(\n makeSourceChangeEdit(next, prev as Row),\n ),\n );\n }\n}\n\nasync function deleteImpl(\n tx: WriteTransaction,\n arg: DeleteOp,\n schema: Schema,\n ivmBranch: IVMSourceBranch | undefined,\n): Promise<void> {\n const key = toPrimaryKeyString(\n arg.tableName,\n schema.tables[arg.tableName].primaryKey,\n arg.value,\n );\n const prev = await tx.get(key);\n if (prev === undefined) {\n return;\n }\n await tx.del(key);\n if (ivmBranch) {\n consume(\n must(ivmBranch.getSource(arg.tableName)).push(\n makeSourceChangeRemove(prev as Row),\n ),\n );\n }\n}\n\nexport {deleteImpl as delete};\n"],"mappings":";;;;;;;;;;;;;AAsBA,SAAS,4BACP,QACA,OACoB;CACpB,IAAI,KAAK;AACT,MAAK,MAAM,QAAQ,OAAO,QACxB,KAAI,GAAG,UAAU,KAAA,EACf,MAAK;EAAC,GAAG;GAAK,OAAO;EAAK;AAG9B,QAAO;;AAGT,eAAsB,OACpB,IACA,KACA,QACA,WACe;CACf,MAAM,MAAM,mBACV,IAAI,WACJ,OAAO,OAAO,IAAI,WAAW,YAC7B,IAAI,MACL;AACD,KAAI,CAAE,MAAM,GAAG,IAAI,IAAI,EAAG;EACxB,MAAM,MAAM,4BACV,OAAO,OAAO,IAAI,YAClB,IAAI,MACL;AACD,QAAM,GAAG,IAAI,KAAK,IAAI;AACtB,MAAI,UACF,SACE,KAAK,UAAU,UAAU,IAAI,UAAU,CAAC,CAAC,KACvC,oBAAoB,IAAI,MAAM,CAC/B,CACF;;;AAKP,eAAsB,OACpB,IACA,KACA,QACA,WACe;CACf,MAAM,MAAM,mBACV,IAAI,WACJ,OAAO,OAAO,IAAI,WAAW,YAC7B,IAAI,MACL;AACD,KAAI,MAAM,GAAG,IAAI,IAAI,CACnB,OAAM,OAAO,IAAI;EAAC,GAAG;EAAK,IAAI;EAAS,EAAE,QAAQ,UAAU;KAE3D,OAAM,OAAO,IAAI;EAAC,GAAG;EAAK,IAAI;EAAS,EAAE,QAAQ,UAAU;;AAI/D,eAAsB,OACpB,IACA,KACA,QACA,WACe;CACf,MAAM,MAAM,mBACV,IAAI,WACJ,OAAO,OAAO,IAAI,WAAW,YAC7B,IAAI,MACL;CACD,MAAM,OAAO,MAAM,GAAG,IAAI,IAAI;AAC9B,KAAI,SAAS,KAAA,EACX;CAEF,MAAM,SAAS,IAAI;CACnB,MAAM,OAAO,EAAC,GAAI,MAA4B;AAC9C,MAAK,MAAM,KAAK,OACd,KAAI,OAAO,OAAO,KAAA,EAChB,MAAK,KAAK,OAAO;AAGrB,OAAM,GAAG,IAAI,KAAK,KAAK;AACvB,KAAI,UACF,SACE,KAAK,UAAU,UAAU,IAAI,UAAU,CAAC,CAAC,KACvC,qBAAqB,MAAM,KAAY,CACxC,CACF;;AAIL,eAAe,WACb,IACA,KACA,QACA,WACe;CACf,MAAM,MAAM,mBACV,IAAI,WACJ,OAAO,OAAO,IAAI,WAAW,YAC7B,IAAI,MACL;CACD,MAAM,OAAO,MAAM,GAAG,IAAI,IAAI;AAC9B,KAAI,SAAS,KAAA,EACX;AAEF,OAAM,GAAG,IAAI,IAAI;AACjB,KAAI,UACF,SACE,KAAK,UAAU,UAAU,IAAI,UAAU,CAAC,CAAC,KACvC,uBAAuB,KAAY,CACpC,CACF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MaybePromise } from '../../../shared/src/types.ts';
|
|
2
|
-
import { CRUD_MUTATION_NAME, type CRUDMutationArg, type CRUDOp } from '../../../zero-protocol/src/
|
|
2
|
+
import { CRUD_MUTATION_NAME, type CRUDMutationArg, type CRUDOp } from '../../../zero-protocol/src/mutation.ts';
|
|
3
3
|
import type { TableSchema } from '../../../zero-schema/src/table-schema.ts';
|
|
4
4
|
import type { Schema } from '../../../zero-types/src/schema.ts';
|
|
5
5
|
import type { CRUDExecutor, TableMutator } from '../../../zql/src/mutate/crud.ts';
|