@rocicorp/zero 1.2.0-canary.12 → 1.2.0-canary.14

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.
Files changed (122) hide show
  1. package/out/shared/src/sorted-entries.d.ts +2 -0
  2. package/out/shared/src/sorted-entries.d.ts.map +1 -0
  3. package/out/shared/src/sorted-entries.js +9 -0
  4. package/out/shared/src/sorted-entries.js.map +1 -0
  5. package/out/zero/package.js +1 -1
  6. package/out/zero/package.js.map +1 -1
  7. package/out/zero-cache/src/auth/auth.d.ts +8 -26
  8. package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
  9. package/out/zero-cache/src/auth/auth.js +57 -82
  10. package/out/zero-cache/src/auth/auth.js.map +1 -1
  11. package/out/zero-cache/src/auth/jwt.d.ts +3 -3
  12. package/out/zero-cache/src/auth/jwt.d.ts.map +1 -1
  13. package/out/zero-cache/src/auth/jwt.js.map +1 -1
  14. package/out/zero-cache/src/config/zero-config.d.ts +22 -2
  15. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  16. package/out/zero-cache/src/config/zero-config.js +21 -0
  17. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  18. package/out/zero-cache/src/custom/fetch.d.ts +2 -9
  19. package/out/zero-cache/src/custom/fetch.d.ts.map +1 -1
  20. package/out/zero-cache/src/custom/fetch.js +9 -4
  21. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  22. package/out/zero-cache/src/custom-queries/transform-query.d.ts +20 -9
  23. package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
  24. package/out/zero-cache/src/custom-queries/transform-query.js +71 -37
  25. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  26. package/out/zero-cache/src/db/transaction-pool.d.ts.map +1 -1
  27. package/out/zero-cache/src/db/transaction-pool.js +3 -0
  28. package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
  29. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  30. package/out/zero-cache/src/server/change-streamer.js +4 -1
  31. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  32. package/out/zero-cache/src/server/inspector-delegate.d.ts +2 -2
  33. package/out/zero-cache/src/server/inspector-delegate.d.ts.map +1 -1
  34. package/out/zero-cache/src/server/inspector-delegate.js +4 -4
  35. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  36. package/out/zero-cache/src/server/reaper.d.ts.map +1 -1
  37. package/out/zero-cache/src/server/reaper.js +4 -1
  38. package/out/zero-cache/src/server/reaper.js.map +1 -1
  39. package/out/zero-cache/src/server/runner/run-worker.js +1 -1
  40. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  41. package/out/zero-cache/src/server/syncer.js +34 -11
  42. package/out/zero-cache/src/server/syncer.js.map +1 -1
  43. package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
  44. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  45. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  46. package/out/zero-cache/src/services/change-source/pg/change-source.js +4 -3
  47. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  48. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +2 -1
  49. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  50. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +7 -5
  51. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  52. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +3 -3
  53. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  54. package/out/zero-cache/src/services/mutagen/pusher.d.ts +20 -20
  55. package/out/zero-cache/src/services/mutagen/pusher.d.ts.map +1 -1
  56. package/out/zero-cache/src/services/mutagen/pusher.js +91 -104
  57. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  58. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +168 -0
  59. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts.map +1 -0
  60. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +385 -0
  61. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -0
  62. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts +2 -3
  63. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts.map +1 -1
  64. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +3 -3
  65. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  66. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +20 -26
  67. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  68. package/out/zero-cache/src/services/view-syncer/view-syncer.js +203 -114
  69. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  70. package/out/zero-cache/src/types/pg-versions.d.ts +3 -0
  71. package/out/zero-cache/src/types/pg-versions.d.ts.map +1 -0
  72. package/out/zero-cache/src/types/pg-versions.js +7 -0
  73. package/out/zero-cache/src/types/pg-versions.js.map +1 -0
  74. package/out/zero-cache/src/workers/connect-params.d.ts +1 -1
  75. package/out/zero-cache/src/workers/connect-params.d.ts.map +1 -1
  76. package/out/zero-cache/src/workers/connect-params.js +1 -1
  77. package/out/zero-cache/src/workers/connect-params.js.map +1 -1
  78. package/out/zero-cache/src/workers/connection.js +4 -4
  79. package/out/zero-cache/src/workers/connection.js.map +1 -1
  80. package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts +2 -1
  81. package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
  82. package/out/zero-cache/src/workers/syncer-ws-message-handler.js +46 -36
  83. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  84. package/out/zero-cache/src/workers/syncer.d.ts +2 -1
  85. package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
  86. package/out/zero-cache/src/workers/syncer.js +53 -26
  87. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  88. package/out/zero-client/src/client/connection.d.ts +4 -4
  89. package/out/zero-client/src/client/connection.d.ts.map +1 -1
  90. package/out/zero-client/src/client/connection.js.map +1 -1
  91. package/out/zero-client/src/client/options.d.ts +34 -5
  92. package/out/zero-client/src/client/options.d.ts.map +1 -1
  93. package/out/zero-client/src/client/options.js.map +1 -1
  94. package/out/zero-client/src/client/version.js +1 -1
  95. package/out/zero-client/src/client/zero.d.ts +4 -3
  96. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  97. package/out/zero-client/src/client/zero.js +33 -11
  98. package/out/zero-client/src/client/zero.js.map +1 -1
  99. package/out/zero-protocol/src/change-desired-queries.d.ts +4 -0
  100. package/out/zero-protocol/src/change-desired-queries.d.ts.map +1 -1
  101. package/out/zero-protocol/src/change-desired-queries.js +4 -1
  102. package/out/zero-protocol/src/change-desired-queries.js.map +1 -1
  103. package/out/zero-protocol/src/connect.d.ts +4 -0
  104. package/out/zero-protocol/src/connect.d.ts.map +1 -1
  105. package/out/zero-protocol/src/connect.js +2 -1
  106. package/out/zero-protocol/src/connect.js.map +1 -1
  107. package/out/zero-protocol/src/protocol-version.d.ts +1 -1
  108. package/out/zero-protocol/src/protocol-version.d.ts.map +1 -1
  109. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  110. package/out/zero-protocol/src/push.d.ts +4 -0
  111. package/out/zero-protocol/src/push.d.ts.map +1 -1
  112. package/out/zero-protocol/src/push.js +2 -1
  113. package/out/zero-protocol/src/push.js.map +1 -1
  114. package/out/zero-protocol/src/up.d.ts +3 -0
  115. package/out/zero-protocol/src/up.d.ts.map +1 -1
  116. package/out/zero-react/src/zero-provider.d.ts.map +1 -1
  117. package/out/zero-react/src/zero-provider.js +11 -5
  118. package/out/zero-react/src/zero-provider.js.map +1 -1
  119. package/out/zero-solid/src/use-zero.d.ts.map +1 -1
  120. package/out/zero-solid/src/use-zero.js +8 -9
  121. package/out/zero-solid/src/use-zero.js.map +1 -1
  122. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"connect-params.js","names":[],"sources":["../../../../../zero-cache/src/workers/connect-params.ts"],"sourcesContent":["import type {IncomingHttpHeaders} from 'node:http2';\nimport {must} from '../../../shared/src/must.ts';\nimport {\n decodeSecProtocols,\n type InitConnectionMessage,\n} from '../../../zero-protocol/src/connect.ts';\nimport {URLParams} from '../types/url-params.ts';\n\nexport type ConnectParams = {\n readonly protocolVersion: number;\n readonly clientID: string;\n readonly clientGroupID: string;\n readonly profileID: string | null;\n readonly baseCookie: string | null;\n readonly timestamp: number;\n readonly lmID: number;\n readonly wsID: string;\n readonly debugPerf: boolean;\n readonly auth: string | undefined;\n readonly userID: string;\n readonly initConnectionMsg: InitConnectionMessage | undefined;\n readonly httpCookie: string | undefined;\n readonly origin: string | undefined;\n};\n\nexport function getConnectParams(\n protocolVersion: number,\n url: URL,\n headers: IncomingHttpHeaders,\n):\n | {\n params: ConnectParams;\n error: null;\n }\n | {\n params: null;\n error: string;\n } {\n const params = new URLParams(url);\n\n try {\n const clientID = params.get('clientID', true);\n const clientGroupID = params.get('clientGroupID', true);\n const profileID = params.get('profileID', false);\n const baseCookie = params.get('baseCookie', false);\n const timestamp = params.getInteger('ts', true);\n const lmID = params.getInteger('lmid', true);\n const wsID = params.get('wsid', false) ?? '';\n const userID = params.get('userID', false) ?? '';\n const debugPerf = params.getBoolean('debugPerf');\n const {initConnectionMessage, authToken} = decodeSecProtocols(\n must(headers['sec-websocket-protocol']),\n );\n\n return {\n params: {\n protocolVersion,\n clientID,\n clientGroupID,\n profileID,\n baseCookie,\n timestamp,\n lmID,\n wsID,\n debugPerf,\n initConnectionMsg: initConnectionMessage,\n auth: authToken,\n userID,\n httpCookie: headers.cookie,\n origin: headers.origin,\n },\n error: null,\n };\n } catch (e) {\n return {\n params: null,\n error: e instanceof Error ? e.message : String(e),\n };\n }\n}\n"],"mappings":";;;;AAyBA,SAAgB,iBACd,iBACA,KACA,SASI;CACJ,MAAM,SAAS,IAAI,UAAU,IAAI;AAEjC,KAAI;EACF,MAAM,WAAW,OAAO,IAAI,YAAY,KAAK;EAC7C,MAAM,gBAAgB,OAAO,IAAI,iBAAiB,KAAK;EACvD,MAAM,YAAY,OAAO,IAAI,aAAa,MAAM;EAChD,MAAM,aAAa,OAAO,IAAI,cAAc,MAAM;EAClD,MAAM,YAAY,OAAO,WAAW,MAAM,KAAK;EAC/C,MAAM,OAAO,OAAO,WAAW,QAAQ,KAAK;EAC5C,MAAM,OAAO,OAAO,IAAI,QAAQ,MAAM,IAAI;EAC1C,MAAM,SAAS,OAAO,IAAI,UAAU,MAAM,IAAI;EAC9C,MAAM,YAAY,OAAO,WAAW,YAAY;EAChD,MAAM,EAAC,uBAAuB,cAAa,mBACzC,KAAK,QAAQ,0BAA0B,CACxC;AAED,SAAO;GACL,QAAQ;IACN;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,mBAAmB;IACnB,MAAM;IACN;IACA,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IACjB;GACD,OAAO;GACR;UACM,GAAG;AACV,SAAO;GACL,QAAQ;GACR,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;GAClD"}
