@rocicorp/zero 1.4.0-canary.5 → 1.5.0-canary.0

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 (200) hide show
  1. package/out/analyze-query/src/analyze-cli.js +2 -2
  2. package/out/analyze-query/src/analyze-cli.js.map +1 -1
  3. package/out/zero/package.js +1 -1
  4. package/out/zero/package.js.map +1 -1
  5. package/out/zero-cache/src/auth/auth.d.ts +1 -1
  6. package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
  7. package/out/zero-cache/src/auth/auth.js +1 -1
  8. package/out/zero-cache/src/auth/auth.js.map +1 -1
  9. package/out/zero-cache/src/auth/write-authorizer.d.ts +1 -1
  10. package/out/zero-cache/src/auth/write-authorizer.d.ts.map +1 -1
  11. package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
  12. package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
  13. package/out/zero-cache/src/config/normalize.js +8 -0
  14. package/out/zero-cache/src/config/normalize.js.map +1 -1
  15. package/out/zero-cache/src/config/zero-config.d.ts +8 -4
  16. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  17. package/out/zero-cache/src/config/zero-config.js +28 -6
  18. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  19. package/out/zero-cache/src/custom/fetch.d.ts +1 -1
  20. package/out/zero-cache/src/custom/fetch.d.ts.map +1 -1
  21. package/out/zero-cache/src/custom/fetch.js +2 -2
  22. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  23. package/out/zero-cache/src/custom-queries/transform-query.d.ts +21 -7
  24. package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
  25. package/out/zero-cache/src/custom-queries/transform-query.js +26 -9
  26. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  27. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  28. package/out/zero-cache/src/server/change-streamer.js +2 -1
  29. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  30. package/out/zero-cache/src/server/runner/run-worker.d.ts.map +1 -1
  31. package/out/zero-cache/src/server/runner/run-worker.js +5 -2
  32. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  33. package/out/zero-cache/src/server/syncer.js +3 -3
  34. package/out/zero-cache/src/server/syncer.js.map +1 -1
  35. package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
  36. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  37. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  38. package/out/zero-cache/src/services/change-source/pg/change-source.js +24 -20
  39. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  40. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +258 -45
  41. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
  42. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +119 -83
  43. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  44. package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
  45. package/out/zero-cache/src/services/change-source/pg/schema/init.js +1 -1
  46. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  47. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
  48. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +11 -2
  49. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  50. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +1 -0
  51. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.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/change-streamer/storer.d.ts.map +1 -1
  55. package/out/zero-cache/src/services/change-streamer/storer.js +3 -3
  56. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  57. package/out/zero-cache/src/services/http-service.d.ts +1 -0
  58. package/out/zero-cache/src/services/http-service.d.ts.map +1 -1
  59. package/out/zero-cache/src/services/http-service.js +5 -4
  60. package/out/zero-cache/src/services/http-service.js.map +1 -1
  61. package/out/zero-cache/src/services/life-cycle.d.ts +1 -1
  62. package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
  63. package/out/zero-cache/src/services/life-cycle.js +1 -2
  64. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  65. package/out/zero-cache/src/services/mutagen/mutagen.d.ts +1 -1
  66. package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
  67. package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
  68. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  69. package/out/zero-cache/src/services/mutagen/pusher.d.ts +4 -3
  70. package/out/zero-cache/src/services/mutagen/pusher.d.ts.map +1 -1
  71. package/out/zero-cache/src/services/mutagen/pusher.js +57 -38
  72. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  73. package/out/zero-cache/src/services/replicator/schema/change-log.d.ts.map +1 -1
  74. package/out/zero-cache/src/services/replicator/schema/change-log.js.map +1 -1
  75. package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js +2 -1
  76. package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
  77. package/out/zero-cache/src/services/view-syncer/client-handler.js +1 -1
  78. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  79. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +39 -27
  80. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts.map +1 -1
  81. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +138 -102
  82. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  83. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
  84. package/out/zero-cache/src/services/view-syncer/cvr-store.js +22 -2
  85. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  86. package/out/zero-cache/src/services/view-syncer/cvr.d.ts +6 -0
  87. package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
  88. package/out/zero-cache/src/services/view-syncer/cvr.js +8 -0
  89. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  90. package/out/zero-cache/src/services/view-syncer/row-record-cache.d.ts.map +1 -1
  91. package/out/zero-cache/src/services/view-syncer/row-record-cache.js +27 -3
  92. package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
  93. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +3 -3
  94. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  95. package/out/zero-cache/src/services/view-syncer/view-syncer.js +115 -86
  96. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  97. package/out/zero-cache/src/workers/connection.js +2 -2
  98. package/out/zero-cache/src/workers/connection.js.map +1 -1
  99. package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts +1 -1
  100. package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
  101. package/out/zero-cache/src/workers/syncer-ws-message-handler.js +7 -7
  102. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  103. package/out/zero-cache/src/workers/syncer.d.ts +1 -1
  104. package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
  105. package/out/zero-cache/src/workers/syncer.js +11 -10
  106. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  107. package/out/zero-client/src/client/connection.d.ts +15 -7
  108. package/out/zero-client/src/client/connection.d.ts.map +1 -1
  109. package/out/zero-client/src/client/connection.js.map +1 -1
  110. package/out/zero-client/src/client/crud-impl.d.ts +1 -1
  111. package/out/zero-client/src/client/crud-impl.d.ts.map +1 -1
  112. package/out/zero-client/src/client/crud-impl.js +1 -1
  113. package/out/zero-client/src/client/crud-impl.js.map +1 -1
  114. package/out/zero-client/src/client/crud.d.ts +1 -1
  115. package/out/zero-client/src/client/crud.d.ts.map +1 -1
  116. package/out/zero-client/src/client/crud.js +1 -1
  117. package/out/zero-client/src/client/crud.js.map +1 -1
  118. package/out/zero-client/src/client/keys.d.ts +1 -1
  119. package/out/zero-client/src/client/keys.d.ts.map +1 -1
  120. package/out/zero-client/src/client/keys.js.map +1 -1
  121. package/out/zero-client/src/client/make-replicache-mutators.js +1 -1
  122. package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
  123. package/out/zero-client/src/client/mutation-tracker.d.ts +2 -1
  124. package/out/zero-client/src/client/mutation-tracker.d.ts.map +1 -1
  125. package/out/zero-client/src/client/mutation-tracker.js +3 -3
  126. package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
  127. package/out/zero-client/src/client/version.js +1 -1
  128. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  129. package/out/zero-client/src/client/zero.js +2 -2
  130. package/out/zero-client/src/client/zero.js.map +1 -1
  131. package/out/zero-client/src/types/client-state.d.ts +1 -1
  132. package/out/zero-client/src/types/client-state.d.ts.map +1 -1
  133. package/out/zero-protocol/src/custom-queries.js +1 -1
  134. package/out/zero-protocol/src/down.js +1 -1
  135. package/out/zero-protocol/src/error-kind-enum.d.ts +1 -2
  136. package/out/zero-protocol/src/error-kind-enum.d.ts.map +1 -1
  137. package/out/zero-protocol/src/error-kind-enum.js.map +1 -1
  138. package/out/zero-protocol/src/mutate-server.d.ts +165 -0
  139. package/out/zero-protocol/src/mutate-server.d.ts.map +1 -0
  140. package/out/zero-protocol/src/mutate-server.js +24 -0
  141. package/out/zero-protocol/src/mutate-server.js.map +1 -0
  142. package/out/zero-protocol/src/mutation.d.ts +229 -0
  143. package/out/zero-protocol/src/mutation.d.ts.map +1 -0
  144. package/out/zero-protocol/src/mutation.js +112 -0
  145. package/out/zero-protocol/src/mutation.js.map +1 -0
  146. package/out/zero-protocol/src/mutations-patch.js +1 -1
  147. package/out/zero-protocol/src/mutations-patch.js.map +1 -1
  148. package/out/zero-protocol/src/push.d.ts +3 -234
  149. package/out/zero-protocol/src/push.d.ts.map +1 -1
  150. package/out/zero-protocol/src/push.js +3 -114
  151. package/out/zero-protocol/src/push.js.map +1 -1
  152. package/out/zero-protocol/src/query-server.d.ts +150 -0
  153. package/out/zero-protocol/src/query-server.d.ts.map +1 -0
  154. package/out/zero-protocol/src/query-server.js +16 -0
  155. package/out/zero-protocol/src/query-server.js.map +1 -0
  156. package/out/zero-protocol/src/up.js +1 -1
  157. package/out/zero-server/src/mod.d.ts +4 -2
  158. package/out/zero-server/src/mod.d.ts.map +1 -1
  159. package/out/zero-server/src/process-mutations.d.ts +41 -4
  160. package/out/zero-server/src/process-mutations.d.ts.map +1 -1
  161. package/out/zero-server/src/process-mutations.js +52 -35
  162. package/out/zero-server/src/process-mutations.js.map +1 -1
  163. package/out/zero-server/src/push-processor.d.ts +3 -3
  164. package/out/zero-server/src/push-processor.d.ts.map +1 -1
  165. package/out/zero-server/src/push-processor.js.map +1 -1
  166. package/out/zero-server/src/queries/process-queries.d.ts +22 -52
  167. package/out/zero-server/src/queries/process-queries.d.ts.map +1 -1
  168. package/out/zero-server/src/queries/process-queries.js +50 -49
  169. package/out/zero-server/src/queries/process-queries.js.map +1 -1
  170. package/out/zero-server/src/zql-database.js.map +1 -1
  171. package/out/zero-types/src/default-types.d.ts +1 -0
  172. package/out/zero-types/src/default-types.d.ts.map +1 -1
  173. package/out/zql/src/builder/builder.d.ts.map +1 -1
  174. package/out/zql/src/builder/builder.js +17 -7
  175. package/out/zql/src/builder/builder.js.map +1 -1
  176. package/out/zql/src/ivm/cap.d.ts +32 -0
  177. package/out/zql/src/ivm/cap.d.ts.map +1 -0
  178. package/out/zql/src/ivm/cap.js +205 -0
  179. package/out/zql/src/ivm/cap.js.map +1 -0
  180. package/out/zql/src/ivm/constraint.js +1 -1
  181. package/out/zql/src/ivm/flipped-join.d.ts.map +1 -1
  182. package/out/zql/src/ivm/flipped-join.js +61 -15
  183. package/out/zql/src/ivm/flipped-join.js.map +1 -1
  184. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  185. package/out/zql/src/ivm/memory-source.js +3 -4
  186. package/out/zql/src/ivm/memory-source.js.map +1 -1
  187. package/out/zql/src/ivm/schema.d.ts +8 -0
  188. package/out/zql/src/ivm/schema.d.ts.map +1 -1
  189. package/out/zql/src/ivm/take.js +2 -2
  190. package/out/zql/src/mutate/mutator-registry.js.map +1 -1
  191. package/out/zql/src/mutate/mutator.d.ts +11 -2
  192. package/out/zql/src/mutate/mutator.d.ts.map +1 -1
  193. package/out/zql/src/mutate/mutator.js.map +1 -1
  194. package/out/zql/src/query/query-registry.d.ts +9 -2
  195. package/out/zql/src/query/query-registry.d.ts.map +1 -1
  196. package/out/zql/src/query/query-registry.js.map +1 -1
  197. package/out/zqlite/src/table-source.d.ts.map +1 -1
  198. package/out/zqlite/src/table-source.js +4 -1
  199. package/out/zqlite/src/table-source.js.map +1 -1
  200. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"pusher.js","names":["#contextManager","#pusher","#queue","#config","#lc","#isStopped","#refCount","#stopped","#clients","#customMutations","#pushes","#processPush","#fanOutResponses","#failDownstream"],"sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"sourcesContent":["import {ROOT_CONTEXT, context, propagation} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {groupBy} from '../../../../shared/src/arrays.ts';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport {getErrorMessage} from '../../../../shared/src/error.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../../zero-protocol/src/error-reason.ts';\nimport {\n isProtocolError,\n type PushFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {\n CLEANUP_RESULTS_MUTATION_NAME,\n pushResponseSchema,\n type MutationID,\n type PushBody,\n type PushResponse,\n} from '../../../../zero-protocol/src/push.ts';\nimport {authEquals, isAuthErrorBody} from '../../auth/auth.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport {fetchFromAPIServer} from '../../custom/fetch.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {HandlerResult, StreamResult} from '../../workers/connection.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport type {\n ConnectionContext,\n ConnectionContextManager,\n ConnectionSelector,\n} from '../view-syncer/connection-context-manager.ts';\n\nexport interface Pusher extends RefCountedService {\n initConnection(selector: ConnectionSelector): Source<Downstream>;\n enqueuePush(selector: ConnectionSelector, push: PushBody): HandlerResult;\n ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void>;\n deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void>;\n}\n\ntype Config = Pick<ZeroConfig, 'app' | 'shard'>;\n\n/**\n * Receives push messages from zero-client and forwards\n * them the the user's API server.\n *\n * If the user's API server is taking too long to process\n * the push, the PusherService will add the push to a queue\n * and send pushes in bulk the next time the user's API server\n * is available.\n *\n * - One PusherService exists per client group.\n * - Mutations for a given client are always sent in-order\n * - Mutations for different clients in the same group may be interleaved\n */\nexport class PusherService implements Service, Pusher {\n readonly id: string;\n readonly #contextManager: ConnectionContextManager;\n readonly #pusher: PushWorker;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #config: Config;\n readonly #lc: LogContext;\n #stopped: Promise<void> | undefined;\n #refCount = 0;\n #isStopped = false;\n\n constructor(\n appConfig: Config,\n lc: LogContext,\n clientGroupID: string,\n contextManager: ConnectionContextManager,\n ) {\n this.#contextManager = contextManager;\n this.#config = appConfig;\n this.#lc = lc.withContext('component', 'pusherService');\n this.#queue = new Queue();\n this.#pusher = new PushWorker(\n appConfig,\n lc,\n this.#contextManager,\n this.#queue,\n );\n this.id = clientGroupID;\n }\n\n initConnection(selector: ConnectionSelector) {\n return this.#pusher.initConnection(selector);\n }\n\n enqueuePush(\n selector: ConnectionSelector,\n push: PushBody,\n ): Exclude<HandlerResult, StreamResult> {\n this.#pusher.enqueuePush(\n this.#contextManager.mustGetConnectionContext(selector),\n push,\n );\n\n return {\n type: 'ok',\n };\n }\n\n async ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void> {\n const ctx = this.#contextManager.getConnectionContext(requester);\n if (!ctx?.pushContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: upToID.clientID,\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'single',\n clientGroupID: this.id,\n clientID: upToID.clientID,\n upToMutationID: upToID.id,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-${this.id}-${upToID.clientID}-${upToID.id}`,\n };\n\n try {\n await fetchFromAPIServer(\n pushResponseSchema,\n 'push',\n this.#lc,\n ctx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n /**\n * Bulk cleanup is routed through the requester's push context.\n *\n * This assumes the client group shares a compatible push endpoint/auth\n * context.\n */\n async deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void> {\n if (clientIDs.length === 0) {\n return;\n }\n\n const ctx = this.#contextManager.getConnectionContext(requester);\n if (!ctx?.pushContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: clientIDs[0], // Use first client as sender\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'bulk',\n clientGroupID: this.id,\n clientIDs,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-bulk-${this.id}-${Date.now()}`,\n };\n\n try {\n await fetchFromAPIServer(\n pushResponseSchema,\n 'push',\n this.#lc,\n ctx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send bulk cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n run(): Promise<void> {\n this.#stopped = this.#pusher.run();\n return this.#stopped;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return must(this.#stopped, 'Stop was called before `run`');\n }\n this.#isStopped = true;\n this.#queue.enqueue('stop');\n return must(this.#stopped, 'Stop was called before `run`');\n }\n}\n\ntype PusherEntry = {\n push: PushBody;\n context: ConnectionContext;\n};\ntype PusherEntryOrStop = PusherEntry | 'stop';\n\n/**\n * Awaits items in the queue then drains and sends them all\n * to the user's API server.\n */\nclass PushWorker {\n readonly #contextManager: ConnectionContextManager;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #lc: LogContext;\n readonly #config: Config;\n readonly #clients: Map<\n string,\n {wsID: string; downstream: Subscription<Downstream>}\n >;\n\n readonly #customMutations = getOrCreateCounter(\n 'mutation',\n 'custom',\n 'Number of custom mutations processed',\n );\n readonly #pushes = getOrCreateCounter(\n 'mutation',\n 'pushes',\n 'Number of pushes processed by the pusher',\n );\n\n constructor(\n config: Config,\n lc: LogContext,\n contextManager: ConnectionContextManager,\n queue: Queue<PusherEntryOrStop>,\n ) {\n this.#lc = lc.withContext('component', 'pusher');\n this.#contextManager = contextManager;\n this.#queue = queue;\n this.#config = config;\n this.#clients = new Map();\n }\n\n /**\n * Returns a new downstream stream if the clientID,wsID pair has not been seen before.\n * If a clientID already exists with a different wsID, that client's downstream is cancelled.\n */\n initConnection(selector: ConnectionSelector) {\n const existing = this.#clients.get(selector.clientID);\n if (existing && existing.wsID === selector.wsID) {\n // already initialized for this socket\n throw new Error('Connection was already initialized');\n }\n\n // client is back on a new connection\n if (existing) {\n existing.downstream.cancel();\n }\n\n const downstream = Subscription.create<Downstream>({\n cleanup: () => {\n this.#clients.delete(selector.clientID);\n },\n });\n this.#clients.set(selector.clientID, {\n wsID: selector.wsID,\n downstream,\n });\n return downstream;\n }\n\n enqueuePush(context: ConnectionContext, push: PushBody) {\n this.#queue.enqueue({\n push,\n context,\n });\n }\n\n async run() {\n for (;;) {\n const task = await this.#queue.dequeue();\n const rest = this.#queue.drain();\n const [pushes, terminate] = combinePushes([task, ...rest]);\n for (const push of pushes) {\n const parentContext = push.push.traceparent\n ? propagation.extract(ROOT_CONTEXT, {\n traceparent: push.push.traceparent,\n })\n : context.active();\n const response = await context.with(parentContext, () =>\n this.#processPush(push),\n );\n await this.#fanOutResponses(response);\n }\n\n if (terminate) {\n break;\n }\n }\n }\n\n /**\n * 1. If the entire `push` fails, we send the error to relevant clients.\n * 2. If the push succeeds, we look for any mutation failure that should cause the connection to terminate\n * and terminate the connection for those clients.\n */\n #fanOutResponses(response: PushResponse) {\n const connectionTerminations: (() => void)[] = [];\n\n // if the entire push failed, send that to the client.\n if ('kind' in response || 'error' in response) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a push error.',\n response,\n );\n // TODO(0xcadams): Fanout is keyed only by clientID here. If a response arrives\n // after reconnect or re-auth, `#clients.get(clientID)` may point at a\n // newer wsID/revision and fail the replacement downstream instead.\n const groupedMutationIDs = groupBy(\n response.mutationIDs ?? [],\n m => m.clientID,\n );\n for (const [clientID, mutationIDs] of groupedMutationIDs) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n // We do not resolve mutations on the client if the push fails\n // as those mutations will be retried.\n if ('error' in response) {\n // This error code path will eventually be removed when we\n // no longer support the legacy push error format.\n const pushFailedBody: PushFailedBody =\n response.error === 'http'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n status: response.status,\n bodyPreview: response.details,\n mutationIDs,\n message: `Fetch from API server returned non-OK status ${response.status}`,\n }\n : response.error === 'unsupportedPushVersion'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.UnsupportedPushVersion,\n mutationIDs,\n message: `Unsupported push version`,\n }\n : {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Internal,\n mutationIDs,\n message:\n response.error === 'zeroPusher'\n ? response.details\n : response.error === 'unsupportedSchemaVersion'\n ? 'Unsupported schema version'\n : 'An unknown error occurred while pushing to the API server',\n };\n\n this.#failDownstream(client.downstream, pushFailedBody);\n } else if ('kind' in response) {\n this.#failDownstream(client.downstream, response);\n } else {\n unreachable(response);\n }\n }\n } else {\n // Look for mutations results that should cause us to terminate the connection\n // TODO(0xcadams): Same stale-routing issue as above: fatal mutation results are\n // still mapped to the current downstream by clientID only.\n const groupedMutations = groupBy(response.mutations, m => m.id.clientID);\n for (const [clientID, mutations] of groupedMutations) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n let failure: PushFailedBody | undefined;\n let i = 0;\n for (; i < mutations.length; i++) {\n const m = mutations[i];\n if ('error' in m.result) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a mutation error.',\n m.result,\n );\n }\n // This error code path will eventually be removed,\n // keeping this for backwards compatibility, but the server\n // should now return a PushFailedBody with the mutationIDs\n if ('error' in m.result && m.result.error === 'oooMutation') {\n failure = {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.OutOfOrderMutation,\n message: 'mutation was out of order',\n details: m.result.details,\n mutationIDs: mutations.map(m => ({\n clientID: m.id.clientID,\n id: m.id.id,\n })),\n };\n break;\n }\n }\n\n if (failure && i < mutations.length - 1) {\n this.#lc.warn?.(\n 'push-response contains mutations after a mutation which should fatal the connection',\n );\n }\n\n if (failure) {\n connectionTerminations.push(() =>\n this.#failDownstream(client.downstream, failure),\n );\n }\n }\n }\n\n connectionTerminations.forEach(cb => cb());\n }\n\n async #processPush(entry: PusherEntry): Promise<PushResponse> {\n this.#customMutations.add(entry.push.mutations.length, {\n clientGroupID: entry.push.clientGroupID,\n });\n this.#pushes.add(1, {\n clientGroupID: entry.push.clientGroupID,\n });\n\n // Record custom mutations for telemetry\n recordMutation('custom', entry.push.mutations.length);\n\n const url = must(\n entry.context.pushContext.url,\n 'ZERO_MUTATE_URL is not set',\n );\n\n this.#lc.debug?.(\n 'pushing to',\n url,\n 'with',\n entry.push.mutations.length,\n 'mutations',\n );\n\n let mutationIDs: MutationID[] = [];\n\n try {\n mutationIDs = entry.push.mutations.map(m => ({\n id: m.id,\n clientID: m.clientID,\n }));\n\n const response = await fetchFromAPIServer(\n pushResponseSchema,\n 'push',\n this.#lc,\n entry.context,\n {\n appID: this.#config.app.id,\n shardNum: this.#config.shard.num,\n },\n entry.push,\n );\n if ('kind' in response || 'error' in response) {\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.context.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#contextManager.failConnection(\n entry.context,\n entry.context.revision,\n );\n }\n return response;\n }\n // A successful push also validates this connection's current auth snapshot.\n // That lets later shared work reuse it without trusting stale credentials.\n this.#contextManager.validateConnection(\n entry.context,\n entry.context.revision,\n );\n return response;\n } catch (e) {\n if (isProtocolError(e) && e.errorBody.kind === ErrorKind.PushFailed) {\n const response = {\n ...e.errorBody,\n mutationIDs,\n } as const satisfies PushFailedBody;\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.context.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#contextManager.failConnection(\n entry.context,\n entry.context.revision,\n );\n }\n return response;\n }\n\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to push: ${getErrorMessage(e)}`,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n }\n\n #failDownstream(\n downstream: Subscription<Downstream>,\n errorBody: PushFailedBody,\n ): void {\n downstream.fail(new ProtocolErrorWithLevel(errorBody, 'warn'));\n }\n}\n\n/**\n * Pushes for different clients, sockets, or auth revisions could be interleaved.\n *\n * In order to batch safely, we only combine pushes from the same\n * clientID/wsID/revision snapshot.\n */\nexport function combinePushes(\n entries: readonly (PusherEntryOrStop | undefined)[],\n): [PusherEntry[], boolean] {\n const pushesByConnection = new Map<string, PusherEntry[]>();\n\n function collect() {\n const ret: PusherEntry[] = [];\n for (const entries of pushesByConnection.values()) {\n const composite: PusherEntry = {\n ...entries[0],\n push: {\n ...entries[0].push,\n mutations: [],\n },\n };\n ret.push(composite);\n for (const entry of entries) {\n assertAreCompatiblePushes(composite, entry);\n composite.push.mutations.push(...entry.push.mutations);\n }\n }\n return ret;\n }\n\n for (const entry of entries) {\n if (entry === 'stop' || entry === undefined) {\n return [collect(), true];\n }\n\n const key = `${entry.context.clientID}:${entry.context.wsID}:${entry.context.revision}`;\n const existing = pushesByConnection.get(key);\n if (existing) {\n existing.push(entry);\n } else {\n pushesByConnection.set(key, [entry]);\n }\n }\n\n return [collect(), false] as const;\n}\n\n// These invariants should always be true for a given clientID.\n// If they are not, we have a bug in the code somewhere.\nfunction assertAreCompatiblePushes(left: PusherEntry, right: PusherEntry) {\n assert(\n left.context.clientID === right.context.clientID,\n 'clientID must be the same for all pushes',\n );\n assert(\n left.context.wsID === right.context.wsID,\n 'wsID must be the same for all pushes',\n );\n assert(\n left.context.revision === right.context.revision,\n 'revision must be the same for all pushes',\n );\n assert(\n authEquals(left.context.auth, right.context.auth),\n 'auth must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.schemaVersion === right.push.schemaVersion,\n 'schemaVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.pushVersion === right.push.pushVersion,\n 'pushVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.context.pushContext.headerOptions.cookie ===\n right.context.pushContext.headerOptions.cookie,\n 'httpCookie must be the same for all pushes with the same clientID',\n );\n assert(\n left.context.pushContext.headerOptions.origin ===\n right.context.pushContext.headerOptions.origin,\n 'origin must be the same for all pushes with the same clientID',\n );\n assert(\n left.context.userID === right.context.userID,\n 'userID must be the same for all pushes with the same clientID',\n );\n assert(\n left.context.pushContext.url === right.context.pushContext.url,\n 'userPushURL must be the same for all pushes with the same clientID',\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pusher.js","names":["#connContextManager","#pusher","#queue","#config","#lc","#isStopped","#refCount","#stopped","#clients","#customMutations","#pushes","#processPush","#fanOutResponses","#failDownstream"],"sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"sourcesContent":["import {context, propagation, ROOT_CONTEXT} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {groupBy} from '../../../../shared/src/arrays.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {getErrorMessage} from '../../../../shared/src/error.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../../zero-protocol/src/error-reason.ts';\nimport {\n isProtocolError,\n type PushFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport {\n mutateResponseSchema,\n type MutateResponse,\n} from '../../../../zero-protocol/src/mutate-server.ts';\nimport type {MutationID} from '../../../../zero-protocol/src/mutation-id.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {CLEANUP_RESULTS_MUTATION_NAME} from '../../../../zero-protocol/src/mutation.ts';\nimport {type PushBody} from '../../../../zero-protocol/src/push.ts';\nimport {authEquals, isAuthErrorBody} from '../../auth/auth.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport {fetchFromAPIServer} from '../../custom/fetch.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {HandlerResult, StreamResult} from '../../workers/connection.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport type {\n ConnectionContext,\n ConnectionContextManager,\n ConnectionSelector,\n} from '../view-syncer/connection-context-manager.ts';\n\nexport interface Pusher extends RefCountedService {\n initConnection(selector: ConnectionSelector): Source<Downstream>;\n enqueuePush(selector: ConnectionSelector, push: PushBody): HandlerResult;\n ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void>;\n deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void>;\n}\n\ntype Config = Pick<ZeroConfig, 'app' | 'shard'>;\n\n/**\n * Receives push messages from zero-client and forwards\n * them the the user's API server.\n *\n * If the user's API server is taking too long to process\n * the push, the PusherService will add the push to a queue\n * and send pushes in bulk the next time the user's API server\n * is available.\n *\n * - One PusherService exists per client group.\n * - Mutations for a given client are always sent in-order\n * - Mutations for different clients in the same group may be interleaved\n */\nexport class PusherService implements Service, Pusher {\n readonly id: string;\n readonly #connContextManager: ConnectionContextManager;\n readonly #pusher: PushWorker;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #config: Config;\n readonly #lc: LogContext;\n #stopped: Promise<void> | undefined;\n #refCount = 0;\n #isStopped = false;\n\n constructor(\n appConfig: Config,\n lc: LogContext,\n clientGroupID: string,\n connContextManager: ConnectionContextManager,\n ) {\n this.#connContextManager = connContextManager;\n this.#config = appConfig;\n this.#lc = lc.withContext('component', 'pusherService');\n this.#queue = new Queue();\n this.#pusher = new PushWorker(\n appConfig,\n lc,\n this.#connContextManager,\n this.#queue,\n );\n this.id = clientGroupID;\n }\n\n initConnection(selector: ConnectionSelector) {\n return this.#pusher.initConnection(selector);\n }\n\n enqueuePush(\n selector: ConnectionSelector,\n push: PushBody,\n ): Exclude<HandlerResult, StreamResult> {\n this.#pusher.enqueuePush(\n this.#connContextManager.mustGetConnectionContext(selector),\n push,\n );\n\n return {\n type: 'ok',\n };\n }\n\n async ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void> {\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: upToID.clientID,\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'single',\n clientGroupID: this.id,\n clientID: upToID.clientID,\n upToMutationID: upToID.id,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-${this.id}-${upToID.clientID}-${upToID.id}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n /**\n * Bulk cleanup is routed through the requester's push context.\n *\n * This assumes the client group shares a compatible push endpoint/auth\n * context.\n */\n async deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void> {\n if (clientIDs.length === 0) {\n return;\n }\n\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: clientIDs[0], // Use first client as sender\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'bulk',\n clientGroupID: this.id,\n clientIDs,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-bulk-${this.id}-${Date.now()}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send bulk cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n run(): Promise<void> {\n this.#stopped = this.#pusher.run();\n return this.#stopped;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return must(this.#stopped, 'Stop was called before `run`');\n }\n this.#isStopped = true;\n this.#queue.enqueue('stop');\n return must(this.#stopped, 'Stop was called before `run`');\n }\n}\n\ntype PusherEntry = {\n push: PushBody;\n connCtx: ConnectionContext;\n};\ntype PusherEntryOrStop = PusherEntry | 'stop';\n\n/**\n * Awaits items in the queue then drains and sends them all\n * to the user's API server.\n */\nclass PushWorker {\n readonly #connContextManager: ConnectionContextManager;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #lc: LogContext;\n readonly #config: Config;\n readonly #clients: Map<\n string,\n {wsID: string; downstream: Subscription<Downstream>}\n >;\n\n readonly #customMutations = getOrCreateCounter(\n 'mutation',\n 'custom',\n 'Number of custom mutations processed',\n );\n readonly #pushes = getOrCreateCounter(\n 'mutation',\n 'pushes',\n 'Number of pushes processed by the pusher',\n );\n\n constructor(\n config: Config,\n lc: LogContext,\n connContextManager: ConnectionContextManager,\n queue: Queue<PusherEntryOrStop>,\n ) {\n this.#lc = lc.withContext('component', 'pusher');\n this.#connContextManager = connContextManager;\n this.#queue = queue;\n this.#config = config;\n this.#clients = new Map();\n }\n\n /**\n * Returns a new downstream stream if the clientID,wsID pair has not been seen before.\n * If a clientID already exists with a different wsID, that client's downstream is cancelled.\n */\n initConnection(selector: ConnectionSelector) {\n const existing = this.#clients.get(selector.clientID);\n if (existing && existing.wsID === selector.wsID) {\n // already initialized for this socket\n throw new Error('Connection was already initialized');\n }\n\n // client is back on a new connection\n if (existing) {\n existing.downstream.cancel();\n }\n\n const downstream = Subscription.create<Downstream>({\n cleanup: () => {\n this.#clients.delete(selector.clientID);\n },\n });\n this.#clients.set(selector.clientID, {\n wsID: selector.wsID,\n downstream,\n });\n return downstream;\n }\n\n enqueuePush(connCtx: ConnectionContext, push: PushBody) {\n this.#queue.enqueue({\n push,\n connCtx,\n });\n }\n\n async run() {\n for (;;) {\n const task = await this.#queue.dequeue();\n const rest = this.#queue.drain();\n const [pushes, terminate] = combinePushes([task, ...rest]);\n for (const push of pushes) {\n const parentContext = push.push.traceparent\n ? propagation.extract(ROOT_CONTEXT, {\n traceparent: push.push.traceparent,\n })\n : context.active();\n const response = await context.with(parentContext, () =>\n this.#processPush(push),\n );\n await this.#fanOutResponses(response);\n }\n\n if (terminate) {\n break;\n }\n }\n }\n\n /**\n * 1. If the entire `push` fails, we send the error to relevant clients.\n * 2. If the push succeeds, we look for any mutation failure that should cause the connection to terminate\n * and terminate the connection for those clients.\n */\n #fanOutResponses(response: MutateResponse) {\n const connectionTerminations: (() => void)[] = [];\n\n // if the entire push failed, send that to the client.\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a push error.',\n response,\n );\n // TODO(0xcadams): Fanout is keyed only by clientID here. If a response arrives\n // after reconnect or re-auth, `#clients.get(clientID)` may point at a\n // newer wsID/revision and fail the replacement downstream instead.\n const groupedMutationIDs = groupBy(\n response.mutationIDs ?? [],\n m => m.clientID,\n );\n for (const [clientID, mutationIDs] of groupedMutationIDs) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n // We do not resolve mutations on the client if the push fails\n // as those mutations will be retried.\n if ('error' in response) {\n // This error code path will eventually be removed when we\n // no longer support the legacy push error format.\n const pushFailedBody: PushFailedBody =\n response.error === 'http'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n status: response.status,\n bodyPreview: response.details,\n mutationIDs,\n message: `Fetch from API server returned non-OK status ${response.status}`,\n }\n : response.error === 'unsupportedPushVersion'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.UnsupportedPushVersion,\n mutationIDs,\n message: `Unsupported push version`,\n }\n : {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Internal,\n mutationIDs,\n message:\n response.error === 'zeroPusher'\n ? response.details\n : response.error === 'unsupportedSchemaVersion'\n ? 'Unsupported schema version'\n : 'An unknown error occurred while pushing to the API server',\n };\n\n this.#failDownstream(client.downstream, pushFailedBody);\n } else {\n this.#failDownstream(client.downstream, response);\n }\n }\n } else {\n // Look for mutations results that should cause us to terminate the connection\n // TODO(0xcadams): Same stale-routing issue as above: fatal mutation results are\n // still mapped to the current downstream by clientID only.\n const groupedMutations = groupBy(response.mutations, m => m.id.clientID);\n for (const [clientID, mutations] of groupedMutations) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n let failure: PushFailedBody | undefined;\n let i = 0;\n for (; i < mutations.length; i++) {\n const m = mutations[i];\n if ('error' in m.result) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a mutation error.',\n m.result,\n );\n }\n // This error code path will eventually be removed,\n // keeping this for backwards compatibility, but the server\n // should now return a PushFailedBody with the mutationIDs\n if ('error' in m.result && m.result.error === 'oooMutation') {\n failure = {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.OutOfOrderMutation,\n message: 'mutation was out of order',\n details: m.result.details,\n mutationIDs: mutations.map(m => ({\n clientID: m.id.clientID,\n id: m.id.id,\n })),\n };\n break;\n }\n }\n\n if (failure && i < mutations.length - 1) {\n this.#lc.warn?.(\n 'push-response contains mutations after a mutation which should fatal the connection',\n );\n }\n\n if (failure) {\n connectionTerminations.push(() =>\n this.#failDownstream(client.downstream, failure),\n );\n }\n }\n }\n\n connectionTerminations.forEach(cb => cb());\n }\n\n async #processPush(entry: PusherEntry): Promise<MutateResponse> {\n this.#customMutations.add(entry.push.mutations.length, {\n clientGroupID: entry.push.clientGroupID,\n });\n this.#pushes.add(1, {\n clientGroupID: entry.push.clientGroupID,\n });\n\n // Record custom mutations for telemetry\n recordMutation('custom', entry.push.mutations.length);\n\n const url = must(\n entry.connCtx.mutateContext.url,\n 'ZERO_MUTATE_URL is not set',\n );\n\n this.#lc.debug?.(\n 'pushing to',\n url,\n 'with',\n entry.push.mutations.length,\n 'mutations',\n );\n\n let mutationIDs: MutationID[] = [];\n\n try {\n mutationIDs = entry.push.mutations.map(m => ({\n id: m.id,\n clientID: m.clientID,\n }));\n\n const response = await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n entry.connCtx,\n {\n appID: this.#config.app.id,\n shardNum: this.#config.shard.num,\n },\n entry.push,\n );\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n // A successful push also validates this connection's current auth snapshot.\n // That lets later shared work reuse it without trusting stale credentials.\n this.#connContextManager.validateConnection(\n entry.connCtx,\n entry.connCtx.revision,\n 'kind' in response &&\n response.kind === 'MutateResponse' &&\n response?.userID !== undefined\n ? {kind: 'server-validated', validatedUserID: response.userID}\n : {kind: 'client-fallback'},\n );\n return response;\n } catch (e) {\n if (isProtocolError(e) && e.errorBody.kind === ErrorKind.PushFailed) {\n const response = {\n ...e.errorBody,\n mutationIDs,\n } as const satisfies PushFailedBody;\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n\n if (isProtocolError(e) && isAuthErrorBody(e.errorBody)) {\n // The push completed far enough for local validation to reject the\n // connection, so invalidate it and surface the result as PushFailed.\n this.#lc.warn?.('Push validation failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: e.message,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n message: e.message,\n status: 401,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to push: ${getErrorMessage(e)}`,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n }\n\n #failDownstream(\n downstream: Subscription<Downstream>,\n errorBody: PushFailedBody,\n ): void {\n downstream.fail(new ProtocolErrorWithLevel(errorBody, 'warn'));\n }\n}\n\n/**\n * Pushes for different clients, sockets, or auth revisions could be interleaved.\n *\n * In order to batch safely, we only combine pushes from the same\n * clientID/wsID/revision snapshot.\n */\nexport function combinePushes(\n entries: readonly (PusherEntryOrStop | undefined)[],\n): [PusherEntry[], boolean] {\n const pushesByConnection = new Map<string, PusherEntry[]>();\n\n function collect() {\n const ret: PusherEntry[] = [];\n for (const entries of pushesByConnection.values()) {\n const composite: PusherEntry = {\n ...entries[0],\n push: {\n ...entries[0].push,\n mutations: [],\n },\n };\n ret.push(composite);\n for (const entry of entries) {\n assertAreCompatiblePushes(composite, entry);\n composite.push.mutations.push(...entry.push.mutations);\n }\n }\n return ret;\n }\n\n for (const entry of entries) {\n if (entry === 'stop' || entry === undefined) {\n return [collect(), true];\n }\n\n const key = `${entry.connCtx.clientID}:${entry.connCtx.wsID}:${entry.connCtx.revision}`;\n const existing = pushesByConnection.get(key);\n if (existing) {\n existing.push(entry);\n } else {\n pushesByConnection.set(key, [entry]);\n }\n }\n\n return [collect(), false] as const;\n}\n\n// These invariants should always be true for a given clientID.\n// If they are not, we have a bug in the code somewhere.\nfunction assertAreCompatiblePushes(left: PusherEntry, right: PusherEntry) {\n assert(\n left.connCtx.clientID === right.connCtx.clientID,\n 'clientID must be the same for all pushes',\n );\n assert(\n left.connCtx.wsID === right.connCtx.wsID,\n 'wsID must be the same for all pushes',\n );\n assert(\n left.connCtx.revision === right.connCtx.revision,\n 'revision must be the same for all pushes',\n );\n assert(\n authEquals(left.connCtx.auth, right.connCtx.auth),\n 'auth must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.schemaVersion === right.push.schemaVersion,\n 'schemaVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.pushVersion === right.push.pushVersion,\n 'pushVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.cookie ===\n right.connCtx.mutateContext.headerOptions.cookie,\n 'httpCookie must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.origin ===\n right.connCtx.mutateContext.headerOptions.origin,\n 'origin must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.user.id === right.connCtx.user.id,\n 'userID must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.url === right.connCtx.mutateContext.url,\n 'userPushURL must be the same for all pushes with the same clientID',\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"change-log.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/replicator/schema/change-log.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,CAAC,MAAM,qCAAqC,CAAC;AACzD,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gCAAgC,CAAC;AAChE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,wBAAwB,CAAC;AAGvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,eAAO,MAAM,MAAM,MAAM,CAAC;AAC1B,eAAO,MAAM,MAAM,MAAM,CAAC;AAC1B,eAAO,MAAM,WAAW,MAAM,CAAC;AAC/B,eAAO,MAAM,QAAQ,MAAM,CAAC;AAG5B,eAAO,MAAM,uBAAuB,sbAmCjC,CAAC;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB;;;;;EAc5B,CAAC;AAEN,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,QAAA,MAAM,uBAAuB;;;;;;aAQ3B,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,qBAAa,SAAS;;gBAMR,EAAE,EAAE,QAAQ;IAsCxB;;;;;;;;;;OAUG;IACH,QAAQ,CACN,OAAO,EAAE,WAAW,EACpB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,UAAU,EACf,UAAU,EAAE,MAAM,EAAE,GAAG,SAAS,GAC/B,MAAM;IAIT,WAAW,CACT,OAAO,EAAE,WAAW,EACpB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,UAAU,GACd,MAAM;IAOT,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU;;;;;;;IAoC7C,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM;IAIjD,UAAU,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM;CAG/C"}
1
+ {"version":3,"file":"change-log.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/replicator/schema/change-log.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,CAAC,MAAM,qCAAqC,CAAC;AACzD,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gCAAgC,CAAC;AAChE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,wBAAwB,CAAC;AAGvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,eAAO,MAAM,MAAM,MAAM,CAAC;AAC1B,eAAO,MAAM,MAAM,MAAM,CAAC;AAC1B,eAAO,MAAM,WAAW,MAAM,CAAC;AAC/B,eAAO,MAAM,QAAQ,MAAM,CAAC;AAG5B,eAAO,MAAM,uBAAuB,sbAmCjC,CAAC;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB;;;;;EAc5B,CAAC;AAEN,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,QAAA,MAAM,uBAAuB;;;;;;aAQ3B,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,qBAAa,SAAS;;gBAMR,EAAE,EAAE,QAAQ;IAuCxB;;;;;;;;;;OAUG;IACH,QAAQ,CACN,OAAO,EAAE,WAAW,EACpB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,UAAU,EACf,UAAU,EAAE,MAAM,EAAE,GAAG,SAAS,GAC/B,MAAM;IAIT,WAAW,CACT,OAAO,EAAE,WAAW,EACpB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,UAAU,GACd,MAAM;IAOT,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU;;;;;;;IAoC7C,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM;IAIjD,UAAU,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM;CAG/C"}
@@ -1 +1 @@
1
- {"version":3,"file":"change-log.js","names":["#logRowOpStmt","#logRowOpWithBackfillStmt","#logTableWideOpStmt","#getRowOpStmt","#logRowOp"],"sources":["../../../../../../../zero-cache/src/services/replicator/schema/change-log.ts"],"sourcesContent":["import {\n jsonObjectSchema,\n parse,\n stringify,\n} from '../../../../../shared/src/bigint-json.ts';\nimport * as v from '../../../../../shared/src/valita.ts';\nimport type {Database, Statement} from '../../../../../zqlite/src/db.ts';\nimport type {LexiVersion} from '../../../types/lexi-version.ts';\nimport type {LiteRowKey} from '../../../types/lite.ts';\nimport {normalizedKeyOrder} from '../../../types/row-key.ts';\n\n/**\n * The Change Log tracks the last operation (set or delete) for each row in the\n * data base, ordered by state version; in other words, a cross-table\n * index of row changes ordered by version. This facilitates a minimal \"diff\"\n * of row changes needed to advance a pipeline from one state version to another.\n *\n * The Change Log stores identifiers only, i.e. it does not store contents.\n * A database snapshot at the previous version can be used to query a row's\n * old contents, if any, and the current snapshot can be used to query a row's\n * new contents. (In the common case, the new contents will have just been applied\n * and thus has a high likelihood of being in the SQLite cache.)\n *\n * There are two table-wide operations:\n * - `t` corresponds to the postgres `TRUNCATE` operation\n * - `r` represents any schema (i.e. column) change\n *\n * For both operations, the corresponding row changes are not explicitly included\n * in the change log. The consumer has the option of simulating them be reading\n * from pre- and post- snapshots, or resetting their state entirely with the current\n * snapshot.\n *\n * To achieve the desired ordering semantics when processing tables that have been\n * truncated, reset, and modified, the \"rowKey\" is set to `null` for resets and\n * the empty string `\"\"` for truncates. This means that resets will be encountered\n * before truncates, which will be processed before any subsequent row changes.\n *\n * This ordering is chosen because resets are currently the more \"destructive\" op\n * and result in aborting the processing (and starting from scratch); doing this\n * earlier reduces wasted work.\n */\n\nexport const SET_OP = 's';\nexport const DEL_OP = 'd';\nexport const TRUNCATE_OP = 't';\nexport const RESET_OP = 'r';\n\n// Exported for testing (and migrations)\nexport const CREATE_CHANGELOG_SCHEMA =\n // stateVersion : a.k.a. row version\n // pos : order in which to process the change (within the version)\n // table : The table associated with the change\n // rowKey : JSON row key for a row change. For table-wide changes RESET\n // and TRUNCATE, there is no associated row; instead, `pos` is\n // set to -1 and the rowKey is set to the stateVersion,\n // guaranteeing when attempting to process the transaction,\n // the pipeline is reset (and the change log traversal\n // aborted).\n // op : 's' for set (insert/update)\n // : 'd' for delete\n // : 'r' for table reset (schema change)\n // : 't' for table truncation (which also resets the pipeline)\n // backfillingColumnVersions\n // : A JSON mapping from column name to stateVersion tracked\n // for replicated writes of columns that are being backfilled.\n // This is used to prevent backfill data, which is at a\n // fixed snapshot/version outside of the replication stream,\n // from overwriting newer column values.\n //\n // Naming note: To maintain compatibility between a new replication-manager\n // and old view-syncers, the previous _zero.changeLog table is preserved\n // and its replacement given a new name \"changeLog2\".\n `\n CREATE TABLE \"_zero.changeLog2\" (\n \"stateVersion\" TEXT NOT NULL,\n \"pos\" INT NOT NULL,\n \"table\" TEXT NOT NULL,\n \"rowKey\" TEXT NOT NULL,\n \"op\" TEXT NOT NULL,\n \"backfillingColumnVersions\" TEXT DEFAULT '{}',\n PRIMARY KEY(\"stateVersion\", \"pos\"),\n UNIQUE(\"table\", \"rowKey\")\n );\n `;\n\n/**\n * Contains the changeLog fields relevant for computing the diff between\n * two snapshots of a replica. The `pos` and `backfillingColumnVersions`\n * fields are excluded, though the query should be ordered by\n * `<stateVersion, pos>`.\n */\nexport const changeLogEntrySchema = v\n .object({\n stateVersion: v.string(),\n table: v.string(),\n rowKey: v.string(),\n op: v.literalUnion(SET_OP, DEL_OP, TRUNCATE_OP, RESET_OP),\n })\n .map(val => ({\n ...val,\n // Note: sets the rowKey to `null` for table-wide ops / resets\n rowKey:\n val.op === 't' || val.op === 'r'\n ? null\n : v.parse(parse(val.rowKey), jsonObjectSchema),\n }));\n\nexport type ChangeLogEntry = v.Infer<typeof changeLogEntrySchema>;\n\nconst rawChangeLogEntrySchema = v.object({\n stateVersion: v.string(),\n table: v.string(),\n rowKey: v.string(),\n op: v.literalUnion(SET_OP, DEL_OP, TRUNCATE_OP, RESET_OP),\n backfillingColumnVersions: v\n .string()\n .map(val => v.record(v.string()).parse(JSON.parse(val))),\n});\n\nexport type RawChangeLogEntry = v.Infer<typeof rawChangeLogEntrySchema>;\n\nexport class ChangeLog {\n readonly #logRowOpStmt: Statement;\n readonly #logRowOpWithBackfillStmt: Statement;\n readonly #logTableWideOpStmt;\n readonly #getRowOpStmt: Statement;\n\n constructor(db: Database) {\n this.#logRowOpStmt = db.prepare(/*sql*/ `\n INSERT OR REPLACE INTO \"_zero.changeLog2\" \n (stateVersion, pos, \"table\", rowKey, op)\n VALUES (@version, @pos, @table, JSON(@rowKey), @op)\n `);\n\n this.#logRowOpWithBackfillStmt = db.prepare(/*sql*/ `\n INSERT INTO \"_zero.changeLog2\" \n (stateVersion, pos, \"table\", rowKey, op, backfillingColumnVersions)\n VALUES (@version, @pos, @table, JSON(@rowKey), @op, \n JSON(@backfillingColumnVersions))\n ON CONFLICT (\"table\", rowKey) DO UPDATE \n SET stateVersion = excluded.stateVersion,\n pos = excluded.pos,\n op = excluded.op,\n backfillingColumnVersions = json_patch(\n backfillingColumnVersions, excluded.backfillingColumnVersions)\n `);\n\n // Because table-wide ops result in aborting an incremental update\n // and rehydrating all queries at \"head\", they are assigned pos = -1\n // as an optimization to abort as early as possible to skip unnecessary\n // updates.\n //\n // However, changeLog entries that are destined to be \"skipped\" are\n // nonetheless kept for the purpose of tracking backfillingColumnVersions.\n this.#logTableWideOpStmt = db.prepare(/*sql*/ `\n INSERT OR REPLACE INTO \"_zero.changeLog2\" \n (stateVersion, pos, \"table\", rowKey, op) \n VALUES (@version, -1, @table, @version, @op)\n `);\n\n this.#getRowOpStmt = db.prepare(/*sql*/ `\n SELECT * FROM \"_zero.changeLog2\" WHERE \"table\" = ? AND \"rowKey\" = JSON(?)\n `);\n }\n\n /**\n *\n * @param backfilled The backfilling columns for which values were set. Note\n * that an empty list and the `undefined` value mean different things;\n * * An empty list indicates that a backfill is in progress but no\n * backfilling values were set. In this case, existing\n * backfillingColumnVersions are preserved.\n * * `undefined` indicates that there are no columns being backfilled.\n * In this case, any vestigial `backfillingColumnVersions` value\n * is cleared.\n */\n logSetOp(\n version: LexiVersion,\n pos: number,\n table: string,\n row: LiteRowKey,\n backfilled: string[] | undefined,\n ): string {\n return this.#logRowOp(version, pos, table, row, SET_OP, backfilled);\n }\n\n logDeleteOp(\n version: LexiVersion,\n pos: number,\n table: string,\n row: LiteRowKey,\n ): string {\n // Note: For delete ops, it is always safe to clear the\n // backfillingColumnVersions because the backfill algorithm\n // understands that deletes apply to the whole row.\n return this.#logRowOp(version, pos, table, row, DEL_OP, undefined);\n }\n\n getLatestRowOp(table: string, row: LiteRowKey) {\n const rowKey = stringify(normalizedKeyOrder(row));\n const result = this.#getRowOpStmt.get(table, rowKey);\n return result === undefined\n ? undefined\n : v.parse(result, rawChangeLogEntrySchema, 'passthrough');\n }\n\n #logRowOp(\n version: LexiVersion,\n pos: number,\n table: string,\n row: LiteRowKey,\n op: string,\n backfilled: string[] | undefined,\n ): string {\n const rowKey = stringify(normalizedKeyOrder(row));\n if (backfilled === undefined) {\n this.#logRowOpStmt.run({version, pos, table, rowKey, op});\n } else {\n const versions: Record<string, string> = {};\n for (const col of backfilled) {\n versions[col] = version;\n }\n this.#logRowOpWithBackfillStmt.run({\n version,\n pos,\n table,\n rowKey,\n op,\n backfillingColumnVersions: JSON.stringify(versions),\n });\n }\n return rowKey;\n }\n\n logTruncateOp(version: LexiVersion, table: string) {\n this.#logTableWideOpStmt.run({version, table, op: TRUNCATE_OP});\n }\n\n logResetOp(version: LexiVersion, table: string) {\n this.#logTableWideOpStmt.run({version, table, op: RESET_OP});\n }\n}\n"],"mappings":";;;AAgDA,IAAa,0BAwBX;;;;;;;;;;;;;;;;;;AAmBF,IAAa,uBAAuB,eACjC,OAAO;CACN,cAAc,eAAE,QAAQ;CACxB,OAAO,eAAE,QAAQ;CACjB,QAAQ,eAAE,QAAQ;CAClB,IAAI,aAAA,KAAA,KAAA,KAAA,IAAqD;CAC1D,CAAC,CACD,KAAI,SAAQ;CACX,GAAG;CAEH,QACE,IAAI,OAAO,OAAO,IAAI,OAAO,MACzB,OACA,MAAQ,QAAM,IAAI,OAAO,EAAE,iBAAiB;CACnD,EAAE;AAIL,IAAM,0BAA0B,eAAE,OAAO;CACvC,cAAc,eAAE,QAAQ;CACxB,OAAO,eAAE,QAAQ;CACjB,QAAQ,eAAE,QAAQ;CAClB,IAAI,aAAA,KAAA,KAAA,KAAA,IAAqD;CACzD,2BAA2B,eACxB,QAAQ,CACR,KAAI,QAAO,eAAE,OAAO,eAAE,QAAQ,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC;CAC3D,CAAC;AAIF,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CACA;CAEA,YAAY,IAAc;AACxB,QAAA,eAAqB,GAAG,QAAgB;;;;MAItC;AAEF,QAAA,2BAAiC,GAAG,QAAgB;;;;;;;;;;;MAWlD;AASF,QAAA,qBAA2B,GAAG,QAAgB;;;;MAI5C;AAEF,QAAA,eAAqB,GAAG,QAAgB;;MAEtC;;;;;;;;;;;;;CAcJ,SACE,SACA,KACA,OACA,KACA,YACQ;AACR,SAAO,MAAA,SAAe,SAAS,KAAK,OAAO,KAAA,KAAa,WAAW;;CAGrE,YACE,SACA,KACA,OACA,KACQ;AAIR,SAAO,MAAA,SAAe,SAAS,KAAK,OAAO,KAAA,KAAa,KAAA,EAAU;;CAGpE,eAAe,OAAe,KAAiB;EAC7C,MAAM,SAAS,UAAU,mBAAmB,IAAI,CAAC;EACjD,MAAM,SAAS,MAAA,aAAmB,IAAI,OAAO,OAAO;AACpD,SAAO,WAAW,KAAA,IACd,KAAA,IACA,MAAQ,QAAQ,yBAAyB,cAAc;;CAG7D,UACE,SACA,KACA,OACA,KACA,IACA,YACQ;EACR,MAAM,SAAS,UAAU,mBAAmB,IAAI,CAAC;AACjD,MAAI,eAAe,KAAA,EACjB,OAAA,aAAmB,IAAI;GAAC;GAAS;GAAK;GAAO;GAAQ;GAAG,CAAC;OACpD;GACL,MAAM,WAAmC,EAAE;AAC3C,QAAK,MAAM,OAAO,WAChB,UAAS,OAAO;AAElB,SAAA,yBAA+B,IAAI;IACjC;IACA;IACA;IACA;IACA;IACA,2BAA2B,KAAK,UAAU,SAAS;IACpD,CAAC;;AAEJ,SAAO;;CAGT,cAAc,SAAsB,OAAe;AACjD,QAAA,mBAAyB,IAAI;GAAC;GAAS;GAAO,IAAA;GAAgB,CAAC;;CAGjE,WAAW,SAAsB,OAAe;AAC9C,QAAA,mBAAyB,IAAI;GAAC;GAAS;GAAO,IAAA;GAAa,CAAC"}
1
+ {"version":3,"file":"change-log.js","names":["#logRowOpStmt","#logRowOpWithBackfillStmt","#logTableWideOpStmt","#getRowOpStmt","#logRowOp"],"sources":["../../../../../../../zero-cache/src/services/replicator/schema/change-log.ts"],"sourcesContent":["import {\n jsonObjectSchema,\n parse,\n stringify,\n} from '../../../../../shared/src/bigint-json.ts';\nimport * as v from '../../../../../shared/src/valita.ts';\nimport type {Database, Statement} from '../../../../../zqlite/src/db.ts';\nimport type {LexiVersion} from '../../../types/lexi-version.ts';\nimport type {LiteRowKey} from '../../../types/lite.ts';\nimport {normalizedKeyOrder} from '../../../types/row-key.ts';\n\n/**\n * The Change Log tracks the last operation (set or delete) for each row in the\n * data base, ordered by state version; in other words, a cross-table\n * index of row changes ordered by version. This facilitates a minimal \"diff\"\n * of row changes needed to advance a pipeline from one state version to another.\n *\n * The Change Log stores identifiers only, i.e. it does not store contents.\n * A database snapshot at the previous version can be used to query a row's\n * old contents, if any, and the current snapshot can be used to query a row's\n * new contents. (In the common case, the new contents will have just been applied\n * and thus has a high likelihood of being in the SQLite cache.)\n *\n * There are two table-wide operations:\n * - `t` corresponds to the postgres `TRUNCATE` operation\n * - `r` represents any schema (i.e. column) change\n *\n * For both operations, the corresponding row changes are not explicitly included\n * in the change log. The consumer has the option of simulating them be reading\n * from pre- and post- snapshots, or resetting their state entirely with the current\n * snapshot.\n *\n * To achieve the desired ordering semantics when processing tables that have been\n * truncated, reset, and modified, the \"rowKey\" is set to `null` for resets and\n * the empty string `\"\"` for truncates. This means that resets will be encountered\n * before truncates, which will be processed before any subsequent row changes.\n *\n * This ordering is chosen because resets are currently the more \"destructive\" op\n * and result in aborting the processing (and starting from scratch); doing this\n * earlier reduces wasted work.\n */\n\nexport const SET_OP = 's';\nexport const DEL_OP = 'd';\nexport const TRUNCATE_OP = 't';\nexport const RESET_OP = 'r';\n\n// Exported for testing (and migrations)\nexport const CREATE_CHANGELOG_SCHEMA =\n // stateVersion : a.k.a. row version\n // pos : order in which to process the change (within the version)\n // table : The table associated with the change\n // rowKey : JSON row key for a row change. For table-wide changes RESET\n // and TRUNCATE, there is no associated row; instead, `pos` is\n // set to -1 and the rowKey is set to the stateVersion,\n // guaranteeing when attempting to process the transaction,\n // the pipeline is reset (and the change log traversal\n // aborted).\n // op : 's' for set (insert/update)\n // : 'd' for delete\n // : 'r' for table reset (schema change)\n // : 't' for table truncation (which also resets the pipeline)\n // backfillingColumnVersions\n // : A JSON mapping from column name to stateVersion tracked\n // for replicated writes of columns that are being backfilled.\n // This is used to prevent backfill data, which is at a\n // fixed snapshot/version outside of the replication stream,\n // from overwriting newer column values.\n //\n // Naming note: To maintain compatibility between a new replication-manager\n // and old view-syncers, the previous _zero.changeLog table is preserved\n // and its replacement given a new name \"changeLog2\".\n `\n CREATE TABLE \"_zero.changeLog2\" (\n \"stateVersion\" TEXT NOT NULL,\n \"pos\" INT NOT NULL,\n \"table\" TEXT NOT NULL,\n \"rowKey\" TEXT NOT NULL,\n \"op\" TEXT NOT NULL,\n \"backfillingColumnVersions\" TEXT DEFAULT '{}',\n PRIMARY KEY(\"stateVersion\", \"pos\"),\n UNIQUE(\"table\", \"rowKey\")\n );\n `;\n\n/**\n * Contains the changeLog fields relevant for computing the diff between\n * two snapshots of a replica. The `pos` and `backfillingColumnVersions`\n * fields are excluded, though the query should be ordered by\n * `<stateVersion, pos>`.\n */\nexport const changeLogEntrySchema = v\n .object({\n stateVersion: v.string(),\n table: v.string(),\n rowKey: v.string(),\n op: v.literalUnion(SET_OP, DEL_OP, TRUNCATE_OP, RESET_OP),\n })\n .map(val => ({\n ...val,\n // Note: sets the rowKey to `null` for table-wide ops / resets\n rowKey:\n val.op === 't' || val.op === 'r'\n ? null\n : v.parse(parse(val.rowKey), jsonObjectSchema),\n }));\n\nexport type ChangeLogEntry = v.Infer<typeof changeLogEntrySchema>;\n\nconst rawChangeLogEntrySchema = v.object({\n stateVersion: v.string(),\n table: v.string(),\n rowKey: v.string(),\n op: v.literalUnion(SET_OP, DEL_OP, TRUNCATE_OP, RESET_OP),\n backfillingColumnVersions: v\n .string()\n .map(val => v.record(v.string()).parse(JSON.parse(val))),\n});\n\nexport type RawChangeLogEntry = v.Infer<typeof rawChangeLogEntrySchema>;\n\nexport class ChangeLog {\n readonly #logRowOpStmt: Statement;\n readonly #logRowOpWithBackfillStmt: Statement;\n readonly #logTableWideOpStmt;\n readonly #getRowOpStmt: Statement;\n\n constructor(db: Database) {\n this.#logRowOpStmt = db.prepare(/*sql*/ `\n INSERT OR REPLACE INTO \"_zero.changeLog2\" \n (stateVersion, pos, \"table\", rowKey, op)\n VALUES (@version, @pos, @table, JSON(@rowKey), @op)\n `);\n\n this.#logRowOpWithBackfillStmt = db.prepare(/*sql*/ `\n INSERT INTO \"_zero.changeLog2\" \n (stateVersion, pos, \"table\", rowKey, op, backfillingColumnVersions)\n VALUES (@version, @pos, @table, JSON(@rowKey), @op, \n JSON(@backfillingColumnVersions))\n ON CONFLICT (\"table\", rowKey) DO UPDATE \n SET stateVersion = excluded.stateVersion,\n pos = excluded.pos,\n op = excluded.op,\n backfillingColumnVersions = json_patch(\n backfillingColumnVersions, excluded.backfillingColumnVersions)\n `);\n\n // Because table-wide ops result in aborting an incremental update\n // and rehydrating all queries at \"head\", they are assigned pos = -1\n // as an optimization to abort as early as possible to skip unnecessary\n // updates.\n //\n // However, changeLog entries that are destined to be \"skipped\" are\n // nonetheless kept for the purpose of tracking backfillingColumnVersions.\n this.#logTableWideOpStmt = db.prepare(/*sql*/ `\n INSERT OR REPLACE INTO \"_zero.changeLog2\" \n (stateVersion, pos, \"table\", rowKey, op) \n VALUES (@version, -1, @table, @version, @op)\n `);\n\n // oxlint-disable-next-line zero/no-select-star -- Local SQLite replica query; not run through pg prepared statements.\n this.#getRowOpStmt = db.prepare(/*sql*/ `\n SELECT * FROM \"_zero.changeLog2\" WHERE \"table\" = ? AND \"rowKey\" = JSON(?)\n `);\n }\n\n /**\n *\n * @param backfilled The backfilling columns for which values were set. Note\n * that an empty list and the `undefined` value mean different things;\n * * An empty list indicates that a backfill is in progress but no\n * backfilling values were set. In this case, existing\n * backfillingColumnVersions are preserved.\n * * `undefined` indicates that there are no columns being backfilled.\n * In this case, any vestigial `backfillingColumnVersions` value\n * is cleared.\n */\n logSetOp(\n version: LexiVersion,\n pos: number,\n table: string,\n row: LiteRowKey,\n backfilled: string[] | undefined,\n ): string {\n return this.#logRowOp(version, pos, table, row, SET_OP, backfilled);\n }\n\n logDeleteOp(\n version: LexiVersion,\n pos: number,\n table: string,\n row: LiteRowKey,\n ): string {\n // Note: For delete ops, it is always safe to clear the\n // backfillingColumnVersions because the backfill algorithm\n // understands that deletes apply to the whole row.\n return this.#logRowOp(version, pos, table, row, DEL_OP, undefined);\n }\n\n getLatestRowOp(table: string, row: LiteRowKey) {\n const rowKey = stringify(normalizedKeyOrder(row));\n const result = this.#getRowOpStmt.get(table, rowKey);\n return result === undefined\n ? undefined\n : v.parse(result, rawChangeLogEntrySchema, 'passthrough');\n }\n\n #logRowOp(\n version: LexiVersion,\n pos: number,\n table: string,\n row: LiteRowKey,\n op: string,\n backfilled: string[] | undefined,\n ): string {\n const rowKey = stringify(normalizedKeyOrder(row));\n if (backfilled === undefined) {\n this.#logRowOpStmt.run({version, pos, table, rowKey, op});\n } else {\n const versions: Record<string, string> = {};\n for (const col of backfilled) {\n versions[col] = version;\n }\n this.#logRowOpWithBackfillStmt.run({\n version,\n pos,\n table,\n rowKey,\n op,\n backfillingColumnVersions: JSON.stringify(versions),\n });\n }\n return rowKey;\n }\n\n logTruncateOp(version: LexiVersion, table: string) {\n this.#logTableWideOpStmt.run({version, table, op: TRUNCATE_OP});\n }\n\n logResetOp(version: LexiVersion, table: string) {\n this.#logTableWideOpStmt.run({version, table, op: RESET_OP});\n }\n}\n"],"mappings":";;;AAgDA,IAAa,0BAwBX;;;;;;;;;;;;;;;;;;AAmBF,IAAa,uBAAuB,eACjC,OAAO;CACN,cAAc,eAAE,QAAQ;CACxB,OAAO,eAAE,QAAQ;CACjB,QAAQ,eAAE,QAAQ;CAClB,IAAI,aAAA,KAAA,KAAA,KAAA,IAAqD;CAC1D,CAAC,CACD,KAAI,SAAQ;CACX,GAAG;CAEH,QACE,IAAI,OAAO,OAAO,IAAI,OAAO,MACzB,OACA,MAAQ,QAAM,IAAI,OAAO,EAAE,iBAAiB;CACnD,EAAE;AAIL,IAAM,0BAA0B,eAAE,OAAO;CACvC,cAAc,eAAE,QAAQ;CACxB,OAAO,eAAE,QAAQ;CACjB,QAAQ,eAAE,QAAQ;CAClB,IAAI,aAAA,KAAA,KAAA,KAAA,IAAqD;CACzD,2BAA2B,eACxB,QAAQ,CACR,KAAI,QAAO,eAAE,OAAO,eAAE,QAAQ,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC;CAC3D,CAAC;AAIF,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CACA;CAEA,YAAY,IAAc;AACxB,QAAA,eAAqB,GAAG,QAAgB;;;;MAItC;AAEF,QAAA,2BAAiC,GAAG,QAAgB;;;;;;;;;;;MAWlD;AASF,QAAA,qBAA2B,GAAG,QAAgB;;;;MAI5C;AAGF,QAAA,eAAqB,GAAG,QAAgB;;MAEtC;;;;;;;;;;;;;CAcJ,SACE,SACA,KACA,OACA,KACA,YACQ;AACR,SAAO,MAAA,SAAe,SAAS,KAAK,OAAO,KAAA,KAAa,WAAW;;CAGrE,YACE,SACA,KACA,OACA,KACQ;AAIR,SAAO,MAAA,SAAe,SAAS,KAAK,OAAO,KAAA,KAAa,KAAA,EAAU;;CAGpE,eAAe,OAAe,KAAiB;EAC7C,MAAM,SAAS,UAAU,mBAAmB,IAAI,CAAC;EACjD,MAAM,SAAS,MAAA,aAAmB,IAAI,OAAO,OAAO;AACpD,SAAO,WAAW,KAAA,IACd,KAAA,IACA,MAAQ,QAAQ,yBAAyB,cAAc;;CAG7D,UACE,SACA,KACA,OACA,KACA,IACA,YACQ;EACR,MAAM,SAAS,UAAU,mBAAmB,IAAI,CAAC;AACjD,MAAI,eAAe,KAAA,EACjB,OAAA,aAAmB,IAAI;GAAC;GAAS;GAAK;GAAO;GAAQ;GAAG,CAAC;OACpD;GACL,MAAM,WAAmC,EAAE;AAC3C,QAAK,MAAM,OAAO,WAChB,UAAS,OAAO;AAElB,SAAA,yBAA+B,IAAI;IACjC;IACA;IACA;IACA;IACA;IACA,2BAA2B,KAAK,UAAU,SAAS;IACpD,CAAC;;AAEJ,SAAO;;CAGT,cAAc,SAAsB,OAAe;AACjD,QAAA,mBAAyB,IAAI;GAAC;GAAS;GAAO,IAAA;GAAgB,CAAC;;CAGjE,WAAW,SAAsB,OAAe;AAC9C,QAAA,mBAAyB,IAAI;GAAC;GAAS;GAAO,IAAA;GAAa,CAAC"}
@@ -19,7 +19,8 @@ var ShadowSyncService = class {
19
19
  }
20
20
  async run() {
21
21
  const { intervalMs, sampleRate, maxRowsPerTable, textCopy } = this.#options;
22
- const firstRunDelay = intervalMs + Math.floor(Math.random() * intervalMs);
22
+ const minFirstRunDelay = Math.floor(intervalMs * 2 / 3);
23
+ const firstRunDelay = minFirstRunDelay + Math.floor(Math.random() * (intervalMs - minFirstRunDelay));
23
24
  this.#lc.info?.(`shadow-syncer started; first run in ${firstRunDelay} ms, then every ${intervalMs} ms`);
24
25
  await this.#state.sleep(firstRunDelay);
25
26
  while (this.#state.shouldRun()) {
@@ -1 +1 @@
1
- {"version":3,"file":"shadow-sync-service.js","names":["#lc","#shard","#upstreamURI","#context","#options","#state"],"sources":["../../../../../../zero-cache/src/services/shadow-sync/shadow-sync-service.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport type {ShardConfig} from '../../types/shards.ts';\nimport type {ServerContext} from '../change-source/pg/initial-sync.ts';\nimport {shadowInitialSync} from '../change-source/pg/initial-sync.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {Service} from '../service.ts';\n\nexport type ShadowSyncOptions = {\n intervalMs: number;\n sampleRate: number;\n maxRowsPerTable: number;\n textCopy?: boolean | undefined;\n};\n\nexport class ShadowSyncService implements Service {\n readonly id = 'shadow-syncer';\n\n readonly #lc: LogContext;\n readonly #shard: ShardConfig;\n readonly #upstreamURI: string;\n readonly #context: ServerContext;\n readonly #options: ShadowSyncOptions;\n readonly #state = new RunningState('shadow-syncer');\n\n constructor(\n lc: LogContext,\n shard: ShardConfig,\n upstreamURI: string,\n context: ServerContext,\n options: ShadowSyncOptions,\n ) {\n this.#lc = lc;\n this.#shard = shard;\n this.#upstreamURI = upstreamURI;\n this.#context = context;\n this.#options = options;\n }\n\n async run() {\n const {intervalMs, sampleRate, maxRowsPerTable, textCopy} = this.#options;\n\n // Why: wait at least one full interval before the first run so shadow\n // sync never fires immediately on task startup, and add a random\n // fraction of the interval on top so a fleet-wide restart does not\n // cause every task to canary simultaneously.\n const firstRunDelay = intervalMs + Math.floor(Math.random() * intervalMs);\n this.#lc.info?.(\n `shadow-syncer started; first run in ${firstRunDelay} ms, then every ${intervalMs} ms`,\n );\n await this.#state.sleep(firstRunDelay);\n\n while (this.#state.shouldRun()) {\n const start = performance.now();\n try {\n await shadowInitialSync(\n this.#lc,\n this.#shard,\n this.#upstreamURI,\n {sampleRate, maxRowsPerTable},\n this.#context,\n textCopy !== undefined ? {textCopy} : undefined,\n );\n const elapsed = performance.now() - start;\n this.#lc.info?.(\n `shadow initial-sync completed (${elapsed.toFixed(0)} ms)`,\n );\n } catch (e) {\n const elapsed = performance.now() - start;\n this.#lc.error?.(\n `shadow initial-sync failed after ${elapsed.toFixed(0)} ms`,\n e,\n );\n }\n await this.#state.sleep(intervalMs);\n }\n }\n\n stop(): Promise<void> {\n this.#state.stop(this.#lc);\n return promiseVoid;\n }\n}\n"],"mappings":";;;;AAeA,IAAa,oBAAb,MAAkD;CAChD,KAAc;CAEd;CACA;CACA;CACA;CACA;CACA,SAAkB,IAAI,aAAa,gBAAgB;CAEnD,YACE,IACA,OACA,aACA,SACA,SACA;AACA,QAAA,KAAW;AACX,QAAA,QAAc;AACd,QAAA,cAAoB;AACpB,QAAA,UAAgB;AAChB,QAAA,UAAgB;;CAGlB,MAAM,MAAM;EACV,MAAM,EAAC,YAAY,YAAY,iBAAiB,aAAY,MAAA;EAM5D,MAAM,gBAAgB,aAAa,KAAK,MAAM,KAAK,QAAQ,GAAG,WAAW;AACzE,QAAA,GAAS,OACP,uCAAuC,cAAc,kBAAkB,WAAW,KACnF;AACD,QAAM,MAAA,MAAY,MAAM,cAAc;AAEtC,SAAO,MAAA,MAAY,WAAW,EAAE;GAC9B,MAAM,QAAQ,YAAY,KAAK;AAC/B,OAAI;AACF,UAAM,kBACJ,MAAA,IACA,MAAA,OACA,MAAA,aACA;KAAC;KAAY;KAAgB,EAC7B,MAAA,SACA,aAAa,KAAA,IAAY,EAAC,UAAS,GAAG,KAAA,EACvC;IACD,MAAM,UAAU,YAAY,KAAK,GAAG;AACpC,UAAA,GAAS,OACP,kCAAkC,QAAQ,QAAQ,EAAE,CAAC,MACtD;YACM,GAAG;IACV,MAAM,UAAU,YAAY,KAAK,GAAG;AACpC,UAAA,GAAS,QACP,oCAAoC,QAAQ,QAAQ,EAAE,CAAC,MACvD,EACD;;AAEH,SAAM,MAAA,MAAY,MAAM,WAAW;;;CAIvC,OAAsB;AACpB,QAAA,MAAY,KAAK,MAAA,GAAS;AAC1B,SAAO"}
1
+ {"version":3,"file":"shadow-sync-service.js","names":["#lc","#shard","#upstreamURI","#context","#options","#state"],"sources":["../../../../../../zero-cache/src/services/shadow-sync/shadow-sync-service.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport type {ShardConfig} from '../../types/shards.ts';\nimport type {ServerContext} from '../change-source/pg/initial-sync.ts';\nimport {shadowInitialSync} from '../change-source/pg/initial-sync.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {Service} from '../service.ts';\n\nexport type ShadowSyncOptions = {\n intervalMs: number;\n sampleRate: number;\n maxRowsPerTable: number;\n textCopy?: boolean | undefined;\n};\n\nexport class ShadowSyncService implements Service {\n readonly id = 'shadow-syncer';\n\n readonly #lc: LogContext;\n readonly #shard: ShardConfig;\n readonly #upstreamURI: string;\n readonly #context: ServerContext;\n readonly #options: ShadowSyncOptions;\n readonly #state = new RunningState('shadow-syncer');\n\n constructor(\n lc: LogContext,\n shard: ShardConfig,\n upstreamURI: string,\n context: ServerContext,\n options: ShadowSyncOptions,\n ) {\n this.#lc = lc;\n this.#shard = shard;\n this.#upstreamURI = upstreamURI;\n this.#context = context;\n this.#options = options;\n }\n\n async run() {\n const {intervalMs, sampleRate, maxRowsPerTable, textCopy} = this.#options;\n\n // Why: first run fires in [intervalMs * 2/3, intervalMs) late enough\n // that shadow sync never fires immediately on task startup, but always\n // before one full interval elapses so the canary completes at least once\n // per task lifetime (the replication manager is restarted every ~24h).\n // The last third is randomized so a fleet-wide restart does not cause\n // every task to canary simultaneously.\n const minFirstRunDelay = Math.floor((intervalMs * 2) / 3);\n const firstRunDelay =\n minFirstRunDelay +\n Math.floor(Math.random() * (intervalMs - minFirstRunDelay));\n this.#lc.info?.(\n `shadow-syncer started; first run in ${firstRunDelay} ms, then every ${intervalMs} ms`,\n );\n await this.#state.sleep(firstRunDelay);\n\n while (this.#state.shouldRun()) {\n const start = performance.now();\n try {\n await shadowInitialSync(\n this.#lc,\n this.#shard,\n this.#upstreamURI,\n {sampleRate, maxRowsPerTable},\n this.#context,\n textCopy !== undefined ? {textCopy} : undefined,\n );\n const elapsed = performance.now() - start;\n this.#lc.info?.(\n `shadow initial-sync completed (${elapsed.toFixed(0)} ms)`,\n );\n } catch (e) {\n const elapsed = performance.now() - start;\n this.#lc.error?.(\n `shadow initial-sync failed after ${elapsed.toFixed(0)} ms`,\n e,\n );\n }\n await this.#state.sleep(intervalMs);\n }\n }\n\n stop(): Promise<void> {\n this.#state.stop(this.#lc);\n return promiseVoid;\n }\n}\n"],"mappings":";;;;AAeA,IAAa,oBAAb,MAAkD;CAChD,KAAc;CAEd;CACA;CACA;CACA;CACA;CACA,SAAkB,IAAI,aAAa,gBAAgB;CAEnD,YACE,IACA,OACA,aACA,SACA,SACA;AACA,QAAA,KAAW;AACX,QAAA,QAAc;AACd,QAAA,cAAoB;AACpB,QAAA,UAAgB;AAChB,QAAA,UAAgB;;CAGlB,MAAM,MAAM;EACV,MAAM,EAAC,YAAY,YAAY,iBAAiB,aAAY,MAAA;EAQ5D,MAAM,mBAAmB,KAAK,MAAO,aAAa,IAAK,EAAE;EACzD,MAAM,gBACJ,mBACA,KAAK,MAAM,KAAK,QAAQ,IAAI,aAAa,kBAAkB;AAC7D,QAAA,GAAS,OACP,uCAAuC,cAAc,kBAAkB,WAAW,KACnF;AACD,QAAM,MAAA,MAAY,MAAM,cAAc;AAEtC,SAAO,MAAA,MAAY,WAAW,EAAE;GAC9B,MAAM,QAAQ,YAAY,KAAK;AAC/B,OAAI;AACF,UAAM,kBACJ,MAAA,IACA,MAAA,OACA,MAAA,aACA;KAAC;KAAY;KAAgB,EAC7B,MAAA,SACA,aAAa,KAAA,IAAY,EAAC,UAAS,GAAG,KAAA,EACvC;IACD,MAAM,UAAU,YAAY,KAAK,GAAG;AACpC,UAAA,GAAS,OACP,kCAAkC,QAAQ,QAAQ,EAAE,CAAC,MACtD;YACM,GAAG;IACV,MAAM,UAAU,YAAY,KAAK,GAAG;AACpC,UAAA,GAAS,QACP,oCAAoC,QAAQ,QAAQ,EAAE,CAAC,MACvD,EACD;;AAEH,SAAM,MAAA,MAAY,MAAM,WAAW;;;CAIvC,OAAsB;AACpB,QAAA,MAAY,KAAK,MAAA,GAAS;AAC1B,SAAO"}
@@ -5,7 +5,7 @@ import { parse, valita_exports } from "../../../../shared/src/valita.js";
5
5
  import { rowSchema } from "../../../../zero-protocol/src/data.js";
6
6
  import { ProtocolError } from "../../../../zero-protocol/src/error.js";
7
7
  import { primaryKeyValueRecordSchema } from "../../../../zero-protocol/src/primary-key.js";
8
- import { mutationResultSchema } from "../../../../zero-protocol/src/push.js";
8
+ import { mutationResultSchema } from "../../../../zero-protocol/src/mutation.js";
9
9
  import { upstreamSchema } from "../../types/shards.js";
10
10
  import { getOrCreateCounter, getOrCreateLatencyHistogram } from "../../observability/metrics.js";
11
11
  import { cmpVersions, cookieToVersion, versionToCookie, versionToNullableCookie } from "./schema/types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"client-handler.js","names":["#clientGroupID","#zeroClientsTable","#zeroMutationsTable","#lc","#downstream","#pokeTime","#pokeTransactions","#pokedRows","#baseVersion","#push","#updateLMIDs"],"sources":["../../../../../../zero-cache/src/services/view-syncer/client-handler.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport type {JSONObject} from '../../../../shared/src/bigint-json.ts';\nimport {\n assertJSONValue,\n type JSONObject as SafeJSONObject,\n} from '../../../../shared/src/json.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {Writable} from '../../../../shared/src/writable.ts';\nimport type {ErroredQuery} from '../../../../zero-protocol/src/custom-queries.ts';\nimport {rowSchema} from '../../../../zero-protocol/src/data.ts';\nimport type {DeleteClientsBody} from '../../../../zero-protocol/src/delete-clients.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {\n ProtocolError,\n type TransformFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport type {InspectDownBody} from '../../../../zero-protocol/src/inspect-down.ts';\nimport type {\n PokePartBody,\n PokeStartBody,\n} from '../../../../zero-protocol/src/poke.ts';\nimport {primaryKeyValueRecordSchema} from '../../../../zero-protocol/src/primary-key.ts';\nimport {mutationResultSchema} from '../../../../zero-protocol/src/push.ts';\nimport type {RowPatchOp} from '../../../../zero-protocol/src/row-patch.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {\n getLogLevel,\n wrapWithProtocolError,\n} from '../../types/error-with-level.ts';\nimport {upstreamSchema, type ShardID} from '../../types/shards.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport {\n cmpVersions,\n cookieToVersion,\n versionToCookie,\n versionToNullableCookie,\n type CVRVersion,\n type DelQueryPatch,\n type NullableCVRVersion,\n type PutQueryPatch,\n type RowID,\n} from './schema/types.ts';\n\nexport type PutRowPatch = {\n type: 'row';\n op: 'put';\n id: RowID;\n contents: JSONObject;\n};\n\nexport type DeleteRowPatch = {\n type: 'row';\n op: 'del';\n id: RowID;\n};\n\nexport type RowPatch = PutRowPatch | DeleteRowPatch;\nexport type ConfigPatch = DelQueryPatch | PutQueryPatch;\n\nexport type Patch = ConfigPatch | RowPatch;\n\nexport type PatchToVersion = {\n patch: Patch;\n toVersion: CVRVersion;\n};\n\nexport interface PokeHandler {\n addPatch(patch: PatchToVersion): Promise<void>;\n cancel(): Promise<void>;\n end(finalVersion: CVRVersion): Promise<void>;\n}\n\nconst NOOP: PokeHandler = {\n addPatch: () => promiseVoid,\n cancel: () => promiseVoid,\n end: () => promiseVoid,\n};\n\n/** Wraps PokeHandlers for multiple clients in a single PokeHandler. */\nexport function startPoke(\n clients: ClientHandler[],\n tentativeVersion: CVRVersion,\n): PokeHandler {\n const pokers = clients.map(c => c.startPoke(tentativeVersion));\n\n // Promise.allSettled() ensures that a failed (e.g. disconnected) client\n // does not prevent other clients from receiving the pokes. However, the\n // rate (per client group) will be limited by the slowest connection.\n return {\n addPatch: async patch => {\n await Promise.allSettled(pokers.map(poker => poker.addPatch(patch)));\n },\n cancel: async () => {\n await Promise.allSettled(pokers.map(poker => poker.cancel()));\n },\n end: async finalVersion => {\n await Promise.allSettled(pokers.map(poker => poker.end(finalVersion)));\n },\n };\n}\n\n// Semi-arbitrary threshold at which poke body parts are flushed.\n// When row size is being computed, that should be used as a threshold instead.\nconst PART_COUNT_FLUSH_THRESHOLD = 100;\n\n/**\n * Handles a single `ViewSyncer` connection.\n */\nexport class ClientHandler {\n readonly #clientGroupID: string;\n readonly clientID: string;\n readonly wsID: string;\n readonly #zeroClientsTable: string;\n readonly #zeroMutationsTable: string;\n readonly #lc: LogContext;\n readonly #downstream: Subscription<Downstream>;\n #baseVersion: NullableCVRVersion;\n\n readonly #pokeTime = getOrCreateLatencyHistogram(\n 'sync',\n 'poke.time',\n 'Time elapsed for each poke transaction. Canceled / noop pokes are excluded.',\n );\n\n readonly #pokeTransactions = getOrCreateCounter(\n 'sync',\n 'poke.transactions',\n 'Count of poke transactions.',\n );\n\n readonly #pokedRows = getOrCreateCounter(\n 'sync',\n 'poke.rows',\n 'Count of poked rows.',\n );\n\n constructor(\n lc: LogContext,\n clientGroupID: string,\n clientID: string,\n wsID: string,\n shard: ShardID,\n baseCookie: string | null,\n downstream: Subscription<Downstream>,\n ) {\n lc.debug?.('new client handler');\n this.#clientGroupID = clientGroupID;\n this.clientID = clientID;\n this.wsID = wsID;\n this.#zeroClientsTable = `${upstreamSchema(shard)}.clients`;\n this.#zeroMutationsTable = `${upstreamSchema(shard)}.mutations`;\n this.#lc = lc;\n this.#downstream = downstream;\n this.#baseVersion = cookieToVersion(baseCookie);\n }\n\n version(): NullableCVRVersion {\n return this.#baseVersion;\n }\n\n async #push(msg: Downstream): Promise<void> {\n const {result} = this.#downstream.push(msg);\n await result;\n }\n\n fail(e: unknown) {\n this.#lc[getLogLevel(e)]?.(\n `view-syncer closing connection with error: ${String(e)}`,\n e,\n );\n this.#downstream.fail(wrapWithProtocolError(e));\n }\n\n close(reason: string) {\n this.#lc.debug?.(`view-syncer closing connection: ${reason}`);\n this.#downstream.cancel();\n }\n\n startPoke(tentativeVersion: CVRVersion): PokeHandler {\n const pokeID = versionToCookie(tentativeVersion);\n const lc = this.#lc.withContext('pokeID', pokeID);\n\n if (cmpVersions(this.#baseVersion, tentativeVersion) >= 0) {\n lc.info?.(`already caught up, not sending poke.`);\n return NOOP;\n }\n\n const baseCookie = versionToNullableCookie(this.#baseVersion);\n const cookie = versionToCookie(tentativeVersion);\n lc.info?.(`starting poke from ${baseCookie} to ${cookie}`);\n\n const start = performance.now();\n\n const pokeStart: PokeStartBody = {pokeID, baseCookie};\n\n let pokeStarted = false;\n let body: PokePartBody | undefined;\n let partCount = 0;\n const ensureBody = async () => {\n if (!pokeStarted) {\n await this.#push(['pokeStart', pokeStart]);\n pokeStarted = true;\n }\n return (body ??= {pokeID});\n };\n const flushBody = async () => {\n if (body) {\n await this.#push(['pokePart', body]);\n body = undefined;\n partCount = 0;\n }\n };\n\n const addPatch = async (patchToVersion: PatchToVersion) => {\n const {patch, toVersion} = patchToVersion;\n if (cmpVersions(toVersion, this.#baseVersion) <= 0) {\n return;\n }\n const body = await ensureBody();\n\n const {type, op} = patch;\n switch (type) {\n case 'query': {\n const patches = patch.clientID\n ? ((body.desiredQueriesPatches ??= {})[patch.clientID] ??= [])\n : (body.gotQueriesPatch ??= []);\n if (op === 'put') {\n patches.push({op, hash: patch.id});\n } else {\n patches.push({op, hash: patch.id});\n }\n break;\n }\n case 'row':\n if (patch.id.table === this.#zeroClientsTable) {\n this.#updateLMIDs((body.lastMutationIDChanges ??= {}), patch);\n } else if (patch.id.table === this.#zeroMutationsTable) {\n const patches = (body.mutationsPatch ??= []);\n if (op === 'put') {\n const row = v.parse(\n ensureSafeJSON(patch.contents),\n mutationRowSchema,\n 'passthrough',\n );\n patches.push({\n op: 'put',\n mutation: {\n id: {\n clientID: row.clientID,\n id: row.mutationID,\n },\n result: row.result,\n },\n });\n } else {\n const {clientID, mutationID} = patch.id.rowKey;\n assert(\n typeof clientID === 'string',\n 'client id must be a string',\n );\n const id = Number(mutationID);\n assert(\n !Number.isNaN(id) && Number.isFinite(id) && id >= 0,\n 'mutation id must be a finite number',\n );\n patches.push({\n op: 'del',\n id: {\n clientID,\n id,\n },\n });\n }\n } else {\n (body.rowsPatch ??= []).push(makeRowPatch(patch));\n }\n break;\n default:\n unreachable(patch);\n }\n\n if (++partCount >= PART_COUNT_FLUSH_THRESHOLD) {\n await flushBody();\n }\n };\n\n return {\n addPatch: async (patchToVersion: PatchToVersion) => {\n try {\n await addPatch(patchToVersion);\n if (patchToVersion.patch.type === 'row') {\n this.#pokedRows.add(1);\n }\n } catch (e) {\n this.#downstream.fail(wrapWithProtocolError(e));\n }\n },\n\n cancel: async () => {\n if (pokeStarted) {\n await this.#push(['pokeEnd', {pokeID, cookie: '', cancel: true}]);\n }\n },\n\n end: async (finalVersion: CVRVersion) => {\n const cookie = versionToCookie(finalVersion);\n if (!pokeStarted) {\n if (cmpVersions(this.#baseVersion, finalVersion) === 0) {\n return; // Nothing changed and nothing was sent.\n }\n await this.#push(['pokeStart', pokeStart]);\n } else if (cmpVersions(this.#baseVersion, finalVersion) >= 0) {\n // Sanity check: If the poke was started, the finalVersion\n // must be > #baseVersion.\n throw new Error(\n `Patches were sent but finalVersion ${finalVersion} is ` +\n `not greater than baseVersion ${this.#baseVersion}`,\n );\n }\n await flushBody();\n await this.#push(['pokeEnd', {pokeID, cookie}]);\n this.#baseVersion = finalVersion;\n\n const elapsed = performance.now() - start;\n this.#pokeTransactions.add(1);\n this.#pokeTime.recordMs(elapsed);\n },\n };\n }\n\n async sendDeleteClients(\n lc: LogContext,\n deletedClientIDs: string[],\n deletedClientGroupIDs: string[],\n ) {\n const deleteClientsBody: Writable<DeleteClientsBody> = {};\n if (deletedClientIDs.length > 0) {\n deleteClientsBody.clientIDs = deletedClientIDs;\n }\n if (deletedClientGroupIDs.length > 0) {\n deleteClientsBody.clientGroupIDs = deletedClientGroupIDs;\n }\n lc.debug?.('sending deleteClients', deleteClientsBody);\n await this.#push(['deleteClients', deleteClientsBody]);\n }\n\n sendQueryTransformApplicationErrors(errors: ErroredQuery[]) {\n void this.#push(['transformError', errors]);\n }\n\n sendQueryTransformFailedError(error: TransformFailedBody) {\n this.fail(new ProtocolError(error));\n }\n\n sendInspectResponse(lc: LogContext, response: InspectDownBody): void {\n lc.debug?.('sending inspect response', response);\n this.#downstream.push(['inspect', response]);\n }\n\n #updateLMIDs(lmids: Record<string, number>, patch: RowPatch) {\n if (patch.op === 'put') {\n const row = ensureSafeJSON(patch.contents);\n const {clientGroupID, clientID, lastMutationID} = v.parse(\n row,\n lmidRowSchema,\n 'passthrough',\n );\n if (clientGroupID !== this.#clientGroupID) {\n this.#lc.error?.(\n `Received clients row for wrong clientGroupID. Ignoring.`,\n clientGroupID,\n );\n } else {\n lmids[clientID] = lastMutationID;\n }\n } else {\n // The 'constrain' and 'del' ops for clients can be ignored.\n patch.op satisfies 'constrain' | 'del';\n }\n }\n}\n\n// Note: The {APP_ID}_{SHARD_ID}.clients table is set up in replicator/initial-sync.ts.\nconst lmidRowSchema = v.object({\n clientGroupID: v.string(),\n clientID: v.string(),\n lastMutationID: v.number(), // Actually returned as a bigint, but converted by ensureSafeJSON().\n});\n\nconst mutationRowSchema = v.object({\n clientGroupID: v.string(),\n clientID: v.string(),\n mutationID: v.number(),\n result: mutationResultSchema,\n});\n\nfunction makeRowPatch(patch: RowPatch): RowPatchOp {\n const {\n op,\n id: {table: tableName, rowKey: id},\n } = patch;\n\n switch (op) {\n case 'put':\n return {\n op: 'put',\n tableName,\n value: v.parse(ensureSafeJSON(patch.contents), rowSchema),\n };\n\n case 'del':\n return {\n op,\n tableName,\n id: v.parse(id, primaryKeyValueRecordSchema),\n };\n\n default:\n unreachable(op);\n }\n}\n\n/**\n * Column values of type INT8 are returned as the `bigint` from the\n * Postgres library. These are converted to `number` if they are within\n * the safe Number range, allowing the protocol to support numbers larger\n * than 32-bits. Values outside of the safe number range (e.g. > 2^53) will\n * result in an Error.\n */\nexport function ensureSafeJSON(row: JSONObject): SafeJSONObject {\n const modified = Object.entries(row)\n .filter(([k, v]) => {\n if (typeof v === 'bigint') {\n if (v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER) {\n return true; // send this entry onto the next map() step.\n }\n throw new Error(`Value of \"${k}\" exceeds safe Number range (${v})`);\n } else if (typeof v === 'object') {\n assertJSONValue(v);\n }\n return false;\n })\n .map(([k, v]) => [k, Number(v)]);\n\n return modified.length\n ? {...row, ...Object.fromEntries(modified)}\n : (row as SafeJSONObject);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client-handler.js","names":["#clientGroupID","#zeroClientsTable","#zeroMutationsTable","#lc","#downstream","#pokeTime","#pokeTransactions","#pokedRows","#baseVersion","#push","#updateLMIDs"],"sources":["../../../../../../zero-cache/src/services/view-syncer/client-handler.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport type {JSONObject} from '../../../../shared/src/bigint-json.ts';\nimport {\n assertJSONValue,\n type JSONObject as SafeJSONObject,\n} from '../../../../shared/src/json.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {Writable} from '../../../../shared/src/writable.ts';\nimport type {ErroredQuery} from '../../../../zero-protocol/src/custom-queries.ts';\nimport {rowSchema} from '../../../../zero-protocol/src/data.ts';\nimport type {DeleteClientsBody} from '../../../../zero-protocol/src/delete-clients.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {\n ProtocolError,\n type TransformFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport type {InspectDownBody} from '../../../../zero-protocol/src/inspect-down.ts';\nimport {mutationResultSchema} from '../../../../zero-protocol/src/mutation.ts';\nimport type {\n PokePartBody,\n PokeStartBody,\n} from '../../../../zero-protocol/src/poke.ts';\nimport {primaryKeyValueRecordSchema} from '../../../../zero-protocol/src/primary-key.ts';\nimport type {RowPatchOp} from '../../../../zero-protocol/src/row-patch.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {\n getLogLevel,\n wrapWithProtocolError,\n} from '../../types/error-with-level.ts';\nimport {upstreamSchema, type ShardID} from '../../types/shards.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport {\n cmpVersions,\n cookieToVersion,\n versionToCookie,\n versionToNullableCookie,\n type CVRVersion,\n type DelQueryPatch,\n type NullableCVRVersion,\n type PutQueryPatch,\n type RowID,\n} from './schema/types.ts';\n\nexport type PutRowPatch = {\n type: 'row';\n op: 'put';\n id: RowID;\n contents: JSONObject;\n};\n\nexport type DeleteRowPatch = {\n type: 'row';\n op: 'del';\n id: RowID;\n};\n\nexport type RowPatch = PutRowPatch | DeleteRowPatch;\nexport type ConfigPatch = DelQueryPatch | PutQueryPatch;\n\nexport type Patch = ConfigPatch | RowPatch;\n\nexport type PatchToVersion = {\n patch: Patch;\n toVersion: CVRVersion;\n};\n\nexport interface PokeHandler {\n addPatch(patch: PatchToVersion): Promise<void>;\n cancel(): Promise<void>;\n end(finalVersion: CVRVersion): Promise<void>;\n}\n\nconst NOOP: PokeHandler = {\n addPatch: () => promiseVoid,\n cancel: () => promiseVoid,\n end: () => promiseVoid,\n};\n\n/** Wraps PokeHandlers for multiple clients in a single PokeHandler. */\nexport function startPoke(\n clients: ClientHandler[],\n tentativeVersion: CVRVersion,\n): PokeHandler {\n const pokers = clients.map(c => c.startPoke(tentativeVersion));\n\n // Promise.allSettled() ensures that a failed (e.g. disconnected) client\n // does not prevent other clients from receiving the pokes. However, the\n // rate (per client group) will be limited by the slowest connection.\n return {\n addPatch: async patch => {\n await Promise.allSettled(pokers.map(poker => poker.addPatch(patch)));\n },\n cancel: async () => {\n await Promise.allSettled(pokers.map(poker => poker.cancel()));\n },\n end: async finalVersion => {\n await Promise.allSettled(pokers.map(poker => poker.end(finalVersion)));\n },\n };\n}\n\n// Semi-arbitrary threshold at which poke body parts are flushed.\n// When row size is being computed, that should be used as a threshold instead.\nconst PART_COUNT_FLUSH_THRESHOLD = 100;\n\n/**\n * Handles a single `ViewSyncer` connection.\n */\nexport class ClientHandler {\n readonly #clientGroupID: string;\n readonly clientID: string;\n readonly wsID: string;\n readonly #zeroClientsTable: string;\n readonly #zeroMutationsTable: string;\n readonly #lc: LogContext;\n readonly #downstream: Subscription<Downstream>;\n #baseVersion: NullableCVRVersion;\n\n readonly #pokeTime = getOrCreateLatencyHistogram(\n 'sync',\n 'poke.time',\n 'Time elapsed for each poke transaction. Canceled / noop pokes are excluded.',\n );\n\n readonly #pokeTransactions = getOrCreateCounter(\n 'sync',\n 'poke.transactions',\n 'Count of poke transactions.',\n );\n\n readonly #pokedRows = getOrCreateCounter(\n 'sync',\n 'poke.rows',\n 'Count of poked rows.',\n );\n\n constructor(\n lc: LogContext,\n clientGroupID: string,\n clientID: string,\n wsID: string,\n shard: ShardID,\n baseCookie: string | null,\n downstream: Subscription<Downstream>,\n ) {\n lc.debug?.('new client handler');\n this.#clientGroupID = clientGroupID;\n this.clientID = clientID;\n this.wsID = wsID;\n this.#zeroClientsTable = `${upstreamSchema(shard)}.clients`;\n this.#zeroMutationsTable = `${upstreamSchema(shard)}.mutations`;\n this.#lc = lc;\n this.#downstream = downstream;\n this.#baseVersion = cookieToVersion(baseCookie);\n }\n\n version(): NullableCVRVersion {\n return this.#baseVersion;\n }\n\n async #push(msg: Downstream): Promise<void> {\n const {result} = this.#downstream.push(msg);\n await result;\n }\n\n fail(e: unknown) {\n this.#lc[getLogLevel(e)]?.(\n `view-syncer closing connection with error: ${String(e)}`,\n e,\n );\n this.#downstream.fail(wrapWithProtocolError(e));\n }\n\n close(reason: string) {\n this.#lc.debug?.(`view-syncer closing connection: ${reason}`);\n this.#downstream.cancel();\n }\n\n startPoke(tentativeVersion: CVRVersion): PokeHandler {\n const pokeID = versionToCookie(tentativeVersion);\n const lc = this.#lc.withContext('pokeID', pokeID);\n\n if (cmpVersions(this.#baseVersion, tentativeVersion) >= 0) {\n lc.info?.(`already caught up, not sending poke.`);\n return NOOP;\n }\n\n const baseCookie = versionToNullableCookie(this.#baseVersion);\n const cookie = versionToCookie(tentativeVersion);\n lc.info?.(`starting poke from ${baseCookie} to ${cookie}`);\n\n const start = performance.now();\n\n const pokeStart: PokeStartBody = {pokeID, baseCookie};\n\n let pokeStarted = false;\n let body: PokePartBody | undefined;\n let partCount = 0;\n const ensureBody = async () => {\n if (!pokeStarted) {\n await this.#push(['pokeStart', pokeStart]);\n pokeStarted = true;\n }\n return (body ??= {pokeID});\n };\n const flushBody = async () => {\n if (body) {\n await this.#push(['pokePart', body]);\n body = undefined;\n partCount = 0;\n }\n };\n\n const addPatch = async (patchToVersion: PatchToVersion) => {\n const {patch, toVersion} = patchToVersion;\n if (cmpVersions(toVersion, this.#baseVersion) <= 0) {\n return;\n }\n const body = await ensureBody();\n\n const {type, op} = patch;\n switch (type) {\n case 'query': {\n const patches = patch.clientID\n ? ((body.desiredQueriesPatches ??= {})[patch.clientID] ??= [])\n : (body.gotQueriesPatch ??= []);\n if (op === 'put') {\n patches.push({op, hash: patch.id});\n } else {\n patches.push({op, hash: patch.id});\n }\n break;\n }\n case 'row':\n if (patch.id.table === this.#zeroClientsTable) {\n this.#updateLMIDs((body.lastMutationIDChanges ??= {}), patch);\n } else if (patch.id.table === this.#zeroMutationsTable) {\n const patches = (body.mutationsPatch ??= []);\n if (op === 'put') {\n const row = v.parse(\n ensureSafeJSON(patch.contents),\n mutationRowSchema,\n 'passthrough',\n );\n patches.push({\n op: 'put',\n mutation: {\n id: {\n clientID: row.clientID,\n id: row.mutationID,\n },\n result: row.result,\n },\n });\n } else {\n const {clientID, mutationID} = patch.id.rowKey;\n assert(\n typeof clientID === 'string',\n 'client id must be a string',\n );\n const id = Number(mutationID);\n assert(\n !Number.isNaN(id) && Number.isFinite(id) && id >= 0,\n 'mutation id must be a finite number',\n );\n patches.push({\n op: 'del',\n id: {\n clientID,\n id,\n },\n });\n }\n } else {\n (body.rowsPatch ??= []).push(makeRowPatch(patch));\n }\n break;\n default:\n unreachable(patch);\n }\n\n if (++partCount >= PART_COUNT_FLUSH_THRESHOLD) {\n await flushBody();\n }\n };\n\n return {\n addPatch: async (patchToVersion: PatchToVersion) => {\n try {\n await addPatch(patchToVersion);\n if (patchToVersion.patch.type === 'row') {\n this.#pokedRows.add(1);\n }\n } catch (e) {\n this.#downstream.fail(wrapWithProtocolError(e));\n }\n },\n\n cancel: async () => {\n if (pokeStarted) {\n await this.#push(['pokeEnd', {pokeID, cookie: '', cancel: true}]);\n }\n },\n\n end: async (finalVersion: CVRVersion) => {\n const cookie = versionToCookie(finalVersion);\n if (!pokeStarted) {\n if (cmpVersions(this.#baseVersion, finalVersion) === 0) {\n return; // Nothing changed and nothing was sent.\n }\n await this.#push(['pokeStart', pokeStart]);\n } else if (cmpVersions(this.#baseVersion, finalVersion) >= 0) {\n // Sanity check: If the poke was started, the finalVersion\n // must be > #baseVersion.\n throw new Error(\n `Patches were sent but finalVersion ${finalVersion} is ` +\n `not greater than baseVersion ${this.#baseVersion}`,\n );\n }\n await flushBody();\n await this.#push(['pokeEnd', {pokeID, cookie}]);\n this.#baseVersion = finalVersion;\n\n const elapsed = performance.now() - start;\n this.#pokeTransactions.add(1);\n this.#pokeTime.recordMs(elapsed);\n },\n };\n }\n\n async sendDeleteClients(\n lc: LogContext,\n deletedClientIDs: string[],\n deletedClientGroupIDs: string[],\n ) {\n const deleteClientsBody: Writable<DeleteClientsBody> = {};\n if (deletedClientIDs.length > 0) {\n deleteClientsBody.clientIDs = deletedClientIDs;\n }\n if (deletedClientGroupIDs.length > 0) {\n deleteClientsBody.clientGroupIDs = deletedClientGroupIDs;\n }\n lc.debug?.('sending deleteClients', deleteClientsBody);\n await this.#push(['deleteClients', deleteClientsBody]);\n }\n\n sendQueryTransformApplicationErrors(errors: ErroredQuery[]) {\n void this.#push(['transformError', errors]);\n }\n\n sendQueryTransformFailedError(error: TransformFailedBody) {\n this.fail(new ProtocolError(error));\n }\n\n sendInspectResponse(lc: LogContext, response: InspectDownBody): void {\n lc.debug?.('sending inspect response', response);\n this.#downstream.push(['inspect', response]);\n }\n\n #updateLMIDs(lmids: Record<string, number>, patch: RowPatch) {\n if (patch.op === 'put') {\n const row = ensureSafeJSON(patch.contents);\n const {clientGroupID, clientID, lastMutationID} = v.parse(\n row,\n lmidRowSchema,\n 'passthrough',\n );\n if (clientGroupID !== this.#clientGroupID) {\n this.#lc.error?.(\n `Received clients row for wrong clientGroupID. Ignoring.`,\n clientGroupID,\n );\n } else {\n lmids[clientID] = lastMutationID;\n }\n } else {\n // The 'constrain' and 'del' ops for clients can be ignored.\n patch.op satisfies 'constrain' | 'del';\n }\n }\n}\n\n// Note: The {APP_ID}_{SHARD_ID}.clients table is set up in replicator/initial-sync.ts.\nconst lmidRowSchema = v.object({\n clientGroupID: v.string(),\n clientID: v.string(),\n lastMutationID: v.number(), // Actually returned as a bigint, but converted by ensureSafeJSON().\n});\n\nconst mutationRowSchema = v.object({\n clientGroupID: v.string(),\n clientID: v.string(),\n mutationID: v.number(),\n result: mutationResultSchema,\n});\n\nfunction makeRowPatch(patch: RowPatch): RowPatchOp {\n const {\n op,\n id: {table: tableName, rowKey: id},\n } = patch;\n\n switch (op) {\n case 'put':\n return {\n op: 'put',\n tableName,\n value: v.parse(ensureSafeJSON(patch.contents), rowSchema),\n };\n\n case 'del':\n return {\n op,\n tableName,\n id: v.parse(id, primaryKeyValueRecordSchema),\n };\n\n default:\n unreachable(op);\n }\n}\n\n/**\n * Column values of type INT8 are returned as the `bigint` from the\n * Postgres library. These are converted to `number` if they are within\n * the safe Number range, allowing the protocol to support numbers larger\n * than 32-bits. Values outside of the safe number range (e.g. > 2^53) will\n * result in an Error.\n */\nexport function ensureSafeJSON(row: JSONObject): SafeJSONObject {\n const modified = Object.entries(row)\n .filter(([k, v]) => {\n if (typeof v === 'bigint') {\n if (v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER) {\n return true; // send this entry onto the next map() step.\n }\n throw new Error(`Value of \"${k}\" exceeds safe Number range (${v})`);\n } else if (typeof v === 'object') {\n assertJSONValue(v);\n }\n return false;\n })\n .map(([k, v]) => [k, Number(v)]);\n\n return modified.length\n ? {...row, ...Object.fromEntries(modified)}\n : (row as SafeJSONObject);\n}\n"],"mappings":"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"}
@@ -5,6 +5,24 @@ import { type Auth, type ValidateLegacyJWT } from '../../auth/auth.ts';
5
5
  import type { ZeroConfig } from '../../config/zero-config.ts';
6
6
  import type { ConnectParams } from '../../workers/connect-params.ts';
7
7
  export type ConnectionState = 'provisional' | 'validated';
8
+ /**
9
+ * Normalized user identity shared by live connection state and group auth state.
10
+ * `id: null` means logged out.
11
+ */
12
+ export type UserState = {
13
+ readonly id: string | null;
14
+ };
15
+ /**
16
+ * Delineates the two paths for validating a connection: either server can validate
17
+ * the user's identity and return a definitive userID to trust, or we fall back to
18
+ * trusting the one provided by the client in the incoming query params.
19
+ */
20
+ export type ConnectionValidation = {
21
+ kind: 'client-fallback';
22
+ } | {
23
+ kind: 'server-validated';
24
+ validatedUserID: string | null;
25
+ };
8
26
  /**
9
27
  * Identifies one live websocket for a client slot.
10
28
  */
@@ -14,16 +32,16 @@ export type ConnectionSelector = {
14
32
  };
15
33
  type FetchConfig = ZeroConfig['query'];
16
34
  export type HeaderOptions = {
17
- apiKey?: string | undefined;
18
- customHeaders?: Record<string, string> | undefined;
19
- allowedClientHeaders?: readonly string[] | undefined;
20
- cookie?: string | undefined;
21
- origin?: string | undefined;
35
+ readonly apiKey?: string | undefined;
36
+ readonly customHeaders?: Readonly<Record<string, string>> | undefined;
37
+ readonly allowedClientHeaders?: readonly string[] | undefined;
38
+ readonly cookie?: string | undefined;
39
+ readonly origin?: string | undefined;
22
40
  };
23
41
  export type ConnectionFetchContext = {
24
- url: string | undefined;
25
- allowedUrlPatterns: URLPattern[] | undefined;
26
- headerOptions: HeaderOptions;
42
+ readonly url: string | undefined;
43
+ readonly allowedUrlPatterns: readonly URLPattern[] | undefined;
44
+ readonly headerOptions: HeaderOptions;
27
45
  };
28
46
  /**
29
47
  * A snapshot of one live connection tracked by the manager.
@@ -31,19 +49,19 @@ export type ConnectionFetchContext = {
31
49
  * `revalidateAt` is only populated while the connection is `validated`.
32
50
  */
33
51
  export type ConnectionContext = {
34
- state: ConnectionState;
52
+ readonly state: ConnectionState;
35
53
  readonly clientID: string;
36
54
  readonly wsID: string;
37
- readonly userID: string | undefined;
38
- auth: Auth | undefined;
55
+ readonly user: UserState;
56
+ readonly auth: Auth | undefined;
39
57
  readonly profileID: string | null;
40
58
  readonly baseCookie: string | null;
41
59
  readonly protocolVersion: number;
42
- revision: number;
43
- revalidateAt: number | undefined;
60
+ readonly revision: number;
61
+ readonly revalidateAt: number | undefined;
44
62
  readonly insertionOrder: number;
45
63
  readonly queryContext: ConnectionFetchContext;
46
- readonly pushContext: ConnectionFetchContext;
64
+ readonly mutateContext: ConnectionFetchContext;
47
65
  };
48
66
  /**
49
67
  * Group-scoped auth state shared across the live connections.
@@ -51,22 +69,18 @@ export type ConnectionContext = {
51
69
  * The background connection is the validated connection currently used for
52
70
  * shared background work. Retransform happens on a group level, and uses
53
71
  * the background connection's credential to refetch the latest queries.
54
- *
55
- * Since auth can be pinned to logged-out users, `userID` can be undefined
56
- * even after validation.
57
72
  */
58
73
  export type GroupAuthState = {
59
- userID: string | undefined;
60
- validated: boolean;
61
- backgroundConnection: ConnectionSelector | undefined;
62
- retransformAt: number | undefined;
63
- maintenanceNotBeforeAt: number | undefined;
74
+ readonly pinnedUser: UserState | undefined;
75
+ readonly backgroundConnection: ConnectionSelector | undefined;
76
+ readonly retransformAt: number | undefined;
77
+ readonly maintenanceNotBeforeAt: number | undefined;
64
78
  };
65
79
  export type ConnectionContextManager = {
66
80
  registerConnection(selector: ConnectionSelector, connectParams: ConnectParams, auth?: Auth): Readonly<ConnectionContext>;
67
81
  initConnection(selector: ConnectionSelector, body: InitConnectionBody): Readonly<ConnectionContext>;
68
82
  updateAuth(selector: ConnectionSelector, body: UpdateAuthBody): Promise<Readonly<ConnectionContext>>;
69
- validateConnection(selector: ConnectionSelector, revision: number): Readonly<{
83
+ validateConnection(selector: ConnectionSelector, revision: number, validation: ConnectionValidation): Readonly<{
70
84
  connection: ConnectionContext;
71
85
  group: GroupAuthState;
72
86
  }> | undefined;
@@ -90,10 +104,8 @@ export type ConnectionContextManager = {
90
104
  *
91
105
  * Connections are registered as `provisional`, optionally backfilled with
92
106
  * `initConnection` metadata, and then promoted to `validated` once their
93
- * stored `userID` is confirmed as valid. The manager also tracks which
107
+ * effective `userID` is confirmed as valid. The manager also tracks which
94
108
  * validated connection currently serves as the group's background connection.
95
- *
96
- * This is intentionally side-effect free.
97
109
  */
98
110
  export declare class ConnectionContextManagerImpl implements ConnectionContextManager {
99
111
  #private;
@@ -126,7 +138,7 @@ export declare class ConnectionContextManagerImpl implements ConnectionContextMa
126
138
  * background connection if none is currently available. If the websocket is
127
139
  * gone by the time async validation finishes, this becomes a no-op.
128
140
  */
129
- validateConnection(selector: ConnectionSelector, revision: number): Readonly<{
141
+ validateConnection(selector: ConnectionSelector, revision: number, validation: ConnectionValidation): Readonly<{
130
142
  connection: ConnectionContext;
131
143
  group: GroupAuthState;
132
144
  }> | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"connection-context-manager.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/connection-context-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"connection-context-manager.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/connection-context-manager.ts"],"names":[],"mappings":"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"}