1
+ {"version":3,"file":"connect-params.js","names":[],"sources":["../../../../../zero-cache/src/workers/connect-params.ts"],"sourcesContent":["import type {IncomingHttpHeaders} from 'node:http2';\nimport {must} from '../../../shared/src/must.ts';\nimport {\n decodeSecProtocols,\n type InitConnectionMessage,\n} from '../../../zero-protocol/src/connect.ts';\nimport {URLParams} from '../types/url-params.ts';\n\nexport type ConnectParams = {\n readonly protocolVersion: number;\n readonly clientID: string;\n readonly clientGroupID: string;\n readonly profileID: string | null;\n readonly baseCookie: string | null;\n readonly timestamp: number;\n readonly lmID: number;\n readonly wsID: string;\n readonly debugPerf: boolean;\n readonly auth: string | undefined;\n readonly userID: string | undefined;\n readonly initConnectionMsg: InitConnectionMessage | undefined;\n readonly httpCookie: string | undefined;\n readonly origin: string | undefined;\n};\n\nexport function getConnectParams(\n protocolVersion: number,\n url: URL,\n headers: IncomingHttpHeaders,\n):\n | {\n params: ConnectParams;\n error: null;\n }\n | {\n params: null;\n error: string;\n } {\n const params = new URLParams(url);\n\n try {\n const clientID = params.get('clientID', true);\n const clientGroupID = params.get('clientGroupID', true);\n const profileID = params.get('profileID', false);\n const baseCookie = params.get('baseCookie', false);\n const timestamp = params.getInteger('ts', true);\n const lmID = params.getInteger('lmid', true);\n const wsID = params.get('wsid', false) ?? '';\n const userID = params.get('userID', false) ?? undefined;\n const debugPerf = params.getBoolean('debugPerf');\n const {initConnectionMessage, authToken} = decodeSecProtocols(\n must(headers['sec-websocket-protocol']),\n );\n\n return {\n params: {\n protocolVersion,\n clientID,\n clientGroupID,\n profileID,\n baseCookie,\n timestamp,\n lmID,\n wsID,\n debugPerf,\n initConnectionMsg: initConnectionMessage,\n auth: authToken,\n userID,\n httpCookie: headers.cookie,\n origin: headers.origin,\n },\n error: null,\n };\n } catch (e) {\n return {\n params: null,\n error: e instanceof Error ? e.message : String(e),\n };\n }\n}\n"],"mappings":";;;;AAyBA,SAAgB,iBACd,iBACA,KACA,SASI;CACJ,MAAM,SAAS,IAAI,UAAU,IAAI;AAEjC,KAAI;EACF,MAAM,WAAW,OAAO,IAAI,YAAY,KAAK;EAC7C,MAAM,gBAAgB,OAAO,IAAI,iBAAiB,KAAK;EACvD,MAAM,YAAY,OAAO,IAAI,aAAa,MAAM;EAChD,MAAM,aAAa,OAAO,IAAI,cAAc,MAAM;EAClD,MAAM,YAAY,OAAO,WAAW,MAAM,KAAK;EAC/C,MAAM,OAAO,OAAO,WAAW,QAAQ,KAAK;EAC5C,MAAM,OAAO,OAAO,IAAI,QAAQ,MAAM,IAAI;EAC1C,MAAM,SAAS,OAAO,IAAI,UAAU,MAAM,IAAI,KAAA;EAC9C,MAAM,YAAY,OAAO,WAAW,YAAY;EAChD,MAAM,EAAC,uBAAuB,cAAa,mBACzC,KAAK,QAAQ,0BAA0B,CACxC;AAED,SAAO;GACL,QAAQ;IACN;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,mBAAmB;IACnB,MAAM;IACN;IACA,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IACjB;GACD,OAAO;GACR;UACM,GAAG;AACV,SAAO;GACL,QAAQ;GACR,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;GAClD"}
@@ -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;
@@ -51,9 +51,9 @@ var Connection = class {
51
51
  * will only parse messages with schema(s) of supported protocol versions.
52
52
  */
53
53
  init() {
54
- if (this.#protocolVersion > 49 || this.#protocolVersion < 30) this.#closeWithError({
54
+ if (this.#protocolVersion > 50 || this.#protocolVersion < 30) this.#closeWithError({
55
55
  kind: VersionNotSupported,
56
- message: `server is at sync protocol v49 and does not support v${this.#protocolVersion}. The ${this.#protocolVersion > 49 ? "server" : "client"} must be updated to a newer release.`,
56
+ message: `server is at sync protocol v50 and does not support v${this.#protocolVersion}. The ${this.#protocolVersion > 50 ? "server" : "client"} must be updated to a newer release.`,
57
57
  origin: ZeroCache
58
58
  });
59
59
  else {
@@ -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 type {LogContext, LogLevel} from '@rocicorp/logger';\nimport {pipeline, Readable, Writable} from 'node:stream';\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 type {ErrorBody} 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';\nimport {\n isProtocolError,\n type ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.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 this.#lc.warn?.(`failed to parse message \"${data}\": ${String(e)}`);\n this.#closeWithError(\n {\n kind: ErrorKind.InvalidMessage,\n message: String(e),\n origin: ErrorOrigin.ZeroCache,\n },\n e,\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 type {LogContext, LogLevel} from '@rocicorp/logger';\nimport {pipeline, Readable, Writable} from 'node:stream';\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 type {ErrorBody} 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';\nimport {\n isProtocolError,\n type ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.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 this.#lc.warn?.(`failed to parse message \"${data}\": ${String(e)}`);\n this.#closeWithError(\n {\n kind: ErrorKind.InvalidMessage,\n message: String(e),\n origin: ErrorOrigin.ZeroCache,\n },\n e,\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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"}
@@ -2,12 +2,13 @@ import type { LogContext } from '@rocicorp/logger';
2
2
  import type { Upstream } from '../../../zero-protocol/src/up.ts';
3
3
  import type { Mutagen } from '../services/mutagen/mutagen.ts';
4
4
  import type { Pusher } from '../services/mutagen/pusher.ts';
5
+ import { type ConnectionContextManager } from '../services/view-syncer/connection-context-manager.ts';
5
6
  import { type ViewSyncer } from '../services/view-syncer/view-syncer.ts';
6
7
  import type { ConnectParams } from './connect-params.ts';
7
8
  import type { HandlerResult, MessageHandler } from './connection.ts';
8
9
  export declare class SyncerWsMessageHandler implements MessageHandler {
9
10
  #private;
10
- constructor(lc: LogContext, connectParams: ConnectParams, viewSyncer: ViewSyncer, mutagen: Mutagen | undefined, pusher: Pusher | undefined);
11
+ constructor(lc: LogContext, connectParams: ConnectParams, contextManager: ConnectionContextManager, viewSyncer: ViewSyncer, mutagen: Mutagen | undefined, pusher: Pusher | undefined);
11
12
  handleMessage(msg: Upstream): Promise<HandlerResult[]>;
12
13
  }
13
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,EAEL,KAAK,UAAU,EAChB,MAAM,wCAAwC,CAAC;AAChD,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAC,aAAa,EAAE,cAAc,EAAC,MAAM,iBAAiB,CAAC;AAInE,qBAAa,sBAAuB,YAAW,cAAc;;gBAUzD,EAAE,EAAE,UAAU,EACd,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,OAAO,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,GAAG,SAAS;IAmCtB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;CAsM7D"}
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,cAAc,EAAE,wBAAwB,EACxC,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;CAgN7D"}
@@ -3,36 +3,43 @@ import { InvalidPush } from "../../../zero-protocol/src/error-kind-enum.js";
3
3
  import { ZeroCache } from "../../../zero-protocol/src/error-origin-enum.js";
4
4
  import { startAsyncSpan, startSpan } from "../../../otel/src/span.js";
5
5
  import { version } from "../../../otel/src/version.js";
6
+ import "../services/view-syncer/connection-context-manager.js";
6
7
  import "../services/view-syncer/view-syncer.js";
7
8
  import { Lock } from "@rocicorp/lock";
8
- import { trace } from "@opentelemetry/api";
9
+ import { ROOT_CONTEXT, context, propagation, trace } from "@opentelemetry/api";
9
10
  //#region ../zero-cache/src/workers/syncer-ws-message-handler.ts
10
11
  var tracer = trace.getTracer("syncer-ws-server", version);
12
+ /**
13
+ * Wraps a function in an OTEL context extracted from a W3C traceparent header.
14
+ * This enables distributed tracing from the client through zero-cache to
15
+ * the user's API server.
16
+ */
17
+ function withTraceparent(traceparent, fn) {
18
+ if (!traceparent) return fn();
19
+ const extracted = propagation.extract(ROOT_CONTEXT, { traceparent });
20
+ return context.with(extracted, fn);
21
+ }
11
22
  var SyncerWsMessageHandler = class {
12
23
  #viewSyncer;
13
24
  #mutagen;
14
25
  #mutationLock;
15
26
  #lc;
16
27
  #clientGroupID;
17
- #syncContext;
28
+ #connectionSelector;
29
+ #contextManager;
18
30
  #pusher;
19
- constructor(lc, connectParams, viewSyncer, mutagen, pusher) {
20
- const { clientGroupID, clientID, profileID, wsID, baseCookie, protocolVersion, httpCookie, origin, userID } = connectParams;
31
+ constructor(lc, connectParams, contextManager, viewSyncer, mutagen, pusher) {
32
+ const { clientGroupID, clientID, wsID } = connectParams;
21
33
  this.#viewSyncer = viewSyncer;
22
34
  this.#mutagen = mutagen;
35
+ this.#contextManager = contextManager;
23
36
  this.#mutationLock = new Lock();
24
37
  this.#lc = lc.withContext("connection").withContext("clientID", clientID).withContext("clientGroupID", clientGroupID).withContext("wsID", wsID);
25
38
  this.#clientGroupID = clientGroupID;
26
39
  this.#pusher = pusher;
27
- this.#syncContext = {
40
+ this.#connectionSelector = {
28
41
  clientID,
29
- profileID,
30
- wsID,
31
- baseCookie,
32
- protocolVersion,
33
- httpCookie,
34
- origin,
35
- userID
42
+ wsID
36
43
  };
37
44
  }
38
45
  async handleMessage(msg) {
@@ -46,8 +53,8 @@ var SyncerWsMessageHandler = class {
46
53
  case "pull":
47
54
  lc.error?.("Pull is not supported by Zero");
48
55
  break;
49
- case "push": return startAsyncSpan(tracer, "connection.push", async () => {
50
- const { clientGroupID, mutations, auth: pushAuth } = msg[1];
56
+ case "push": return withTraceparent(msg[1].traceparent, () => startAsyncSpan(tracer, "connection.push", async () => {
57
+ const { clientGroupID, mutations } = msg[1];
51
58
  if (clientGroupID !== this.#clientGroupID) return [{
52
59
  type: "fatal",
53
60
  error: {
@@ -56,7 +63,6 @@ var SyncerWsMessageHandler = class {
56
63
  origin: ZeroCache
57
64
  }
58
65
  }];
59
- if (pushAuth) await viewSyncer.updateAuth(this.#syncContext, ["updateAuth", { auth: pushAuth }]);
60
66
  if (mutations.length === 0) return [{ type: "ok" }];
61
67
  if (mutations[0].type === "custom") {
62
68
  if (!this.#pusher) return [{
@@ -67,7 +73,7 @@ var SyncerWsMessageHandler = class {
67
73
  origin: ZeroCache
68
74
  }
69
75
  }];
70
- return [this.#pusher.enqueuePush(this.#syncContext.clientID, msg[1], viewSyncer.auth?.raw, this.#syncContext.httpCookie, this.#syncContext.origin)];
76
+ return [this.#pusher.enqueuePush(this.#connectionSelector, msg[1])];
71
77
  }
72
78
  const mutagen = this.#mutagen;
73
79
  if (!mutagen) return [{
@@ -78,7 +84,7 @@ var SyncerWsMessageHandler = class {
78
84
  origin: ZeroCache
79
85
  }
80
86
  }];
81
- const auth = viewSyncer.auth;
87
+ const auth = this.#contextManager.mustGetConnectionContext(this.#connectionSelector).auth;
82
88
  assert(auth?.type !== "opaque", "Only JWT auth is supported for CRUD mutations");
83
89
  return [await this.#mutationLock.withLock(async () => {
84
90
  const errors = [];
@@ -96,39 +102,43 @@ var SyncerWsMessageHandler = class {
96
102
  };
97
103
  return { type: "ok" };
98
104
  })];
99
- });
105
+ }));
100
106
  case "changeDesiredQueries":
101
- await startAsyncSpan(tracer, "connection.changeDesiredQueries", () => viewSyncer.changeDesiredQueries(this.#syncContext, msg));
107
+ await withTraceparent(msg[1].traceparent, () => startAsyncSpan(tracer, "connection.changeDesiredQueries", () => viewSyncer.changeDesiredQueries(this.#connectionSelector, msg)));
102
108
  break;
103
109
  case "updateAuth":
104
110
  await startAsyncSpan(tracer, "connection.updateAuth", async () => {
105
- await viewSyncer.updateAuth(this.#syncContext, msg);
111
+ const initialConnection = this.#contextManager.mustGetConnectionContext(this.#connectionSelector);
112
+ const authRevisionChanged = (await this.#contextManager.updateAuth(this.#connectionSelector, msg[1])).revision !== initialConnection.revision;
113
+ await viewSyncer.updateAuth(this.#connectionSelector, msg, authRevisionChanged);
106
114
  });
107
115
  break;
108
116
  case "deleteClients": {
109
- const deletedClientIDs = await startAsyncSpan(tracer, "connection.deleteClients", () => viewSyncer.deleteClients(this.#syncContext, msg));
110
- if (this.#pusher && deletedClientIDs.length > 0) await this.#pusher.deleteClientMutations(deletedClientIDs);
117
+ const deletedClientIDs = await startAsyncSpan(tracer, "connection.deleteClients", () => viewSyncer.deleteClients(this.#connectionSelector, msg));
118
+ if (this.#pusher && deletedClientIDs.length > 0) await this.#pusher.deleteClientMutations(this.#connectionSelector, deletedClientIDs);
111
119
  break;
112
120
  }
113
- case "initConnection": {
114
- const ret = [{
115
- type: "stream",
116
- source: "viewSyncer",
117
- stream: startSpan(tracer, "connection.initConnection", () => viewSyncer.initConnection(this.#syncContext, msg))
118
- }];
119
- if (this.#pusher) ret.push({
120
- type: "stream",
121
- source: "pusher",
122
- stream: this.#pusher.initConnection(this.#syncContext.clientID, this.#syncContext.wsID, msg[1].userPushURL, msg[1].userPushHeaders, () => viewSyncer.clearAuth())
121
+ case "initConnection":
122
+ this.#contextManager.initConnection(this.#connectionSelector, msg[1]);
123
+ return withTraceparent(msg[1].traceparent, () => {
124
+ const ret = [{
125
+ type: "stream",
126
+ source: "viewSyncer",
127
+ stream: startSpan(tracer, "connection.initConnection", () => viewSyncer.initConnection(this.#connectionSelector, msg))
128
+ }];
129
+ if (this.#pusher) ret.push({
130
+ type: "stream",
131
+ source: "pusher",
132
+ stream: this.#pusher.initConnection(this.#connectionSelector)
133
+ });
134
+ return ret;
123
135
  });
124
- return ret;
125
- }
126
136
  case "closeConnection": break;
127
137
  case "inspect":
128
- await startAsyncSpan(tracer, "connection.inspect", () => viewSyncer.inspect(this.#syncContext, msg));
138
+ await startAsyncSpan(tracer, "connection.inspect", () => viewSyncer.inspect(this.#connectionSelector, msg));
129
139
  break;
130
140
  case "ackMutationResponses":
131
- if (this.#pusher) await this.#pusher.ackMutationResponses(msg[1]);
141
+ if (this.#pusher) await this.#pusher.ackMutationResponses(this.#connectionSelector, msg[1]);
132
142
  break;
133
143
  default: unreachable(msgType);
134
144
  }
@@ -1 +1 @@
1
- {"version":3,"file":"syncer-ws-message-handler.js","names":["#viewSyncer","#mutagen","#mutationLock","#lc","#clientGroupID","#syncContext","#pusher"],"sources":["../../../../../zero-cache/src/workers/syncer-ws-message-handler.ts"],"sourcesContent":["import {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 SyncContext,\n type ViewSyncer,\n} 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\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 #syncContext: SyncContext;\n readonly #pusher: Pusher | undefined;\n\n constructor(\n lc: LogContext,\n connectParams: ConnectParams,\n viewSyncer: ViewSyncer,\n mutagen: Mutagen | undefined,\n pusher: Pusher | undefined,\n ) {\n const {\n clientGroupID,\n clientID,\n profileID,\n wsID,\n baseCookie,\n protocolVersion,\n httpCookie,\n origin,\n userID,\n } = connectParams;\n this.#viewSyncer = viewSyncer;\n this.#mutagen = mutagen;\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.#syncContext = {\n clientID,\n profileID,\n wsID,\n baseCookie,\n protocolVersion,\n httpCookie,\n origin,\n userID,\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 startAsyncSpan<HandlerResult[]>(\n tracer,\n 'connection.push',\n async () => {\n const {clientGroupID, mutations, auth: pushAuth} = 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 // for backwards compatibility, if the push contains auth, we update it\n if (pushAuth) {\n await viewSyncer.updateAuth(this.#syncContext, [\n 'updateAuth',\n {auth: pushAuth},\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(\n this.#syncContext.clientID,\n msg[1],\n viewSyncer.auth?.raw,\n this.#syncContext.httpCookie,\n this.#syncContext.origin,\n ),\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 = viewSyncer.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 case 'changeDesiredQueries':\n await startAsyncSpan(tracer, 'connection.changeDesiredQueries', () =>\n viewSyncer.changeDesiredQueries(this.#syncContext, msg),\n );\n break;\n case 'updateAuth':\n await startAsyncSpan(tracer, 'connection.updateAuth', async () => {\n await viewSyncer.updateAuth(this.#syncContext, msg);\n });\n break;\n case 'deleteClients': {\n const deletedClientIDs = await startAsyncSpan(\n tracer,\n 'connection.deleteClients',\n () => viewSyncer.deleteClients(this.#syncContext, msg),\n );\n if (this.#pusher && deletedClientIDs.length > 0) {\n await this.#pusher.deleteClientMutations(deletedClientIDs);\n }\n break;\n }\n case 'initConnection': {\n const ret: HandlerResult[] = [\n {\n type: 'stream',\n source: 'viewSyncer',\n stream: startSpan(tracer, 'connection.initConnection', () =>\n viewSyncer.initConnection(this.#syncContext, 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(\n this.#syncContext.clientID,\n this.#syncContext.wsID,\n msg[1].userPushURL,\n msg[1].userPushHeaders,\n () => viewSyncer.clearAuth(),\n ),\n });\n }\n\n return ret;\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.#syncContext, msg),\n );\n break;\n\n case 'ackMutationResponses':\n if (this.#pusher) {\n await this.#pusher.ackMutationResponses(msg[1]);\n }\n break;\n\n default:\n unreachable(msgType);\n }\n\n return [{type: 'ok'}];\n }\n}\n"],"mappings":";;;;;;;;;AAmBA,IAAM,SAAS,MAAM,UAAU,oBAAoB,QAAQ;AAE3D,IAAa,yBAAb,MAA8D;CAC5D;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YACE,IACA,eACA,YACA,SACA,QACA;EACA,MAAM,EACJ,eACA,UACA,WACA,MACA,YACA,iBACA,YACA,QACA,WACE;AACJ,QAAA,aAAmB;AACnB,QAAA,UAAgB;AAChB,QAAA,eAAqB,IAAI,MAAM;AAC/B,QAAA,KAAW,GACR,YAAY,aAAa,CACzB,YAAY,YAAY,SAAS,CACjC,YAAY,iBAAiB,cAAc,CAC3C,YAAY,QAAQ,KAAK;AAC5B,QAAA,gBAAsB;AACtB,QAAA,SAAe;AACf,QAAA,cAAoB;GAClB;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACD;;CAGH,MAAM,cAAc,KAAyC;EAC3D,MAAM,KAAK,MAAA;EACX,MAAM,UAAU,IAAI;EACpB,MAAM,aAAa,MAAA;AACnB,UAAQ,SAAR;GACE,KAAK;AACH,OAAG,QAAQ,8CAA8C;AACzD;GACF,KAAK;AACH,OAAG,QAAQ,gCAAgC;AAC3C;GACF,KAAK,OACH,QAAO,eACL,QACA,mBACA,YAAY;IACV,MAAM,EAAC,eAAe,WAAW,MAAM,aAAY,IAAI;AACvD,QAAI,kBAAkB,MAAA,cACpB,QAAO,CACL;KACE,MAAM;KACN,OAAO;MACL,MAAM;MACN,SACE,8BAA8B,cAAc,gDACZ,MAAA;MAClC,QAAQ;MACT;KACF,CACF;AAIH,QAAI,SACF,OAAM,WAAW,WAAW,MAAA,aAAmB,CAC7C,cACA,EAAC,MAAM,UAAS,CACjB,CAAC;AAGJ,QAAI,UAAU,WAAW,EACvB,QAAO,CACL,EACE,MAAM,MACP,CACF;AAMH,QAAI,UAAU,GAAG,SAAS,UAAU;AAClC,SAAI,CAAC,MAAA,OACH,QAAO,CACL;MACE,MAAM;MACN,OAAO;OACL,MAAM;OACN,SACE;OACF,QAAQ;OACT;MACF,CACF;AAEH,YAAO,CACL,MAAA,OAAa,YACX,MAAA,YAAkB,UAClB,IAAI,IACJ,WAAW,MAAM,KACjB,MAAA,YAAkB,YAClB,MAAA,YAAkB,OACnB,CACF;;IAGH,MAAM,UAAU,MAAA;AAChB,QAAI,CAAC,QACH,QAAO,CACL;KACE,MAAM;KACN,OAAO;MACL,MAAM;MACN,SAAS;MACT,QAAQ;MACT;KACF,CACF;IAGH,MAAM,OAAO,WAAW;AACxB,WACE,MAAM,SAAS,UACf,gDACD;AA0BD,WAAO,CArBK,MAAM,MAAA,aAAmB,SAAS,YAAY;KACxD,MAAM,SAAsB,EAAE;AAC9B,UAAK,MAAM,YAAY,WAAW;MAChC,MAAM,aAAa,MAAM,QAAQ,gBAC/B,UACA,MAAM,SACN,MAAA,WAAiB,KAAA,EAClB;AACD,UAAI,eAAe,KAAA,EACjB,QAAO,KAAK;OACV,MAAM,WAAW;OACjB,SAAS,WAAW;OACpB,QAAQ;OACT,CAAC;;AAGN,SAAI,OAAO,SAAS,EAClB,QAAO;MAAC,MAAM;MAAa;MAAO;AAEpC,YAAO,EAAC,MAAM,MAAK;MACnB,CACU;KAEf;GAEH,KAAK;AACH,UAAM,eAAe,QAAQ,yCAC3B,WAAW,qBAAqB,MAAA,aAAmB,IAAI,CACxD;AACD;GACF,KAAK;AACH,UAAM,eAAe,QAAQ,yBAAyB,YAAY;AAChE,WAAM,WAAW,WAAW,MAAA,aAAmB,IAAI;MACnD;AACF;GACF,KAAK,iBAAiB;IACpB,MAAM,mBAAmB,MAAM,eAC7B,QACA,kCACM,WAAW,cAAc,MAAA,aAAmB,IAAI,CACvD;AACD,QAAI,MAAA,UAAgB,iBAAiB,SAAS,EAC5C,OAAM,MAAA,OAAa,sBAAsB,iBAAiB;AAE5D;;GAEF,KAAK,kBAAkB;IACrB,MAAM,MAAuB,CAC3B;KACE,MAAM;KACN,QAAQ;KACR,QAAQ,UAAU,QAAQ,mCACxB,WAAW,eAAe,MAAA,aAAmB,IAAI,CAClD;KACF,CACF;AAMD,QAAI,MAAA,OACF,KAAI,KAAK;KACP,MAAM;KACN,QAAQ;KACR,QAAQ,MAAA,OAAa,eACnB,MAAA,YAAkB,UAClB,MAAA,YAAkB,MAClB,IAAI,GAAG,aACP,IAAI,GAAG,uBACD,WAAW,WAAW,CAC7B;KACF,CAAC;AAGJ,WAAO;;GAET,KAAK,kBAEH;GAEF,KAAK;AACH,UAAM,eAAe,QAAQ,4BAC3B,WAAW,QAAQ,MAAA,aAAmB,IAAI,CAC3C;AACD;GAEF,KAAK;AACH,QAAI,MAAA,OACF,OAAM,MAAA,OAAa,qBAAqB,IAAI,GAAG;AAEjD;GAEF,QACE,aAAY,QAAQ;;AAGxB,SAAO,CAAC,EAAC,MAAM,MAAK,CAAC"}
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":";;;;;;;;;;AAoBA,IAAM,SAAS,MAAM,UAAU,oBAAoB,QAAQ;;;;;;AAO3D,SAAS,gBAAmB,aAAiC,IAAgB;AAC3E,KAAI,CAAC,YACH,QAAO,IAAI;CAEb,MAAM,YAAY,YAAY,QAAQ,cAAc,EAAC,aAAY,CAAC;AAClE,QAAO,QAAQ,KAAK,WAAW,GAAG;;AAGpC,IAAa,yBAAb,MAA8D;CAC5D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YACE,IACA,eACA,gBACA,YACA,SACA,QACA;EACA,MAAM,EAAC,eAAe,UAAU,SAAQ;AACxC,QAAA,aAAmB;AACnB,QAAA,UAAgB;AAChB,QAAA,iBAAuB;AACvB,QAAA,eAAqB,IAAI,MAAM;AAC/B,QAAA,KAAW,GACR,YAAY,aAAa,CACzB,YAAY,YAAY,SAAS,CACjC,YAAY,iBAAiB,cAAc,CAC3C,YAAY,QAAQ,KAAK;AAC5B,QAAA,gBAAsB;AACtB,QAAA,SAAe;AACf,QAAA,qBAA2B;GACzB;GACA;GACD;;CAGH,MAAM,cAAc,KAAyC;EAC3D,MAAM,KAAK,MAAA;EACX,MAAM,UAAU,IAAI;EACpB,MAAM,aAAa,MAAA;AACnB,UAAQ,SAAR;GACE,KAAK;AACH,OAAG,QAAQ,8CAA8C;AACzD;GACF,KAAK;AACH,OAAG,QAAQ,gCAAgC;AAC3C;GACF,KAAK,OACH,QAAO,gBAAgB,IAAI,GAAG,mBAC5B,eACE,QACA,mBACA,YAAY;IACV,MAAM,EAAC,eAAe,cAAa,IAAI;AACvC,QAAI,kBAAkB,MAAA,cACpB,QAAO,CACL;KACE,MAAM;KACN,OAAO;MACL,MAAM;MACN,SACE,8BAA8B,cAAc,gDACZ,MAAA;MAClC,QAAQ;MACT;KACF,CACF;AAGH,QAAI,UAAU,WAAW,EACvB,QAAO,CACL,EACE,MAAM,MACP,CACF;AAMH,QAAI,UAAU,GAAG,SAAS,UAAU;AAClC,SAAI,CAAC,MAAA,OACH,QAAO,CACL;MACE,MAAM;MACN,OAAO;OACL,MAAM;OACN,SACE;OACF,QAAQ;OACT;MACF,CACF;AAEH,YAAO,CACL,MAAA,OAAa,YAAY,MAAA,oBAA0B,IAAI,GAAG,CAC3D;;IAGH,MAAM,UAAU,MAAA;AAChB,QAAI,CAAC,QACH,QAAO,CACL;KACE,MAAM;KACN,OAAO;MACL,MAAM;MACN,SAAS;MACT,QAAQ;MACT;KACF,CACF;IAGH,MAAM,OAAO,MAAA,eAAqB,yBAChC,MAAA,mBACD,CAAC;AACF,WACE,MAAM,SAAS,UACf,gDACD;AA0BD,WAAO,CArBK,MAAM,MAAA,aAAmB,SAAS,YAAY;KACxD,MAAM,SAAsB,EAAE;AAC9B,UAAK,MAAM,YAAY,WAAW;MAChC,MAAM,aAAa,MAAM,QAAQ,gBAC/B,UACA,MAAM,SACN,MAAA,WAAiB,KAAA,EAClB;AACD,UAAI,eAAe,KAAA,EACjB,QAAO,KAAK;OACV,MAAM,WAAW;OACjB,SAAS,WAAW;OACpB,QAAQ;OACT,CAAC;;AAGN,SAAI,OAAO,SAAS,EAClB,QAAO;MAAC,MAAM;MAAa;MAAO;AAEpC,YAAO,EAAC,MAAM,MAAK;MACnB,CACU;KAEf,CACF;GAEH,KAAK;AACH,UAAM,gBAAgB,IAAI,GAAG,mBAC3B,eAAe,QAAQ,yCACrB,WAAW,qBAAqB,MAAA,oBAA0B,IAAI,CAC/D,CACF;AACD;GACF,KAAK;AACH,UAAM,eAAe,QAAQ,yBAAyB,YAAY;KAChE,MAAM,oBACJ,MAAA,eAAqB,yBACnB,MAAA,mBACD;KAKH,MAAM,uBAJoB,MAAM,MAAA,eAAqB,WACnD,MAAA,oBACA,IAAI,GACL,EAEmB,aAAa,kBAAkB;AAEnD,WAAM,WAAW,WACf,MAAA,oBACA,KACA,oBACD;MACD;AACF;GACF,KAAK,iBAAiB;IACpB,MAAM,mBAAmB,MAAM,eAC7B,QACA,kCACM,WAAW,cAAc,MAAA,oBAA0B,IAAI,CAC9D;AACD,QAAI,MAAA,UAAgB,iBAAiB,SAAS,EAC5C,OAAM,MAAA,OAAa,sBACjB,MAAA,oBACA,iBACD;AAEH;;GAEF,KAAK;AACH,UAAA,eAAqB,eAAe,MAAA,oBAA0B,IAAI,GAAG;AACrE,WAAO,gBAAgB,IAAI,GAAG,mBAAmB;KAC/C,MAAM,MAAuB,CAC3B;MACE,MAAM;MACN,QAAQ;MACR,QAAQ,UAAU,QAAQ,mCACxB,WAAW,eAAe,MAAA,oBAA0B,IAAI,CACzD;MACF,CACF;AAMD,SAAI,MAAA,OACF,KAAI,KAAK;MACP,MAAM;MACN,QAAQ;MACR,QAAQ,MAAA,OAAa,eAAe,MAAA,mBAAyB;MAC9D,CAAC;AAGJ,YAAO;MACP;GAEJ,KAAK,kBAEH;GAEF,KAAK;AACH,UAAM,eAAe,QAAQ,4BAC3B,WAAW,QAAQ,MAAA,oBAA0B,IAAI,CAClD;AACD;GAEF,KAAK;AACH,QAAI,MAAA,OACF,OAAM,MAAA,OAAa,qBACjB,MAAA,oBACA,IAAI,GACL;AAEH;GAEF,QACE,aAAY,QAAQ;;AAGxB,SAAO,CAAC,EAAC,MAAM,MAAK,CAAC"}
@@ -6,6 +6,7 @@ import type { Mutagen } from '../services/mutagen/mutagen.ts';
6
6
  import type { Pusher } from '../services/mutagen/pusher.ts';
7
7
  import type { ReplicaState } from '../services/replicator/replicator.ts';
8
8
  import type { ActivityBasedService, Service, SingletonService } from '../services/service.ts';
9
+ import type { ConnectionContextManager } from '../services/view-syncer/connection-context-manager.ts';
9
10
  import { DrainCoordinator } from '../services/view-syncer/drain-coordinator.ts';
10
11
  import type { ViewSyncer } from '../services/view-syncer/view-syncer.ts';
11
12
  import type { Worker } from '../types/processes.ts';
@@ -23,7 +24,7 @@ export type SyncerWorkerData = {
23
24
  export declare class Syncer implements SingletonService {
24
25
  #private;
25
26
  readonly id: string;
26
- constructor(lc: LogContext, config: ZeroConfig, viewSyncerFactory: (id: string, sub: Subscription<ReplicaState>, drainCoordinator: DrainCoordinator, validateLegacyJWT: ValidateLegacyJWT | undefined) => ViewSyncer & ActivityBasedService, mutagenFactory: ((id: string) => Mutagen & Service) | undefined, pusherFactory: ((id: string) => Pusher & Service) | undefined, parent: Worker);
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, contextManager: ConnectionContextManager) => Pusher & Service) | undefined, parent: Worker, validateLegacyJWT: ValidateLegacyJWT | undefined);
27
28
  run(): Promise<void>;
28
29
  /**
29
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":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAGjD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAAC,KAAK,iBAAiB,EAAC,MAAM,iBAAiB,CAAC;AAEvD,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,0BAA0B,CAAC;AAMzD,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,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;gBAa5B,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,EAClC,iBAAiB,EAAE,iBAAiB,GAAG,SAAS,KAC7C,UAAU,GAAG,oBAAoB,EACtC,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,GAAG,SAAS,EAC/D,aAAa,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,GAAG,SAAS,EAC7D,MAAM,EAAE,MAAM;IA8IhB,GAAG;IAIH;;;;;OAKG;IACG,KAAK;IAqBX,IAAI;CA4BL"}
1
+ {"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/workers/syncer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAGjD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,qBAAqB,CAAC;AASrD,OAAO,EAAyB,KAAK,iBAAiB,EAAC,MAAM,iBAAiB,CAAC;AAE/E,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,0BAA0B,CAAC;AAMzD,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,cAAc,EAAE,wBAAwB,KACrC,MAAM,GAAG,OAAO,CAAC,GACtB,SAAS,EACb,MAAM,EAAE,MAAM,EACd,iBAAiB,EAAE,iBAAiB,GAAG,SAAS;IAwMlD,GAAG;IAIH;;;;;OAKG;IACG,KAAK;IAqBX,IAAI;CAKL"}
@@ -1,10 +1,13 @@
1
1
  import { promiseVoid } from "../../../shared/src/resolved-promises.js";
2
+ import { Unauthorized } from "../../../zero-protocol/src/error-kind-enum.js";
3
+ import { ZeroCache } from "../../../zero-protocol/src/error-origin-enum.js";
4
+ import { ProtocolError, isProtocolError } from "../../../zero-protocol/src/error.js";
2
5
  import "../config/zero-config.js";
3
6
  import { installWebSocketReceiver } from "../types/websocket-handoff.js";
4
7
  import { createNotifierFrom, subscribeTo } from "./replicator.js";
5
8
  import { recordConnectionAttempted, recordConnectionSuccess, setActiveClientGroupsGetter } from "../server/anonymous-otel-start.js";
6
- import "../auth/auth.js";
7
- import { tokenConfigOptions, verifyToken } from "../auth/jwt.js";
9
+ import { resolveAuth } from "../auth/auth.js";
10
+ import { tokenConfigOptions } from "../auth/jwt.js";
8
11
  import { ServiceRunner } from "../services/runner.js";
9
12
  import { DrainCoordinator } from "../services/view-syncer/drain-coordinator.js";
10
13
  import { Connection, sendError } from "./connection.js";
@@ -47,14 +50,16 @@ var Syncer = class {
47
50
  #wss;
48
51
  #stopped = resolver();
49
52
  #config;
50
- constructor(lc, config, viewSyncerFactory, mutagenFactory, pusherFactory, parent) {
53
+ #validateLegacyJWT;
54
+ constructor(lc, config, viewSyncerFactory, mutagenFactory, pusherFactory, parent, validateLegacyJWT) {
51
55
  this.#config = config;
56
+ this.#validateLegacyJWT = validateLegacyJWT;
52
57
  const notifier = createNotifierFrom(lc, parent);
53
58
  subscribeTo(lc, parent);
54
59
  this.#lc = lc;
55
- this.#viewSyncers = new ServiceRunner(lc, (id) => viewSyncerFactory(id, notifier.subscribe(), this.#drainCoordinator, this.#validateLegacyJWT()), (v) => v.keepalive());
60
+ this.#viewSyncers = new ServiceRunner(lc, (id) => viewSyncerFactory(id, notifier.subscribe(), this.#drainCoordinator), (v) => v.keepalive());
56
61
  if (mutagenFactory) this.#mutagens = new ServiceRunner(lc, mutagenFactory, (m) => m.hasRefs());
57
- if (pusherFactory) this.#pushers = new ServiceRunner(lc, pusherFactory, (p) => p.hasRefs());
62
+ if (pusherFactory) this.#pushers = new ServiceRunner(lc, (id) => pusherFactory(id, this.#viewSyncers.getService(id).contextManager), (p) => p.hasRefs());
58
63
  this.#parent = parent;
59
64
  this.#wss = new WebSocketServer(getWebSocketServerOptions(config));
60
65
  installWebSocketReceiver(lc, this.#wss, this.#createConnection, this.#parent);
@@ -64,17 +69,42 @@ var Syncer = class {
64
69
  this.#lc.debug?.("creating connection", params.clientGroupID, params.clientID);
65
70
  recordConnectionAttempted();
66
71
  const { clientID, clientGroupID, auth, userID } = params;
67
- if (auth !== void 0 && auth !== "") {
72
+ const hasProvidedAuth = auth !== void 0 && auth !== "";
73
+ if (hasProvidedAuth) {
68
74
  const tokenOptions = tokenConfigOptions(this.#config.auth ?? {});
69
75
  const hasPushOrMutate = this.#config?.push?.url !== void 0 || this.#config?.mutate?.url !== void 0;
70
76
  const hasQueries = this.#config?.query?.url !== void 0 || this.#config?.getQueries?.url !== void 0;
71
77
  if (!(tokenOptions.length === 1) && !(hasPushOrMutate && hasQueries)) throw new Error("Exactly one of jwk, secret, or jwksUrl must be set in order to verify tokens but actually the following were set: " + JSON.stringify(tokenOptions) + ". You may also set both ZERO_MUTATE_URL and ZERO_QUERY_URL to enable custom mutations and queries without passing token verification options.");
72
78
  }
79
+ let initialAuth;
80
+ try {
81
+ initialAuth = await resolveAuth(this.#lc.withContext("clientGroupID", clientGroupID).withContext("clientID", clientID), void 0, userID, auth, this.#validateLegacyJWT);
82
+ } catch (e) {
83
+ if (isProtocolError(e)) {
84
+ this.#lc.warn?.("Rejecting sync connection during initial auth resolution", {
85
+ clientGroupID,
86
+ clientID,
87
+ userID,
88
+ hasProvidedAuth,
89
+ errorKind: e.message
90
+ });
91
+ sendError(this.#lc, ws, e.errorBody);
92
+ ws.close(3e3, e.errorBody.message);
93
+ return;
94
+ }
95
+ throw e;
96
+ }
73
97
  const viewSyncer = this.#viewSyncers.getService(clientGroupID);
74
- const authResult = await viewSyncer.initAuthSession(userID, auth);
75
- if (!authResult.ok) {
76
- sendError(this.#lc, ws, authResult.error);
77
- ws.close(3e3, authResult.error.message);
98
+ const contextManager = viewSyncer.contextManager;
99
+ const group = contextManager.getGroupState();
100
+ if (group.validated && group.userID !== userID) {
101
+ const error = new ProtocolError({
102
+ kind: Unauthorized,
103
+ message: "Client groups are pinned to a single userID. Connection userID does not match existing client group userID.",
104
+ origin: ZeroCache
105
+ });
106
+ sendError(this.#lc, ws, error.errorBody);
107
+ ws.close(3e3, error.message);
78
108
  return;
79
109
  }
80
110
  const existing = this.#connections.get(clientID);
@@ -82,18 +112,30 @@ var Syncer = class {
82
112
  this.#lc.debug?.(`client ${clientID} already connected, closing existing connection`);
83
113
  existing.close(`replaced by ${params.wsID}`);
84
114
  }
115
+ contextManager.registerConnection({
116
+ clientID,
117
+ wsID: params.wsID
118
+ }, params, initialAuth);
85
119
  const mutagen = this.#mutagens?.getService(clientGroupID);
86
120
  const pusher = this.#pushers?.getService(clientGroupID);
87
121
  mutagen?.ref();
88
122
  pusher?.ref();
89
123
  let connection;
90
124
  try {
91
- connection = new Connection(this.#lc, params, ws, new SyncerWsMessageHandler(this.#lc, params, viewSyncer, mutagen, pusher), () => {
125
+ connection = new Connection(this.#lc, params, ws, new SyncerWsMessageHandler(this.#lc, params, contextManager, viewSyncer, mutagen, pusher), () => {
126
+ contextManager.closeConnection({
127
+ clientID,
128
+ wsID: params.wsID
129
+ });
92
130
  if (this.#connections.get(clientID) === connection) this.#connections.delete(clientID);
93
131
  mutagen?.unref();
94
132
  pusher?.unref();
95
133
  });
96
134
  } catch (e) {
135
+ contextManager.closeConnection({
136
+ clientID,
137
+ wsID: params.wsID
138
+ });
97
139
  mutagen?.unref();
98
140
  pusher?.unref();
99
141
  throw e;
@@ -133,21 +175,6 @@ var Syncer = class {
133
175
  this.#stopped.resolve();
134
176
  return promiseVoid;
135
177
  }
136
- /** @deprecated used in JWT validation */
137
- #validateLegacyJWT() {
138
- if (tokenConfigOptions(this.#config.auth ?? {}).length !== 1) return;
139
- return async (token, { userID }) => {
140
- return {
141
- type: "jwt",
142
- raw: token,
143
- decoded: await verifyToken(this.#config.auth, token, {
144
- subject: userID,
145
- ...this.#config.auth?.issuer && { issuer: this.#config.auth.issuer },
146
- ...this.#config.auth?.audience && { audience: this.#config.auth.audience }
147
- })
148
- };
149
- };
150
- }
151
178
  };
152
179
  //#endregion
153
180
  export { Syncer };