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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/out/shared/src/sorted-entries.d.ts +2 -0
  2. package/out/shared/src/sorted-entries.d.ts.map +1 -0
  3. package/out/shared/src/sorted-entries.js +9 -0
  4. package/out/shared/src/sorted-entries.js.map +1 -0
  5. package/out/zero/package.js +1 -1
  6. package/out/zero/package.js.map +1 -1
  7. package/out/zero-cache/src/auth/auth.d.ts +8 -26
  8. package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
  9. package/out/zero-cache/src/auth/auth.js +57 -82
  10. package/out/zero-cache/src/auth/auth.js.map +1 -1
  11. package/out/zero-cache/src/auth/jwt.d.ts +3 -3
  12. package/out/zero-cache/src/auth/jwt.d.ts.map +1 -1
  13. package/out/zero-cache/src/auth/jwt.js.map +1 -1
  14. package/out/zero-cache/src/config/zero-config.d.ts +22 -2
  15. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  16. package/out/zero-cache/src/config/zero-config.js +21 -0
  17. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  18. package/out/zero-cache/src/custom/fetch.d.ts +2 -9
  19. package/out/zero-cache/src/custom/fetch.d.ts.map +1 -1
  20. package/out/zero-cache/src/custom/fetch.js +9 -4
  21. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  22. package/out/zero-cache/src/custom-queries/transform-query.d.ts +20 -9
  23. package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
  24. package/out/zero-cache/src/custom-queries/transform-query.js +71 -37
  25. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  26. package/out/zero-cache/src/db/transaction-pool.d.ts.map +1 -1
  27. package/out/zero-cache/src/db/transaction-pool.js +3 -0
  28. package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
  29. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  30. package/out/zero-cache/src/server/change-streamer.js +4 -1
  31. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  32. package/out/zero-cache/src/server/inspector-delegate.d.ts +2 -2
  33. package/out/zero-cache/src/server/inspector-delegate.d.ts.map +1 -1
  34. package/out/zero-cache/src/server/inspector-delegate.js +4 -4
  35. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  36. package/out/zero-cache/src/server/reaper.d.ts.map +1 -1
  37. package/out/zero-cache/src/server/reaper.js +4 -1
  38. package/out/zero-cache/src/server/reaper.js.map +1 -1
  39. package/out/zero-cache/src/server/runner/run-worker.js +1 -1
  40. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  41. package/out/zero-cache/src/server/syncer.js +34 -11
  42. package/out/zero-cache/src/server/syncer.js.map +1 -1
  43. package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
  44. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  45. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  46. package/out/zero-cache/src/services/change-source/pg/change-source.js +4 -3
  47. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  48. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +2 -1
  49. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  50. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +7 -5
  51. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  52. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +3 -3
  53. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  54. package/out/zero-cache/src/services/mutagen/pusher.d.ts +20 -20
  55. package/out/zero-cache/src/services/mutagen/pusher.d.ts.map +1 -1
  56. package/out/zero-cache/src/services/mutagen/pusher.js +91 -104
  57. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  58. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +168 -0
  59. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts.map +1 -0
  60. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +385 -0
  61. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -0
  62. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts +2 -3
  63. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts.map +1 -1
  64. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +3 -3
  65. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  66. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +20 -26
  67. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  68. package/out/zero-cache/src/services/view-syncer/view-syncer.js +203 -114
  69. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  70. package/out/zero-cache/src/types/pg-versions.d.ts +3 -0
  71. package/out/zero-cache/src/types/pg-versions.d.ts.map +1 -0
  72. package/out/zero-cache/src/types/pg-versions.js +7 -0
  73. package/out/zero-cache/src/types/pg-versions.js.map +1 -0
  74. package/out/zero-cache/src/workers/connect-params.d.ts +1 -1
  75. package/out/zero-cache/src/workers/connect-params.d.ts.map +1 -1
  76. package/out/zero-cache/src/workers/connect-params.js +1 -1
  77. package/out/zero-cache/src/workers/connect-params.js.map +1 -1
  78. package/out/zero-cache/src/workers/connection.js +4 -4
  79. package/out/zero-cache/src/workers/connection.js.map +1 -1
  80. package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts +2 -1
  81. package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
  82. package/out/zero-cache/src/workers/syncer-ws-message-handler.js +46 -36
  83. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  84. package/out/zero-cache/src/workers/syncer.d.ts +2 -1
  85. package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
  86. package/out/zero-cache/src/workers/syncer.js +53 -26
  87. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  88. package/out/zero-client/src/client/connection.d.ts +4 -4
  89. package/out/zero-client/src/client/connection.d.ts.map +1 -1
  90. package/out/zero-client/src/client/connection.js.map +1 -1
  91. package/out/zero-client/src/client/options.d.ts +34 -5
  92. package/out/zero-client/src/client/options.d.ts.map +1 -1
  93. package/out/zero-client/src/client/options.js.map +1 -1
  94. package/out/zero-client/src/client/version.js +1 -1
  95. package/out/zero-client/src/client/zero.d.ts +4 -3
  96. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  97. package/out/zero-client/src/client/zero.js +33 -11
  98. package/out/zero-client/src/client/zero.js.map +1 -1
  99. package/out/zero-protocol/src/change-desired-queries.d.ts +4 -0
  100. package/out/zero-protocol/src/change-desired-queries.d.ts.map +1 -1
  101. package/out/zero-protocol/src/change-desired-queries.js +4 -1
  102. package/out/zero-protocol/src/change-desired-queries.js.map +1 -1
  103. package/out/zero-protocol/src/connect.d.ts +4 -0
  104. package/out/zero-protocol/src/connect.d.ts.map +1 -1
  105. package/out/zero-protocol/src/connect.js +2 -1
  106. package/out/zero-protocol/src/connect.js.map +1 -1
  107. package/out/zero-protocol/src/protocol-version.d.ts +1 -1
  108. package/out/zero-protocol/src/protocol-version.d.ts.map +1 -1
  109. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  110. package/out/zero-protocol/src/push.d.ts +4 -0
  111. package/out/zero-protocol/src/push.d.ts.map +1 -1
  112. package/out/zero-protocol/src/push.js +2 -1
  113. package/out/zero-protocol/src/push.js.map +1 -1
  114. package/out/zero-protocol/src/up.d.ts +3 -0
  115. package/out/zero-protocol/src/up.d.ts.map +1 -1
  116. package/out/zero-react/src/zero-provider.d.ts.map +1 -1
  117. package/out/zero-react/src/zero-provider.js +11 -5
  118. package/out/zero-react/src/zero-provider.js.map +1 -1
  119. package/out/zero-solid/src/use-zero.d.ts.map +1 -1
  120. package/out/zero-solid/src/use-zero.js +8 -9
  121. package/out/zero-solid/src/use-zero.js.map +1 -1
  122. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"transform-query.js","names":["#shard","#cache","#config","#urlPatterns","#lc"],"sources":["../../../../../zero-cache/src/custom-queries/transform-query.ts"],"sourcesContent":["import {trace} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {startAsyncSpan} from '../../../otel/src/span.ts';\nimport {TimedCache} from '../../../shared/src/cache.ts';\nimport {getErrorMessage} from '../../../shared/src/error.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {\n transformResponseMessageSchema,\n type ErroredQuery,\n type TransformRequestBody,\n type TransformRequestMessage,\n} from '../../../zero-protocol/src/custom-queries.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 TransformFailedBody,\n} from '../../../zero-protocol/src/error.ts';\nimport {hashOfAST} from '../../../zero-protocol/src/query-hash.ts';\nimport type {TransformedAndHashed} from '../auth/read-authorizer.ts';\nimport {\n compileUrlPattern,\n fetchFromAPIServer,\n type HeaderOptions,\n} from '../custom/fetch.ts';\nimport type {CustomQueryRecord} from '../services/view-syncer/schema/types.ts';\nimport type {ShardID} from '../types/shards.ts';\n\nconst tracer = trace.getTracer('custom-query-transformer');\n\n/**\n * Transforms a custom query by calling the user's API server.\n * Caches the transformed queries for 5 seconds to avoid unnecessary API calls.\n *\n * Error responses are not cached as the user may want to retry the query\n * and the error may be transient.\n *\n * The TTL was chosen to be 5 seconds since custom query requests come with\n * a token which itself may have a short TTL (e.g., 10 seconds).\n *\n * Token expiration isn't expected to be exact so this 5 second\n * caching shouldn't cause unexpected behavior. E.g., many JWT libraries\n * implement leeway for expiration checks: https://github.com/panva/jose/blob/main/docs/jwt/verify/interfaces/JWTVerifyOptions.md#clocktolerance\n *\n * The ViewSyncer will call the API server 3-4 times with the exact same queries\n * if we do not cache requests.\n *\n * Caching is safe here because the cache key encodes both\n * the user's cookies and auth token. A user cannot see another user's\n * transformed queries unless they share the same token and cookies.\n */\nexport class CustomQueryTransformer {\n readonly #shard: ShardID;\n readonly #cache: TimedCache<TransformedAndHashed>;\n readonly #config: {\n url: string[];\n forwardCookies: boolean;\n };\n readonly #urlPatterns: URLPattern[];\n readonly #lc: LogContext;\n\n constructor(\n lc: LogContext,\n config: {\n url: string[];\n forwardCookies: boolean;\n },\n shard: ShardID,\n ) {\n this.#config = config;\n this.#shard = shard;\n this.#lc = lc;\n this.#urlPatterns = config.url.map(compileUrlPattern);\n this.#cache = new TimedCache(5000); // 5 seconds cache TTL\n }\n\n async transform(\n headerOptions: HeaderOptions,\n queries: Iterable<CustomQueryRecord>,\n userQueryURL: string | undefined,\n ): Promise<(TransformedAndHashed | ErroredQuery)[] | TransformFailedBody> {\n const request: TransformRequestBody = [];\n const cachedResponses: TransformedAndHashed[] = [];\n\n if (!this.#config.forwardCookies && headerOptions.cookie) {\n headerOptions = {\n ...headerOptions,\n cookie: undefined, // remove cookies if not forwarded\n };\n }\n\n // split queries into cached and uncached\n for (const query of queries) {\n const cacheKey = getCacheKey(headerOptions, query.id);\n const cached = this.#cache.get(cacheKey);\n if (cached) {\n cachedResponses.push(cached);\n } else {\n request.push({\n id: query.id,\n name: query.name,\n args: query.args,\n });\n }\n }\n\n if (request.length === 0) {\n return cachedResponses;\n }\n\n const queryIDs = request.map(r => r.id);\n\n try {\n const transformResponse = await startAsyncSpan(\n tracer,\n 'customQueryTransformer.fetchFromAPIServer',\n () =>\n fetchFromAPIServer(\n transformResponseMessageSchema,\n 'transform',\n this.#lc,\n userQueryURL ??\n must(\n this.#config.url[0],\n 'A ZERO_QUERY_URL must be configured for custom queries',\n ),\n this.#urlPatterns,\n this.#shard,\n headerOptions,\n ['transform', request] satisfies TransformRequestMessage,\n ),\n );\n\n if (transformResponse[0] === 'transformFailed') {\n return transformResponse[1];\n }\n\n const newResponses = transformResponse[1].map(transformed => {\n if ('error' in transformed) {\n return transformed;\n }\n return {\n id: transformed.id,\n transformedAst: transformed.ast,\n transformationHash: hashOfAST(transformed.ast),\n } satisfies TransformedAndHashed;\n });\n\n for (const transformed of newResponses) {\n if ('error' in transformed) {\n // do not cache error responses\n continue;\n }\n const cacheKey = getCacheKey(headerOptions, transformed.id);\n this.#cache.set(cacheKey, transformed);\n }\n\n return [...newResponses, ...cachedResponses];\n } catch (e) {\n if (\n isProtocolError(e) &&\n e.errorBody.kind === ErrorKind.TransformFailed\n ) {\n return {\n ...e.errorBody,\n queryIDs,\n } as const satisfies TransformFailedBody;\n }\n\n return {\n kind: ErrorKind.TransformFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to transform queries: ${getErrorMessage(e)}`,\n queryIDs,\n } as const satisfies TransformFailedBody;\n }\n }\n}\n\nfunction getCacheKey(headerOptions: HeaderOptions, queryID: string) {\n // For custom queries, queryID is a hash of the name + args.\n // the APIKey from headerOptions is static. Not needed for the cache key.\n // The token is used to identify the user and should be included in the cache key.\n return `${headerOptions.token}:${headerOptions.cookie}:${queryID}`;\n}\n"],"mappings":";;;;;;;;;;;;;AA6BA,IAAM,SAAS,MAAM,UAAU,2BAA2B;;;;;;;;;;;;;;;;;;;;;;AAuB1D,IAAa,yBAAb,MAAoC;CAClC;CACA;CACA;CAIA;CACA;CAEA,YACE,IACA,QAIA,OACA;AACA,QAAA,SAAe;AACf,QAAA,QAAc;AACd,QAAA,KAAW;AACX,QAAA,cAAoB,OAAO,IAAI,IAAI,kBAAkB;AACrD,QAAA,QAAc,IAAI,WAAW,IAAK;;CAGpC,MAAM,UACJ,eACA,SACA,cACwE;EACxE,MAAM,UAAgC,EAAE;EACxC,MAAM,kBAA0C,EAAE;AAElD,MAAI,CAAC,MAAA,OAAa,kBAAkB,cAAc,OAChD,iBAAgB;GACd,GAAG;GACH,QAAQ,KAAA;GACT;AAIH,OAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,WAAW,YAAY,eAAe,MAAM,GAAG;GACrD,MAAM,SAAS,MAAA,MAAY,IAAI,SAAS;AACxC,OAAI,OACF,iBAAgB,KAAK,OAAO;OAE5B,SAAQ,KAAK;IACX,IAAI,MAAM;IACV,MAAM,MAAM;IACZ,MAAM,MAAM;IACb,CAAC;;AAIN,MAAI,QAAQ,WAAW,EACrB,QAAO;EAGT,MAAM,WAAW,QAAQ,KAAI,MAAK,EAAE,GAAG;AAEvC,MAAI;GACF,MAAM,oBAAoB,MAAM,eAC9B,QACA,mDAEE,mBACE,gCACA,aACA,MAAA,IACA,gBACE,KACE,MAAA,OAAa,IAAI,IACjB,yDACD,EACH,MAAA,aACA,MAAA,OACA,eACA,CAAC,aAAa,QAAQ,CACvB,CACJ;AAED,OAAI,kBAAkB,OAAO,kBAC3B,QAAO,kBAAkB;GAG3B,MAAM,eAAe,kBAAkB,GAAG,KAAI,gBAAe;AAC3D,QAAI,WAAW,YACb,QAAO;AAET,WAAO;KACL,IAAI,YAAY;KAChB,gBAAgB,YAAY;KAC5B,oBAAoB,UAAU,YAAY,IAAI;KAC/C;KACD;AAEF,QAAK,MAAM,eAAe,cAAc;AACtC,QAAI,WAAW,YAEb;IAEF,MAAM,WAAW,YAAY,eAAe,YAAY,GAAG;AAC3D,UAAA,MAAY,IAAI,UAAU,YAAY;;AAGxC,UAAO,CAAC,GAAG,cAAc,GAAG,gBAAgB;WACrC,GAAG;AACV,OACE,gBAAgB,EAAE,IAClB,EAAE,UAAU,SAAS,kBAErB,QAAO;IACL,GAAG,EAAE;IACL;IACD;AAGH,UAAO;IACL,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,SAAS,gCAAgC,gBAAgB,EAAE;IAC3D;IACD;;;;AAKP,SAAS,YAAY,eAA8B,SAAiB;AAIlE,QAAO,GAAG,cAAc,MAAM,GAAG,cAAc,OAAO,GAAG"}
1
+ {"version":3,"file":"transform-query.js","names":["#shard","#cache","#lc","#requestTransform"],"sources":["../../../../../zero-cache/src/custom-queries/transform-query.ts"],"sourcesContent":["import {trace} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {startAsyncSpan} from '../../../otel/src/span.ts';\nimport {TimedCache} from '../../../shared/src/cache.ts';\nimport {getErrorMessage} from '../../../shared/src/error.ts';\nimport {sortedEntries} from '../../../shared/src/sorted-entries.ts';\nimport {\n transformResponseMessageSchema,\n type ErroredQuery,\n type TransformRequestBody,\n type TransformRequestMessage,\n type TransformResponseBody,\n} from '../../../zero-protocol/src/custom-queries.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 TransformFailedBody,\n} from '../../../zero-protocol/src/error.ts';\nimport {hashOfAST} from '../../../zero-protocol/src/query-hash.ts';\nimport type {TransformedAndHashed} from '../auth/read-authorizer.ts';\nimport {fetchFromAPIServer} from '../custom/fetch.ts';\nimport type {\n ConnectionContext,\n HeaderOptions,\n} from '../services/view-syncer/connection-context-manager.ts';\nimport type {CustomQueryRecord} from '../services/view-syncer/schema/types.ts';\nimport type {ShardID} from '../types/shards.ts';\n\nconst tracer = trace.getTracer('custom-query-transformer');\n\ntype TransformResponse = TransformResponseBody | TransformFailedBody;\nexport type TransformAttempt = {\n result: (TransformedAndHashed | ErroredQuery)[] | TransformFailedBody;\n cached: boolean;\n};\n\n/**\n * Transforms a custom query by calling the user's API server.\n * Caches the transformed queries for 5 seconds to avoid unnecessary API calls.\n *\n * Error responses are not cached as the user may want to retry the query\n * and the error may be transient.\n *\n * The TTL was chosen to be 5 seconds since custom query requests come with\n * a token which itself may have a short TTL (e.g., 10 seconds).\n *\n * Token expiration isn't expected to be exact so this 5 second\n * caching shouldn't cause unexpected behavior. E.g., many JWT libraries\n * implement leeway for expiration checks: https://github.com/panva/jose/blob/main/docs/jwt/verify/interfaces/JWTVerifyOptions.md#clocktolerance\n *\n * The ViewSyncer will call the API server 3-4 times with the exact same queries\n * if we do not cache requests.\n *\n * Caching is safe here because the cache key encodes the effective request\n * identity used for `/query`: auth, userID, forwarded cookies, origin,\n * custom headers, target URL, and the query hash itself.\n */\nexport class CustomQueryTransformer {\n readonly #shard: ShardID;\n readonly #cache: TimedCache<TransformedAndHashed>;\n readonly #lc: LogContext;\n\n constructor(lc: LogContext, shard: ShardID) {\n this.#shard = shard;\n this.#lc = lc;\n this.#cache = new TimedCache(5000); // 5 seconds cache TTL\n }\n\n /**\n * Forces the empty `/query` validation request used by auth maintenance.\n *\n * This stays separate from `transform()` because `transform([], ...)`\n * short-circuits locally and never hits the API server, while validation\n * still needs to make the request so auth failures are surfaced.\n * Successful validation is intentionally opaque because callers only care\n * whether the request succeeded or failed.\n */\n async validate(\n ctx: ConnectionContext,\n ): Promise<TransformFailedBody | undefined> {\n const response = await this.#requestTransform(ctx, [], 'validate');\n\n return Array.isArray(response) ? undefined : response;\n }\n\n async transform(\n ctx: ConnectionContext,\n queries: Iterable<CustomQueryRecord>,\n ): Promise<TransformAttempt> {\n const request: TransformRequestBody = [];\n const cachedResponses: TransformedAndHashed[] = [];\n\n // split queries into cached and uncached\n for (const query of queries) {\n const cacheKey = getCacheKey(ctx, query.id);\n const cached = this.#cache.get(cacheKey);\n if (cached) {\n cachedResponses.push(cached);\n } else {\n request.push({\n id: query.id,\n name: query.name,\n args: query.args,\n });\n }\n }\n\n let cached = true;\n\n if (request.length === 0) {\n return {\n result: cachedResponses,\n cached: true,\n };\n } else {\n // we are hitting the server with at least one uncached query\n cached = false;\n }\n\n const response = await this.#requestTransform(ctx, request, 'transform');\n if (!Array.isArray(response)) {\n return {\n result: response,\n cached,\n };\n }\n\n const newResponses = response.map(transformed => {\n if ('error' in transformed) {\n return transformed;\n }\n return {\n id: transformed.id,\n transformedAst: transformed.ast,\n transformationHash: hashOfAST(transformed.ast),\n } satisfies TransformedAndHashed;\n });\n\n for (const transformed of newResponses) {\n if ('error' in transformed) {\n // do not cache error responses\n continue;\n }\n const cacheKey = getCacheKey(ctx, transformed.id);\n this.#cache.set(cacheKey, transformed);\n }\n\n return {\n result: [...newResponses, ...cachedResponses],\n cached,\n };\n }\n\n async #requestTransform(\n ctx: ConnectionContext,\n request: TransformRequestBody,\n operation: 'validate' | 'transform',\n ): Promise<TransformResponse> {\n const queryIDs = request.map(({id}) => id);\n\n try {\n const transformResponse = await startAsyncSpan(\n tracer,\n 'customQueryTransformer.fetchFromAPIServer',\n () =>\n fetchFromAPIServer(\n transformResponseMessageSchema,\n 'transform',\n this.#lc,\n ctx,\n this.#shard,\n ['transform', request] satisfies TransformRequestMessage,\n ),\n );\n\n return transformResponse[1];\n } catch (e) {\n if (\n isProtocolError(e) &&\n e.errorBody.kind === ErrorKind.TransformFailed\n ) {\n return {\n ...e.errorBody,\n queryIDs,\n } as const satisfies TransformFailedBody;\n }\n\n return {\n kind: ErrorKind.TransformFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to ${operation} queries: ${getErrorMessage(e)}`,\n queryIDs,\n } as const satisfies TransformFailedBody;\n }\n }\n}\n\nfunction getCacheKey(ctx: ConnectionContext, queryID: string) {\n // For custom queries, queryID is a hash of the name + args.\n // The apiKey is static for a given transformer instance.\n return JSON.stringify({\n queryID,\n token: ctx.auth?.raw,\n cookie: ctx.queryContext.headerOptions.cookie,\n origin: ctx.queryContext.headerOptions.origin,\n userID: ctx.userID,\n url: ctx.queryContext.url,\n customHeaders: normalizedForwardedHeaders(ctx.queryContext.headerOptions),\n });\n}\n\nfunction normalizedForwardedHeaders(headerOptions: HeaderOptions) {\n const {allowedClientHeaders, customHeaders} = headerOptions;\n if (\n !customHeaders ||\n !allowedClientHeaders ||\n allowedClientHeaders.length === 0\n ) {\n return undefined;\n }\n\n const allowedHeaders = new Set(\n allowedClientHeaders.map(header => header.toLowerCase()),\n );\n const forwardedHeaders = sortedEntries(customHeaders).filter(([header]) =>\n allowedHeaders.has(header.toLowerCase()),\n );\n\n return forwardedHeaders.length === 0\n ? undefined\n : JSON.stringify(forwardedHeaders);\n}\n"],"mappings":";;;;;;;;;;;;;AA8BA,IAAM,SAAS,MAAM,UAAU,2BAA2B;;;;;;;;;;;;;;;;;;;;;;AA6B1D,IAAa,yBAAb,MAAoC;CAClC;CACA;CACA;CAEA,YAAY,IAAgB,OAAgB;AAC1C,QAAA,QAAc;AACd,QAAA,KAAW;AACX,QAAA,QAAc,IAAI,WAAW,IAAK;;;;;;;;;;;CAYpC,MAAM,SACJ,KAC0C;EAC1C,MAAM,WAAW,MAAM,MAAA,iBAAuB,KAAK,EAAE,EAAE,WAAW;AAElE,SAAO,MAAM,QAAQ,SAAS,GAAG,KAAA,IAAY;;CAG/C,MAAM,UACJ,KACA,SAC2B;EAC3B,MAAM,UAAgC,EAAE;EACxC,MAAM,kBAA0C,EAAE;AAGlD,OAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,WAAW,YAAY,KAAK,MAAM,GAAG;GAC3C,MAAM,SAAS,MAAA,MAAY,IAAI,SAAS;AACxC,OAAI,OACF,iBAAgB,KAAK,OAAO;OAE5B,SAAQ,KAAK;IACX,IAAI,MAAM;IACV,MAAM,MAAM;IACZ,MAAM,MAAM;IACb,CAAC;;EAIN,IAAI,SAAS;AAEb,MAAI,QAAQ,WAAW,EACrB,QAAO;GACL,QAAQ;GACR,QAAQ;GACT;MAGD,UAAS;EAGX,MAAM,WAAW,MAAM,MAAA,iBAAuB,KAAK,SAAS,YAAY;AACxE,MAAI,CAAC,MAAM,QAAQ,SAAS,CAC1B,QAAO;GACL,QAAQ;GACR;GACD;EAGH,MAAM,eAAe,SAAS,KAAI,gBAAe;AAC/C,OAAI,WAAW,YACb,QAAO;AAET,UAAO;IACL,IAAI,YAAY;IAChB,gBAAgB,YAAY;IAC5B,oBAAoB,UAAU,YAAY,IAAI;IAC/C;IACD;AAEF,OAAK,MAAM,eAAe,cAAc;AACtC,OAAI,WAAW,YAEb;GAEF,MAAM,WAAW,YAAY,KAAK,YAAY,GAAG;AACjD,SAAA,MAAY,IAAI,UAAU,YAAY;;AAGxC,SAAO;GACL,QAAQ,CAAC,GAAG,cAAc,GAAG,gBAAgB;GAC7C;GACD;;CAGH,OAAA,iBACE,KACA,SACA,WAC4B;EAC5B,MAAM,WAAW,QAAQ,KAAK,EAAC,SAAQ,GAAG;AAE1C,MAAI;AAeF,WAd0B,MAAM,eAC9B,QACA,mDAEE,mBACE,gCACA,aACA,MAAA,IACA,KACA,MAAA,OACA,CAAC,aAAa,QAAQ,CACvB,CACJ,EAEwB;WAClB,GAAG;AACV,OACE,gBAAgB,EAAE,IAClB,EAAE,UAAU,SAAS,kBAErB,QAAO;IACL,GAAG,EAAE;IACL;IACD;AAGH,UAAO;IACL,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,SAAS,aAAa,UAAU,YAAY,gBAAgB,EAAE;IAC9D;IACD;;;;AAKP,SAAS,YAAY,KAAwB,SAAiB;AAG5D,QAAO,KAAK,UAAU;EACpB;EACA,OAAO,IAAI,MAAM;EACjB,QAAQ,IAAI,aAAa,cAAc;EACvC,QAAQ,IAAI,aAAa,cAAc;EACvC,QAAQ,IAAI;EACZ,KAAK,IAAI,aAAa;EACtB,eAAe,2BAA2B,IAAI,aAAa,cAAc;EAC1E,CAAC;;AAGJ,SAAS,2BAA2B,eAA8B;CAChE,MAAM,EAAC,sBAAsB,kBAAiB;AAC9C,KACE,CAAC,iBACD,CAAC,wBACD,qBAAqB,WAAW,EAEhC;CAGF,MAAM,iBAAiB,IAAI,IACzB,qBAAqB,KAAI,WAAU,OAAO,aAAa,CAAC,CACzD;CACD,MAAM,mBAAmB,cAAc,cAAc,CAAC,QAAQ,CAAC,YAC7D,eAAe,IAAI,OAAO,aAAa,CAAC,CACzC;AAED,QAAO,iBAAiB,WAAW,IAC/B,KAAA,IACA,KAAK,UAAU,iBAAiB"}
@@ -1 +1 @@
1
- {"version":3,"file":"transaction-pool.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/db/transaction-pool.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transaction-pool.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/db/transaction-pool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AAErC,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,6BAA6B,CAAC;AAGtD,OAAO,EAAC,KAAK,UAAU,EAAE,KAAK,mBAAmB,EAAC,MAAM,gBAAgB,CAAC;AACzE,OAAO,KAAK,KAAK,IAAI,MAAM,gBAAgB,CAAC;AAG5C,KAAK,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;AAE9B,KAAK,YAAY,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAEtC,MAAM,MAAM,SAAS,GACjB,QAAQ,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,GAAG,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAChE,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC;AAE1C;;;;;GAKG;AACH,MAAM,MAAM,IAAI,GAAG,CACjB,EAAE,EAAE,mBAAmB,EACvB,EAAE,EAAE,UAAU,KACX,YAAY,CAAC,SAAS,EAAE,CAAC,CAAC;AAE/B;;;;GAIG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CACxB,EAAE,EAAE,mBAAmB,EACvB,EAAE,EAAE,UAAU,KACX,YAAY,CAAC,CAAC,CAAC,CAAC;AAErB;;;;;;;GAOG;AACH,qBAAa,eAAe;;IAkB1B;;;;;;;;;;;;;;;OAeG;gBAED,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,IAAI,EACV,IAAI,CAAC,EAAE,IAAI,EACX,OAAO,CAAC,EAAE,IAAI,EACd,cAAc,SAAI,EAClB,UAAU,SAAiB,EAC3B,YAAY,eAAgB;IAkB9B;;;OAGG;IACH,GAAG,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI;IASzB;;;;;;OAMG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;iCAIf,MAAM,SAAS,MAAM;;IAKlD;;;;;;;;;;;;;;;;;;;OAmBG;IACG,IAAI;IAuHV;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA+DlC;;;;;OAKG;IACH,eAAe,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAsDrD;;;OAGG;IACH,KAAK;IAIL;;;OAGG;IACH,OAAO;IASP;;;;;;;;;;;;;;;;;OAiBG;IAEH,GAAG,CAAC,KAAK,SAAI;IAQb;;OAEG;IACH,KAAK,CAAC,KAAK,SAAI;IAYf,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,IAAI,CAAC,GAAG,EAAE,OAAO;CAelB;AAED,KAAK,wBAAwB,GAAG;IAC9B;;;;;OAKG;IACH,cAAc,EAAE,IAAI,CAAC;IAErB;;;;;OAKG;IACH,aAAa,EAAE,IAAI,CAAC;IAEpB;;;;OAIG;IACH,WAAW,EAAE,IAAI,CAAC;IAElB,qCAAqC;IACrC,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7B,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,wBAAwB,CAoDhE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,IAAI;IAChC,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,IAAI,CAAC;IACd,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7B,CAqDA;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG;IAClD,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB,CAYA;AAED;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,KAAK,CAAC,EAAE,OAAO;CAI5B;AAgFD,KAAK,WAAW,GAAG;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,IAAI,GAAG,MAAM,CAAC;CACrB,CAAC;AAEF,KAAK,YAAY,GAAG;IAClB,iBAAiB,EAAE,WAAW,CAAC;IAC/B,eAAe,EAAE,WAAW,CAAC;CAC9B,CAAC;AAGF,eAAO,MAAM,aAAa,EAAE,YAS3B,CAAC"}
@@ -103,6 +103,9 @@ var TransactionPool = class {
103
103
  await Promise.all(this.#workers);
104
104
  if (numWorkers < this.#workers.length) await Promise.all(this.#workers);
105
105
  this.#lc.debug?.("transaction pool done");
106
+ const elapsed = performance.now() - this.#start;
107
+ if (elapsed > 6e4) if (this.#stmts > 0) this.#lc.warn?.(`finished long transaction with ${this.#stmts} statements (${elapsed.toFixed(3)} ms)`);
108
+ else this.#lc.warn?.(`finished long read transaction (${elapsed.toFixed(3)} ms)`);
106
109
  }
107
110
  #addWorker(db) {
108
111
  const id = this.#workers.length + 1;
@@ -1 +1 @@
1
- {"version":3,"file":"transaction-pool.js","names":["#mode","#init","#cleanup","#tasks","#workers","#initialWorkers","#maxWorkers","#timeoutTask","#lc","#stmtRunner","#numWorkers","#db","#addWorker","#numWorking","#failure","#done","#process","#start","#stmts","#readRunner","#refCount"],"sources":["../../../../../zero-cache/src/db/transaction-pool.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {type Resolver, resolver} from '@rocicorp/resolver';\nimport type postgres from 'postgres';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport type {Enum} from '../../../shared/src/enum.ts';\nimport {Queue} from '../../../shared/src/queue.ts';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {type PostgresDB, type PostgresTransaction} from '../types/pg.ts';\nimport type * as Mode from './mode-enum.ts';\nimport {runTx} from './run-transaction.ts';\n\ntype Mode = Enum<typeof Mode>;\n\ntype MaybePromise<T> = Promise<T> | T;\n\nexport type Statement =\n | postgres.PendingQuery<(postgres.Row & Iterable<postgres.Row>)[]>\n | postgres.PendingQuery<postgres.Row[]>;\n\n/**\n * A {@link Task} is logic run from within a transaction in a {@link TransactionPool}.\n * It returns a list of `Statements` that the transaction executes asynchronously and\n * awaits when it receives the 'done' signal.\n *\n */\nexport type Task = (\n tx: PostgresTransaction,\n lc: LogContext,\n) => MaybePromise<Statement[]>;\n\n/**\n * A {@link ReadTask} is run from within a transaction, but unlike a {@link Task},\n * the results of a ReadTask are opaque to the TransactionPool and returned to the\n * caller of {@link TransactionPool.processReadTask}.\n */\nexport type ReadTask<T> = (\n tx: PostgresTransaction,\n lc: LogContext,\n) => MaybePromise<T>;\n\n/**\n * A TransactionPool is a pool of one or more {@link postgres.TransactionSql}\n * objects that participate in processing a dynamic queue of tasks.\n *\n * This can be used for serializing a set of tasks that arrive asynchronously\n * to a single transaction (for writing) or performing parallel reads across\n * multiple connections at the same snapshot (e.g. read only snapshot transactions).\n */\nexport class TransactionPool {\n #lc: LogContext;\n readonly #mode: Mode;\n readonly #init: TaskRunner | undefined;\n readonly #cleanup: TaskRunner | undefined;\n readonly #tasks = new Queue<TaskRunner | Error | 'done'>();\n readonly #workers: Promise<unknown>[] = [];\n readonly #initialWorkers: number;\n readonly #maxWorkers: number;\n readonly #timeoutTask: TimeoutTasks;\n #numWorkers: number;\n #numWorking = 0;\n #db: PostgresDB | undefined; // set when running. stored to allow adaptive pool sizing.\n\n #refCount = 1;\n #done = false;\n #failure: Error | undefined;\n\n /**\n * @param init A {@link Task} that is run in each Transaction before it begins\n * processing general tasks. This can be used to to set the transaction\n * mode, export/set snapshots, etc. This will be run even if\n * {@link fail} has been called on the pool.\n * @param cleanup A {@link Task} that is run in each Transaction before it closes.\n * This will be run even if {@link fail} has been called, or if a\n * preceding Task threw an Error.\n * @param initialWorkers The initial number of transaction workers to process tasks.\n * This is the steady state number of workers that will be kept\n * alive if the TransactionPool is long lived.\n * This must be greater than 0. Defaults to 1.\n * @param maxWorkers When specified, allows the pool to grow to `maxWorkers`. This\n * must be greater than or equal to `initialWorkers`. On-demand\n * workers will be shut down after an idle timeout of 5 seconds.\n */\n constructor(\n lc: LogContext,\n mode: Mode,\n init?: Task,\n cleanup?: Task,\n initialWorkers = 1,\n maxWorkers = initialWorkers,\n timeoutTasks = TIMEOUT_TASKS, // Overridden for tests.\n ) {\n assert(initialWorkers > 0, 'initialWorkers must be positive');\n assert(\n maxWorkers >= initialWorkers,\n 'maxWorkers must be >= initialWorkers',\n );\n\n this.#lc = lc;\n this.#mode = mode;\n this.#init = init ? this.#stmtRunner(init) : undefined;\n this.#cleanup = cleanup ? this.#stmtRunner(cleanup) : undefined;\n this.#initialWorkers = initialWorkers;\n this.#numWorkers = initialWorkers;\n this.#maxWorkers = maxWorkers;\n this.#timeoutTask = timeoutTasks;\n }\n\n /**\n * Starts the pool of workers to process Tasks with transactions opened from the\n * specified {@link db}.\n */\n run(db: PostgresDB): this {\n assert(!this.#db, 'already running');\n this.#db = db;\n for (let i = 0; i < this.#numWorkers; i++) {\n this.#addWorker(db);\n }\n return this;\n }\n\n /**\n * Adds context parameters to internal LogContext. This is useful for context values that\n * are not known when the TransactionPool is constructed (e.g. determined after a database\n * call when the pool is running).\n *\n * Returns an object that can be used to add more parameters.\n */\n addLoggingContext(key: string, value: string) {\n this.#lc = this.#lc.withContext(key, value);\n\n return {\n addLoggingContext: (key: string, value: string) =>\n this.addLoggingContext(key, value),\n };\n }\n\n /**\n * Returns a promise that:\n *\n * * resolves after {@link setDone} has been called (or the the pool as been {@link unref}ed\n * to a 0 ref count), once all added tasks have been processed and all transactions have been\n * committed or closed.\n *\n * * rejects if processing was aborted with {@link fail} or if processing any of\n * the tasks resulted in an error. All uncommitted transactions will have been\n * rolled back.\n *\n * Note that partial failures are possible if processing writes with multiple workers\n * (e.g. `setDone` is called, allowing some workers to commit, after which other\n * workers encounter errors). Using a TransactionPool in this manner does not make\n * sense in terms of transactional semantics, and is thus not recommended.\n *\n * For reads, however, multiple workers is useful for performing parallel reads\n * at the same snapshot. See {@link synchronizedSnapshots} for an example.\n * Resolves or rejects when all workers are done or failed.\n */\n async done() {\n const numWorkers = this.#workers.length;\n await Promise.all(this.#workers);\n\n if (numWorkers < this.#workers.length) {\n // If workers were added after the initial set, they must be awaited to ensure\n // that the results (i.e. rejections) of all workers are accounted for. This only\n // needs to be re-done once, because the fact that the first `await` completed\n // guarantees that the pool is in a terminal state and no new workers can be added.\n await Promise.all(this.#workers);\n }\n this.#lc.debug?.('transaction pool done');\n }\n\n #addWorker(db: PostgresDB) {\n const id = this.#workers.length + 1;\n const lc = this.#lc.withContext('tx', id);\n\n const tt: TimeoutTask =\n this.#workers.length < this.#initialWorkers\n ? this.#timeoutTask.forInitialWorkers\n : this.#timeoutTask.forExtraWorkers;\n const {timeoutMs} = tt;\n const timeoutTask = tt.task === 'done' ? 'done' : this.#stmtRunner(tt.task);\n\n const worker = async (tx: PostgresTransaction) => {\n const start = performance.now();\n try {\n lc.debug?.('started transaction');\n\n let last: Promise<void> = promiseVoid;\n\n const executeTask = async (runner: TaskRunner) => {\n runner !== this.#init && this.#numWorking++;\n const {pending} = await runner.run(tx, lc, () => {\n runner !== this.#init && this.#numWorking--;\n });\n last = pending ?? last;\n };\n\n let task: TaskRunner | Error | 'done' =\n this.#init ?? (await this.#tasks.dequeue(timeoutTask, timeoutMs));\n\n try {\n while (task !== 'done') {\n if (\n task instanceof Error ||\n (task !== this.#init && this.#failure)\n ) {\n throw this.#failure ?? task;\n }\n await executeTask(task);\n\n // await the next task.\n task = await this.#tasks.dequeue(timeoutTask, timeoutMs);\n }\n } finally {\n // Execute the cleanup task even on failure.\n if (this.#cleanup) {\n await executeTask(this.#cleanup);\n }\n }\n\n const elapsed = performance.now() - start;\n lc.debug?.(`closing transaction (${elapsed.toFixed(3)} ms)`);\n // Given the semantics of a Postgres transaction, the last statement\n // will only succeed if all of the preceding statements succeeded.\n return last;\n } catch (e) {\n if (e !== this.#failure) {\n this.fail(e); // A failure in any worker should fail the pool.\n }\n throw e;\n }\n };\n\n const workerTx = runTx(db, worker, {mode: this.#mode})\n .catch(e => {\n if (e instanceof RollbackSignal) {\n // A RollbackSignal is used to gracefully rollback the postgres.js\n // transaction block. It should not be thrown up to the application.\n lc.debug?.('aborted transaction');\n } else {\n throw e;\n }\n })\n .finally(() => this.#numWorkers--);\n\n // Attach a rejection handler immediately to prevent unhandledRejections.\n // The application will handle errors when it awaits processReadTask()\n // or done().\n workerTx.catch(() => {});\n\n this.#workers.push(workerTx);\n\n // After adding the worker, enqueue a terminal signal if we are in either of the\n // terminal states (both of which prevent more tasks from being enqueued), to ensure\n // that the added worker eventually exits.\n if (this.#done) {\n this.#tasks.enqueue('done');\n }\n if (this.#failure) {\n this.#tasks.enqueue(this.#failure);\n }\n }\n\n /**\n * Processes the statements produced by the specified {@link Task},\n * returning a Promise that resolves when the statements are either processed\n * by the database or rejected.\n *\n * Note that statement failures will result in failing the entire\n * TransactionPool (per transaction semantics). However, the returned Promise\n * itself will resolve rather than reject. As such, it is fine to ignore\n * returned Promises in order to pipeline requests to the database. It is\n * recommended to occasionally await them (e.g. after some threshold) in\n * order to avoid memory blowup in the case of database slowness.\n */\n process(task: Task): Promise<void> {\n const r = resolver<void>();\n this.#process(this.#stmtRunner(task, r));\n return r.promise;\n }\n\n readonly #start = performance.now();\n #stmts = 0;\n\n /**\n * Implements the semantics specified in {@link process()}.\n *\n * Specifically:\n * * `freeWorker()` is called as soon as the statements are produced,\n * allowing them to be pipelined to the database.\n * * Statement errors result in failing the transaction pool.\n * * The client-supplied Resolver resolves on success or failure;\n * it is never rejected.\n */\n #stmtRunner(task: Task, r: {resolve: () => void} = resolver()): TaskRunner {\n return {\n run: async (tx, lc, freeWorker) => {\n let stmts: Statement[];\n try {\n stmts = await task(tx, lc);\n } catch (e) {\n r.resolve();\n throw e;\n } finally {\n freeWorker();\n }\n\n if (stmts.length === 0) {\n r.resolve();\n return {pending: null};\n }\n\n // Execute the statements (i.e. send to the db) immediately.\n // The last result is returned for the worker to await before\n // closing the transaction.\n const last = stmts.reduce(\n (_, stmt) =>\n stmt\n .execute()\n .then(() => {\n if (++this.#stmts % 1000 === 0) {\n const log = this.#stmts % 10000 === 0 ? 'info' : 'debug';\n const q = stmt as unknown as Query;\n lc[log]?.(\n `executed ${this.#stmts}th statement (${(performance.now() - this.#start).toFixed(3)} ms)`,\n {statement: q.string},\n );\n }\n })\n .catch(e => this.fail(e)),\n promiseVoid,\n );\n return {pending: last.then(r.resolve)};\n },\n rejected: r.resolve,\n };\n }\n\n /**\n * Processes and returns the result of executing the {@link ReadTask} from\n * within the transaction. An error thrown by the task will result in\n * rejecting the returned Promise, but will not affect the transaction pool\n * itself.\n */\n processReadTask<T>(readTask: ReadTask<T>): Promise<T> {\n const r = resolver<T>();\n this.#process(this.#readRunner(readTask, r));\n return r.promise;\n }\n\n /**\n * Implements the semantics specified in {@link processReadTask()}.\n *\n * Specifically:\n * * `freeWorker()` is called as soon as the result is produced,\n * before resolving the client-supplied Resolver.\n * * Errors result in rejecting the client-supplied Resolver but\n * do not affect transaction pool.\n */\n #readRunner<T>(readTask: ReadTask<T>, r: Resolver<T>): TaskRunner {\n return {\n run: async (tx, lc, freeWorker) => {\n let result: T;\n try {\n result = await readTask(tx, lc);\n freeWorker();\n r.resolve(result);\n } catch (e) {\n freeWorker();\n r.reject(e);\n }\n return {pending: null};\n },\n rejected: r.reject,\n };\n }\n\n #process(runner: TaskRunner): void {\n assert(!this.#done, 'already set done');\n if (this.#failure) {\n runner.rejected(this.#failure);\n return;\n }\n\n this.#tasks.enqueue(runner);\n\n // Check if the pool size can and should be increased.\n if (this.#numWorkers < this.#maxWorkers) {\n const outstanding = this.#tasks.size();\n\n if (outstanding > this.#numWorkers - this.#numWorking) {\n this.#db && this.#addWorker(this.#db);\n this.#numWorkers++;\n this.#lc.debug?.(`Increased pool size to ${this.#numWorkers}`);\n }\n }\n }\n\n /**\n * Ends all workers with a ROLLBACK. Throws if the pool is already done\n * or aborted.\n */\n abort() {\n this.fail(new RollbackSignal());\n }\n\n /**\n * Signals to all workers to end their transaction once all pending tasks have\n * been completed. Throws if the pool is already done or aborted.\n */\n setDone() {\n assert(!this.#done, 'already set done');\n this.#done = true;\n\n for (let i = 0; i < this.#numWorkers; i++) {\n this.#tasks.enqueue('done');\n }\n }\n\n /**\n * An alternative to explicitly calling {@link setDone}, `ref()` increments an internal reference\n * count, and {@link unref} decrements it. When the reference count reaches 0, {@link setDone} is\n * automatically called. A TransactionPool is initialized with a reference count of 1.\n *\n * `ref()` should be called before sharing the pool with another component, and only after the\n * pool has been started with {@link run()}. It must not be called on a TransactionPool that is\n * already done (either via {@link unref()} or {@link setDone()}. (Doing so indicates a logical\n * error in the code.)\n *\n * It follows that:\n * * The creator of the TransactionPool is responsible for running it.\n * * The TransactionPool should be ref'ed before being sharing.\n * * The receiver of the TransactionPool is only responsible for unref'ing it.\n *\n * On the other hand, a transaction pool that fails with a runtime error can still be ref'ed;\n * attempts to use the pool will result in the runtime error as expected.\n */\n // TODO: Get rid of the ref-counting stuff. It's no longer needed.\n ref(count = 1) {\n assert(\n this.#db !== undefined && !this.#done,\n `Cannot ref() a TransactionPool that is not running`,\n );\n this.#refCount += count;\n }\n\n /**\n * Decrements the internal reference count, automatically invoking {@link setDone} when it reaches 0.\n */\n unref(count = 1) {\n assert(\n count <= this.#refCount,\n () => `Cannot unref ${count} when refCount is ${this.#refCount}`,\n );\n\n this.#refCount -= count;\n if (this.#refCount === 0) {\n this.setDone();\n }\n }\n\n isRunning(): boolean {\n return this.#db !== undefined && !this.#done && this.#failure === undefined;\n }\n\n /**\n * Signals all workers to fail their transactions with the given {@link err}.\n */\n fail(err: unknown) {\n if (!this.#failure) {\n this.#failure = ensureError(err); // Fail fast: this is checked in the worker loop.\n // Logged for informational purposes. It is the responsibility of\n // higher level logic to classify and handle the exception.\n const level =\n this.#failure instanceof ControlFlowError ? 'debug' : 'info';\n this.#lc[level]?.(this.#failure);\n\n for (let i = 0; i < this.#numWorkers; i++) {\n // Enqueue the Error to terminate any workers waiting for tasks.\n this.#tasks.enqueue(this.#failure);\n }\n }\n }\n}\n\ntype SynchronizeSnapshotTasks = {\n /**\n * The `init` Task for the TransactionPool from which the snapshot originates.\n * The pool must have Mode.SERIALIZABLE, and will be set to READ ONLY by the\n * `exportSnapshot` init task. If the TransactionPool has multiple workers, the\n * first worker will export a snapshot that the others set.\n */\n exportSnapshot: Task;\n\n /**\n * The `cleanup` Task for the TransactionPool from which the snapshot\n * originates. This Task will wait for the follower pool to `setSnapshot`\n * to ensure that the snapshot is successfully shared before the originating\n * transaction is closed.\n */\n cleanupExport: Task;\n\n /**\n * The `init` Task for the TransactionPool in which workers will\n * consequently see the same snapshot as that of the first pool. The pool\n * must have Mode.SERIALIZABLE, and will have the ability to perform writes.\n */\n setSnapshot: Task;\n\n /** The ID of the shared snapshot. */\n snapshotID: Promise<string>;\n};\n\n/**\n * Init Tasks for Postgres snapshot synchronization across transactions.\n *\n * https://www.postgresql.org/docs/9.3/functions-admin.html#:~:text=Snapshot%20Synchronization%20Functions,identical%20content%20in%20the%20database.\n */\nexport function synchronizedSnapshots(): SynchronizeSnapshotTasks {\n const {\n promise: snapshotExported,\n resolve: exportSnapshot,\n reject: failExport,\n } = resolver<string>();\n\n const {\n promise: snapshotCaptured,\n resolve: captureSnapshot,\n reject: failCapture,\n } = resolver<unknown>();\n\n // Set by the first worker to run its initTask, who becomes responsible for\n // exporting the snapshot. TODO: Plumb the workerNum and use that instead.\n let firstWorkerRun = false;\n\n // Note: Neither init task should `await`, as processing in each pool can proceed\n // as soon as the statements have been sent to the db. However, the `cleanupExport`\n // task must `await` the result of `setSnapshot` to ensure that exporting transaction\n // does not close before the snapshot has been captured.\n return {\n exportSnapshot: tx => {\n if (!firstWorkerRun) {\n firstWorkerRun = true;\n const stmt =\n tx`SELECT pg_export_snapshot() AS snapshot; SET TRANSACTION READ ONLY;`.simple();\n // Intercept the promise to propagate the information to `snapshotExported`.\n stmt.then(result => exportSnapshot(result[0].snapshot), failExport);\n return [stmt]; // Also return the stmt so that it gets awaited (and errors handled).\n }\n return snapshotExported.then(snapshotID => [\n tx.unsafe(`SET TRANSACTION SNAPSHOT '${snapshotID}'`),\n tx`SET TRANSACTION READ ONLY`.simple(),\n ]);\n },\n\n setSnapshot: tx =>\n snapshotExported.then(snapshotID => {\n const stmt = tx.unsafe(`SET TRANSACTION SNAPSHOT '${snapshotID}'`);\n // Intercept the promise to propagate the information to `cleanupExport`.\n stmt.then(captureSnapshot, failCapture);\n return [stmt];\n }),\n\n cleanupExport: async () => {\n await snapshotCaptured;\n return [];\n },\n\n snapshotID: snapshotExported,\n };\n}\n\n/**\n * Returns `init` and `cleanup` {@link Task}s for a TransactionPool that ensure its workers\n * share a single view of the database. This is used for View Notifier and View Syncer logic\n * that allows multiple entities to perform parallel reads on the same snapshot of the database.\n */\nexport function sharedSnapshot(): {\n init: Task;\n cleanup: Task;\n snapshotID: Promise<string>;\n} {\n const {\n promise: snapshotExported,\n resolve: exportSnapshot,\n reject: failExport,\n } = resolver<string>();\n\n // Set by the first worker to run its initTask, who becomes responsible for\n // exporting the snapshot.\n let firstWorkerRun = false;\n\n // The LogContext of the exporting worker, used to identify its cleanup call.\n // Each worker receives a unique lc instance (via withContext('tx', id)), so\n // reference equality reliably identifies the exporting worker.\n let exporterLc: LogContext | undefined;\n\n // Set when the exporting worker's cleanup runs, signalling that the snapshot\n // is no longer needed and any subsequently spawned workers should skip their\n // initTask.\n let firstWorkerDone = false;\n\n return {\n init: (tx, lc) => {\n if (!firstWorkerRun) {\n firstWorkerRun = true;\n exporterLc = lc; // Remember which worker is the exporter.\n const stmt = tx`SELECT pg_export_snapshot() AS snapshot;`.simple();\n // Intercept the promise to propagate the information to `snapshotExported`.\n stmt.then(result => exportSnapshot(result[0].snapshot), failExport);\n return [stmt]; // Also return the stmt so that it gets awaited (and errors handled).\n }\n if (!firstWorkerDone) {\n return snapshotExported.then(snapshotID => [\n tx.unsafe(`SET TRANSACTION SNAPSHOT '${snapshotID}'`),\n ]);\n }\n lc.debug?.('All work is done. No need to set snapshot');\n return [];\n },\n\n cleanup: (_tx, lc) => {\n // Only the exporting worker's cleanup should disable snapshot-setting.\n // Non-exporter workers may finish early; letting them flip this flag\n // would cause subsequently spawned workers to skip SET TRANSACTION SNAPSHOT\n // and read a newer database view, violating snapshot isolation.\n if (lc === exporterLc) {\n firstWorkerDone = true;\n }\n return [];\n },\n\n snapshotID: snapshotExported,\n };\n}\n\n/**\n * @returns An `init` Task for importing a snapshot from another transaction.\n */\nexport function importSnapshot(snapshotID: string): {\n init: Task;\n imported: Promise<void>;\n} {\n const {promise: imported, resolve, reject} = resolver<void>();\n\n return {\n init: tx => {\n const stmt = tx.unsafe(`SET TRANSACTION SNAPSHOT '${snapshotID}'`);\n stmt.then(() => resolve(), reject);\n return [stmt];\n },\n\n imported,\n };\n}\n\n/**\n * A superclass of Errors used for control flow that is needed to handle\n * another Error but does not constitute an error condition itself (e.g.\n * aborting transactions after a previous one fails). Subclassing this Error\n * will result in lowering the log level from `error` to `debug`.\n */\nexport class ControlFlowError extends Error {\n constructor(cause?: unknown) {\n super();\n this.cause = cause;\n }\n}\n\n/**\n * Internal error used to rollback the worker transaction. This is used\n * instead of executing a `ROLLBACK` statement because the postgres.js\n * library will otherwise try to execute an extraneous `COMMIT`, which\n * results in outputting a \"no transaction in progress\" warning to the\n * database logs.\n *\n * Throwing an exception, on the other hand, executes the postgres.js\n * codepath that calls `ROLLBACK` instead.\n */\nclass RollbackSignal extends ControlFlowError {\n readonly name = 'RollbackSignal';\n readonly message = 'rolling back transaction';\n}\n\nfunction ensureError(err: unknown): Error {\n if (err instanceof Error) {\n return err;\n }\n const error = new Error();\n error.cause = err;\n return error;\n}\n\ninterface TaskRunner {\n /**\n * Manages the running of a Task or ReadTask in two phases:\n *\n * - If the task involves blocking, this is done in the worker. Once the\n * blocking is done, `freeWorker()` is invoked to signal that the worker\n * is available to run another task. Note that this should be invoked\n * *before* resolving the result to the calling thread so that a\n * subsequent task can reuse the same worker.\n *\n * - Task statements are executed on the database asynchronously. The final\n * result of this processing is encapsulated in the returned `pending`\n * Promise. The worker will await the last pending Promise before closing\n * the transaction.\n *\n * @param freeWorker should be called as soon as all blocking operations are\n * completed in order to return the transaction to the pool.\n * @returns A `pending` Promise indicating when the statements have been\n * processed by the database, allowing the transaction to be closed.\n * This should be `null` if there are no transaction-dependent\n * statements to await.\n */\n run(\n tx: PostgresTransaction,\n lc: LogContext,\n freeWorker: () => void,\n ): Promise<{pending: Promise<void> | null}>;\n\n /**\n * Invoked if the TransactionPool is already in a failed state when the task\n * is requested.\n */\n rejected(reason: unknown): void;\n}\n\nconst IDLE_TIMEOUT_MS = 5_000;\n\n// The keepalive interval is settable by ZERO_TRANSACTION_POOL_KEEPALIVE_MS\n// as an emergency measure and is explicitly not made available as a server\n// option. This value is function of how the zero-cache uses transactions, and\n// should never need to be \"tuned\" or adjusted for different environments.\n//\n// Note that it must be shorter than IDLE_IN_TRANSACTION_SESSION_TIMEOUT_MS\n// with sufficient buffering to account for when the process is blocked by\n// synchronous calls (e.g. to the replica).\nconst KEEPALIVE_TIMEOUT_MS = parseInt(\n process.env.ZERO_TRANSACTION_POOL_KEEPALIVE_MS ?? '5000',\n);\n\nconst KEEPALIVE_TASK: Task = (tx, lc) => {\n lc.debug?.(`sending tx keepalive`);\n return [tx`SELECT 1`.simple()];\n};\n\ntype TimeoutTask = {\n timeoutMs: number;\n task: Task | 'done';\n};\n\ntype TimeoutTasks = {\n forInitialWorkers: TimeoutTask;\n forExtraWorkers: TimeoutTask;\n};\n\n// Production timeout tasks. Overridden in tests.\nexport const TIMEOUT_TASKS: TimeoutTasks = {\n forInitialWorkers: {\n timeoutMs: KEEPALIVE_TIMEOUT_MS,\n task: KEEPALIVE_TASK,\n },\n forExtraWorkers: {\n timeoutMs: IDLE_TIMEOUT_MS,\n task: 'done',\n },\n};\n\n// The slice of information from the Query object in Postgres.js that gets logged for debugging.\n// https://github.com/porsager/postgres/blob/f58cd4f3affd3e8ce8f53e42799672d86cd2c70b/src/connection.js#L219\ntype Query = {string: string; parameters: object[]};\n"],"mappings":"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"}
1
+ {"version":3,"file":"transaction-pool.js","names":["#mode","#init","#cleanup","#tasks","#workers","#initialWorkers","#maxWorkers","#timeoutTask","#lc","#stmtRunner","#numWorkers","#db","#addWorker","#start","#stmts","#numWorking","#failure","#done","#process","#readRunner","#refCount"],"sources":["../../../../../zero-cache/src/db/transaction-pool.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {type Resolver, resolver} from '@rocicorp/resolver';\nimport type postgres from 'postgres';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport type {Enum} from '../../../shared/src/enum.ts';\nimport {Queue} from '../../../shared/src/queue.ts';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {type PostgresDB, type PostgresTransaction} from '../types/pg.ts';\nimport type * as Mode from './mode-enum.ts';\nimport {runTx} from './run-transaction.ts';\n\ntype Mode = Enum<typeof Mode>;\n\ntype MaybePromise<T> = Promise<T> | T;\n\nexport type Statement =\n | postgres.PendingQuery<(postgres.Row & Iterable<postgres.Row>)[]>\n | postgres.PendingQuery<postgres.Row[]>;\n\n/**\n * A {@link Task} is logic run from within a transaction in a {@link TransactionPool}.\n * It returns a list of `Statements` that the transaction executes asynchronously and\n * awaits when it receives the 'done' signal.\n *\n */\nexport type Task = (\n tx: PostgresTransaction,\n lc: LogContext,\n) => MaybePromise<Statement[]>;\n\n/**\n * A {@link ReadTask} is run from within a transaction, but unlike a {@link Task},\n * the results of a ReadTask are opaque to the TransactionPool and returned to the\n * caller of {@link TransactionPool.processReadTask}.\n */\nexport type ReadTask<T> = (\n tx: PostgresTransaction,\n lc: LogContext,\n) => MaybePromise<T>;\n\n/**\n * A TransactionPool is a pool of one or more {@link postgres.TransactionSql}\n * objects that participate in processing a dynamic queue of tasks.\n *\n * This can be used for serializing a set of tasks that arrive asynchronously\n * to a single transaction (for writing) or performing parallel reads across\n * multiple connections at the same snapshot (e.g. read only snapshot transactions).\n */\nexport class TransactionPool {\n #lc: LogContext;\n readonly #mode: Mode;\n readonly #init: TaskRunner | undefined;\n readonly #cleanup: TaskRunner | undefined;\n readonly #tasks = new Queue<TaskRunner | Error | 'done'>();\n readonly #workers: Promise<unknown>[] = [];\n readonly #initialWorkers: number;\n readonly #maxWorkers: number;\n readonly #timeoutTask: TimeoutTasks;\n #numWorkers: number;\n #numWorking = 0;\n #db: PostgresDB | undefined; // set when running. stored to allow adaptive pool sizing.\n\n #refCount = 1;\n #done = false;\n #failure: Error | undefined;\n\n /**\n * @param init A {@link Task} that is run in each Transaction before it begins\n * processing general tasks. This can be used to to set the transaction\n * mode, export/set snapshots, etc. This will be run even if\n * {@link fail} has been called on the pool.\n * @param cleanup A {@link Task} that is run in each Transaction before it closes.\n * This will be run even if {@link fail} has been called, or if a\n * preceding Task threw an Error.\n * @param initialWorkers The initial number of transaction workers to process tasks.\n * This is the steady state number of workers that will be kept\n * alive if the TransactionPool is long lived.\n * This must be greater than 0. Defaults to 1.\n * @param maxWorkers When specified, allows the pool to grow to `maxWorkers`. This\n * must be greater than or equal to `initialWorkers`. On-demand\n * workers will be shut down after an idle timeout of 5 seconds.\n */\n constructor(\n lc: LogContext,\n mode: Mode,\n init?: Task,\n cleanup?: Task,\n initialWorkers = 1,\n maxWorkers = initialWorkers,\n timeoutTasks = TIMEOUT_TASKS, // Overridden for tests.\n ) {\n assert(initialWorkers > 0, 'initialWorkers must be positive');\n assert(\n maxWorkers >= initialWorkers,\n 'maxWorkers must be >= initialWorkers',\n );\n\n this.#lc = lc;\n this.#mode = mode;\n this.#init = init ? this.#stmtRunner(init) : undefined;\n this.#cleanup = cleanup ? this.#stmtRunner(cleanup) : undefined;\n this.#initialWorkers = initialWorkers;\n this.#numWorkers = initialWorkers;\n this.#maxWorkers = maxWorkers;\n this.#timeoutTask = timeoutTasks;\n }\n\n /**\n * Starts the pool of workers to process Tasks with transactions opened from the\n * specified {@link db}.\n */\n run(db: PostgresDB): this {\n assert(!this.#db, 'already running');\n this.#db = db;\n for (let i = 0; i < this.#numWorkers; i++) {\n this.#addWorker(db);\n }\n return this;\n }\n\n /**\n * Adds context parameters to internal LogContext. This is useful for context values that\n * are not known when the TransactionPool is constructed (e.g. determined after a database\n * call when the pool is running).\n *\n * Returns an object that can be used to add more parameters.\n */\n addLoggingContext(key: string, value: string) {\n this.#lc = this.#lc.withContext(key, value);\n\n return {\n addLoggingContext: (key: string, value: string) =>\n this.addLoggingContext(key, value),\n };\n }\n\n /**\n * Returns a promise that:\n *\n * * resolves after {@link setDone} has been called (or the the pool as been {@link unref}ed\n * to a 0 ref count), once all added tasks have been processed and all transactions have been\n * committed or closed.\n *\n * * rejects if processing was aborted with {@link fail} or if processing any of\n * the tasks resulted in an error. All uncommitted transactions will have been\n * rolled back.\n *\n * Note that partial failures are possible if processing writes with multiple workers\n * (e.g. `setDone` is called, allowing some workers to commit, after which other\n * workers encounter errors). Using a TransactionPool in this manner does not make\n * sense in terms of transactional semantics, and is thus not recommended.\n *\n * For reads, however, multiple workers is useful for performing parallel reads\n * at the same snapshot. See {@link synchronizedSnapshots} for an example.\n * Resolves or rejects when all workers are done or failed.\n */\n async done() {\n const numWorkers = this.#workers.length;\n await Promise.all(this.#workers);\n\n if (numWorkers < this.#workers.length) {\n // If workers were added after the initial set, they must be awaited to ensure\n // that the results (i.e. rejections) of all workers are accounted for. This only\n // needs to be re-done once, because the fact that the first `await` completed\n // guarantees that the pool is in a terminal state and no new workers can be added.\n await Promise.all(this.#workers);\n }\n this.#lc.debug?.('transaction pool done');\n\n const elapsed = performance.now() - this.#start;\n if (elapsed > 60_000) {\n if (this.#stmts > 0) {\n this.#lc.warn?.(\n `finished long transaction with ${this.#stmts} statements (${elapsed.toFixed(3)} ms)`,\n );\n } else {\n this.#lc.warn?.(\n `finished long read transaction (${elapsed.toFixed(3)} ms)`,\n );\n }\n }\n }\n\n #addWorker(db: PostgresDB) {\n const id = this.#workers.length + 1;\n const lc = this.#lc.withContext('tx', id);\n\n const tt: TimeoutTask =\n this.#workers.length < this.#initialWorkers\n ? this.#timeoutTask.forInitialWorkers\n : this.#timeoutTask.forExtraWorkers;\n const {timeoutMs} = tt;\n const timeoutTask = tt.task === 'done' ? 'done' : this.#stmtRunner(tt.task);\n\n const worker = async (tx: PostgresTransaction) => {\n const start = performance.now();\n try {\n lc.debug?.('started transaction');\n\n let last: Promise<void> = promiseVoid;\n\n const executeTask = async (runner: TaskRunner) => {\n runner !== this.#init && this.#numWorking++;\n const {pending} = await runner.run(tx, lc, () => {\n runner !== this.#init && this.#numWorking--;\n });\n last = pending ?? last;\n };\n\n let task: TaskRunner | Error | 'done' =\n this.#init ?? (await this.#tasks.dequeue(timeoutTask, timeoutMs));\n\n try {\n while (task !== 'done') {\n if (\n task instanceof Error ||\n (task !== this.#init && this.#failure)\n ) {\n throw this.#failure ?? task;\n }\n await executeTask(task);\n\n // await the next task.\n task = await this.#tasks.dequeue(timeoutTask, timeoutMs);\n }\n } finally {\n // Execute the cleanup task even on failure.\n if (this.#cleanup) {\n await executeTask(this.#cleanup);\n }\n }\n\n const elapsed = performance.now() - start;\n lc.debug?.(`closing transaction (${elapsed.toFixed(3)} ms)`);\n // Given the semantics of a Postgres transaction, the last statement\n // will only succeed if all of the preceding statements succeeded.\n return last;\n } catch (e) {\n if (e !== this.#failure) {\n this.fail(e); // A failure in any worker should fail the pool.\n }\n throw e;\n }\n };\n\n const workerTx = runTx(db, worker, {mode: this.#mode})\n .catch(e => {\n if (e instanceof RollbackSignal) {\n // A RollbackSignal is used to gracefully rollback the postgres.js\n // transaction block. It should not be thrown up to the application.\n lc.debug?.('aborted transaction');\n } else {\n throw e;\n }\n })\n .finally(() => this.#numWorkers--);\n\n // Attach a rejection handler immediately to prevent unhandledRejections.\n // The application will handle errors when it awaits processReadTask()\n // or done().\n workerTx.catch(() => {});\n\n this.#workers.push(workerTx);\n\n // After adding the worker, enqueue a terminal signal if we are in either of the\n // terminal states (both of which prevent more tasks from being enqueued), to ensure\n // that the added worker eventually exits.\n if (this.#done) {\n this.#tasks.enqueue('done');\n }\n if (this.#failure) {\n this.#tasks.enqueue(this.#failure);\n }\n }\n\n /**\n * Processes the statements produced by the specified {@link Task},\n * returning a Promise that resolves when the statements are either processed\n * by the database or rejected.\n *\n * Note that statement failures will result in failing the entire\n * TransactionPool (per transaction semantics). However, the returned Promise\n * itself will resolve rather than reject. As such, it is fine to ignore\n * returned Promises in order to pipeline requests to the database. It is\n * recommended to occasionally await them (e.g. after some threshold) in\n * order to avoid memory blowup in the case of database slowness.\n */\n process(task: Task): Promise<void> {\n const r = resolver<void>();\n this.#process(this.#stmtRunner(task, r));\n return r.promise;\n }\n\n readonly #start = performance.now();\n #stmts = 0;\n\n /**\n * Implements the semantics specified in {@link process()}.\n *\n * Specifically:\n * * `freeWorker()` is called as soon as the statements are produced,\n * allowing them to be pipelined to the database.\n * * Statement errors result in failing the transaction pool.\n * * The client-supplied Resolver resolves on success or failure;\n * it is never rejected.\n */\n #stmtRunner(task: Task, r: {resolve: () => void} = resolver()): TaskRunner {\n return {\n run: async (tx, lc, freeWorker) => {\n let stmts: Statement[];\n try {\n stmts = await task(tx, lc);\n } catch (e) {\n r.resolve();\n throw e;\n } finally {\n freeWorker();\n }\n\n if (stmts.length === 0) {\n r.resolve();\n return {pending: null};\n }\n\n // Execute the statements (i.e. send to the db) immediately.\n // The last result is returned for the worker to await before\n // closing the transaction.\n const last = stmts.reduce(\n (_, stmt) =>\n stmt\n .execute()\n .then(() => {\n if (++this.#stmts % 1000 === 0) {\n const log = this.#stmts % 10000 === 0 ? 'info' : 'debug';\n const q = stmt as unknown as Query;\n lc[log]?.(\n `executed ${this.#stmts}th statement (${(performance.now() - this.#start).toFixed(3)} ms)`,\n {statement: q.string},\n );\n }\n })\n .catch(e => this.fail(e)),\n promiseVoid,\n );\n return {pending: last.then(r.resolve)};\n },\n rejected: r.resolve,\n };\n }\n\n /**\n * Processes and returns the result of executing the {@link ReadTask} from\n * within the transaction. An error thrown by the task will result in\n * rejecting the returned Promise, but will not affect the transaction pool\n * itself.\n */\n processReadTask<T>(readTask: ReadTask<T>): Promise<T> {\n const r = resolver<T>();\n this.#process(this.#readRunner(readTask, r));\n return r.promise;\n }\n\n /**\n * Implements the semantics specified in {@link processReadTask()}.\n *\n * Specifically:\n * * `freeWorker()` is called as soon as the result is produced,\n * before resolving the client-supplied Resolver.\n * * Errors result in rejecting the client-supplied Resolver but\n * do not affect transaction pool.\n */\n #readRunner<T>(readTask: ReadTask<T>, r: Resolver<T>): TaskRunner {\n return {\n run: async (tx, lc, freeWorker) => {\n let result: T;\n try {\n result = await readTask(tx, lc);\n freeWorker();\n r.resolve(result);\n } catch (e) {\n freeWorker();\n r.reject(e);\n }\n return {pending: null};\n },\n rejected: r.reject,\n };\n }\n\n #process(runner: TaskRunner): void {\n assert(!this.#done, 'already set done');\n if (this.#failure) {\n runner.rejected(this.#failure);\n return;\n }\n\n this.#tasks.enqueue(runner);\n\n // Check if the pool size can and should be increased.\n if (this.#numWorkers < this.#maxWorkers) {\n const outstanding = this.#tasks.size();\n\n if (outstanding > this.#numWorkers - this.#numWorking) {\n this.#db && this.#addWorker(this.#db);\n this.#numWorkers++;\n this.#lc.debug?.(`Increased pool size to ${this.#numWorkers}`);\n }\n }\n }\n\n /**\n * Ends all workers with a ROLLBACK. Throws if the pool is already done\n * or aborted.\n */\n abort() {\n this.fail(new RollbackSignal());\n }\n\n /**\n * Signals to all workers to end their transaction once all pending tasks have\n * been completed. Throws if the pool is already done or aborted.\n */\n setDone() {\n assert(!this.#done, 'already set done');\n this.#done = true;\n\n for (let i = 0; i < this.#numWorkers; i++) {\n this.#tasks.enqueue('done');\n }\n }\n\n /**\n * An alternative to explicitly calling {@link setDone}, `ref()` increments an internal reference\n * count, and {@link unref} decrements it. When the reference count reaches 0, {@link setDone} is\n * automatically called. A TransactionPool is initialized with a reference count of 1.\n *\n * `ref()` should be called before sharing the pool with another component, and only after the\n * pool has been started with {@link run()}. It must not be called on a TransactionPool that is\n * already done (either via {@link unref()} or {@link setDone()}. (Doing so indicates a logical\n * error in the code.)\n *\n * It follows that:\n * * The creator of the TransactionPool is responsible for running it.\n * * The TransactionPool should be ref'ed before being sharing.\n * * The receiver of the TransactionPool is only responsible for unref'ing it.\n *\n * On the other hand, a transaction pool that fails with a runtime error can still be ref'ed;\n * attempts to use the pool will result in the runtime error as expected.\n */\n // TODO: Get rid of the ref-counting stuff. It's no longer needed.\n ref(count = 1) {\n assert(\n this.#db !== undefined && !this.#done,\n `Cannot ref() a TransactionPool that is not running`,\n );\n this.#refCount += count;\n }\n\n /**\n * Decrements the internal reference count, automatically invoking {@link setDone} when it reaches 0.\n */\n unref(count = 1) {\n assert(\n count <= this.#refCount,\n () => `Cannot unref ${count} when refCount is ${this.#refCount}`,\n );\n\n this.#refCount -= count;\n if (this.#refCount === 0) {\n this.setDone();\n }\n }\n\n isRunning(): boolean {\n return this.#db !== undefined && !this.#done && this.#failure === undefined;\n }\n\n /**\n * Signals all workers to fail their transactions with the given {@link err}.\n */\n fail(err: unknown) {\n if (!this.#failure) {\n this.#failure = ensureError(err); // Fail fast: this is checked in the worker loop.\n // Logged for informational purposes. It is the responsibility of\n // higher level logic to classify and handle the exception.\n const level =\n this.#failure instanceof ControlFlowError ? 'debug' : 'info';\n this.#lc[level]?.(this.#failure);\n\n for (let i = 0; i < this.#numWorkers; i++) {\n // Enqueue the Error to terminate any workers waiting for tasks.\n this.#tasks.enqueue(this.#failure);\n }\n }\n }\n}\n\ntype SynchronizeSnapshotTasks = {\n /**\n * The `init` Task for the TransactionPool from which the snapshot originates.\n * The pool must have Mode.SERIALIZABLE, and will be set to READ ONLY by the\n * `exportSnapshot` init task. If the TransactionPool has multiple workers, the\n * first worker will export a snapshot that the others set.\n */\n exportSnapshot: Task;\n\n /**\n * The `cleanup` Task for the TransactionPool from which the snapshot\n * originates. This Task will wait for the follower pool to `setSnapshot`\n * to ensure that the snapshot is successfully shared before the originating\n * transaction is closed.\n */\n cleanupExport: Task;\n\n /**\n * The `init` Task for the TransactionPool in which workers will\n * consequently see the same snapshot as that of the first pool. The pool\n * must have Mode.SERIALIZABLE, and will have the ability to perform writes.\n */\n setSnapshot: Task;\n\n /** The ID of the shared snapshot. */\n snapshotID: Promise<string>;\n};\n\n/**\n * Init Tasks for Postgres snapshot synchronization across transactions.\n *\n * https://www.postgresql.org/docs/9.3/functions-admin.html#:~:text=Snapshot%20Synchronization%20Functions,identical%20content%20in%20the%20database.\n */\nexport function synchronizedSnapshots(): SynchronizeSnapshotTasks {\n const {\n promise: snapshotExported,\n resolve: exportSnapshot,\n reject: failExport,\n } = resolver<string>();\n\n const {\n promise: snapshotCaptured,\n resolve: captureSnapshot,\n reject: failCapture,\n } = resolver<unknown>();\n\n // Set by the first worker to run its initTask, who becomes responsible for\n // exporting the snapshot. TODO: Plumb the workerNum and use that instead.\n let firstWorkerRun = false;\n\n // Note: Neither init task should `await`, as processing in each pool can proceed\n // as soon as the statements have been sent to the db. However, the `cleanupExport`\n // task must `await` the result of `setSnapshot` to ensure that exporting transaction\n // does not close before the snapshot has been captured.\n return {\n exportSnapshot: tx => {\n if (!firstWorkerRun) {\n firstWorkerRun = true;\n const stmt =\n tx`SELECT pg_export_snapshot() AS snapshot; SET TRANSACTION READ ONLY;`.simple();\n // Intercept the promise to propagate the information to `snapshotExported`.\n stmt.then(result => exportSnapshot(result[0].snapshot), failExport);\n return [stmt]; // Also return the stmt so that it gets awaited (and errors handled).\n }\n return snapshotExported.then(snapshotID => [\n tx.unsafe(`SET TRANSACTION SNAPSHOT '${snapshotID}'`),\n tx`SET TRANSACTION READ ONLY`.simple(),\n ]);\n },\n\n setSnapshot: tx =>\n snapshotExported.then(snapshotID => {\n const stmt = tx.unsafe(`SET TRANSACTION SNAPSHOT '${snapshotID}'`);\n // Intercept the promise to propagate the information to `cleanupExport`.\n stmt.then(captureSnapshot, failCapture);\n return [stmt];\n }),\n\n cleanupExport: async () => {\n await snapshotCaptured;\n return [];\n },\n\n snapshotID: snapshotExported,\n };\n}\n\n/**\n * Returns `init` and `cleanup` {@link Task}s for a TransactionPool that ensure its workers\n * share a single view of the database. This is used for View Notifier and View Syncer logic\n * that allows multiple entities to perform parallel reads on the same snapshot of the database.\n */\nexport function sharedSnapshot(): {\n init: Task;\n cleanup: Task;\n snapshotID: Promise<string>;\n} {\n const {\n promise: snapshotExported,\n resolve: exportSnapshot,\n reject: failExport,\n } = resolver<string>();\n\n // Set by the first worker to run its initTask, who becomes responsible for\n // exporting the snapshot.\n let firstWorkerRun = false;\n\n // The LogContext of the exporting worker, used to identify its cleanup call.\n // Each worker receives a unique lc instance (via withContext('tx', id)), so\n // reference equality reliably identifies the exporting worker.\n let exporterLc: LogContext | undefined;\n\n // Set when the exporting worker's cleanup runs, signalling that the snapshot\n // is no longer needed and any subsequently spawned workers should skip their\n // initTask.\n let firstWorkerDone = false;\n\n return {\n init: (tx, lc) => {\n if (!firstWorkerRun) {\n firstWorkerRun = true;\n exporterLc = lc; // Remember which worker is the exporter.\n const stmt = tx`SELECT pg_export_snapshot() AS snapshot;`.simple();\n // Intercept the promise to propagate the information to `snapshotExported`.\n stmt.then(result => exportSnapshot(result[0].snapshot), failExport);\n return [stmt]; // Also return the stmt so that it gets awaited (and errors handled).\n }\n if (!firstWorkerDone) {\n return snapshotExported.then(snapshotID => [\n tx.unsafe(`SET TRANSACTION SNAPSHOT '${snapshotID}'`),\n ]);\n }\n lc.debug?.('All work is done. No need to set snapshot');\n return [];\n },\n\n cleanup: (_tx, lc) => {\n // Only the exporting worker's cleanup should disable snapshot-setting.\n // Non-exporter workers may finish early; letting them flip this flag\n // would cause subsequently spawned workers to skip SET TRANSACTION SNAPSHOT\n // and read a newer database view, violating snapshot isolation.\n if (lc === exporterLc) {\n firstWorkerDone = true;\n }\n return [];\n },\n\n snapshotID: snapshotExported,\n };\n}\n\n/**\n * @returns An `init` Task for importing a snapshot from another transaction.\n */\nexport function importSnapshot(snapshotID: string): {\n init: Task;\n imported: Promise<void>;\n} {\n const {promise: imported, resolve, reject} = resolver<void>();\n\n return {\n init: tx => {\n const stmt = tx.unsafe(`SET TRANSACTION SNAPSHOT '${snapshotID}'`);\n stmt.then(() => resolve(), reject);\n return [stmt];\n },\n\n imported,\n };\n}\n\n/**\n * A superclass of Errors used for control flow that is needed to handle\n * another Error but does not constitute an error condition itself (e.g.\n * aborting transactions after a previous one fails). Subclassing this Error\n * will result in lowering the log level from `error` to `debug`.\n */\nexport class ControlFlowError extends Error {\n constructor(cause?: unknown) {\n super();\n this.cause = cause;\n }\n}\n\n/**\n * Internal error used to rollback the worker transaction. This is used\n * instead of executing a `ROLLBACK` statement because the postgres.js\n * library will otherwise try to execute an extraneous `COMMIT`, which\n * results in outputting a \"no transaction in progress\" warning to the\n * database logs.\n *\n * Throwing an exception, on the other hand, executes the postgres.js\n * codepath that calls `ROLLBACK` instead.\n */\nclass RollbackSignal extends ControlFlowError {\n readonly name = 'RollbackSignal';\n readonly message = 'rolling back transaction';\n}\n\nfunction ensureError(err: unknown): Error {\n if (err instanceof Error) {\n return err;\n }\n const error = new Error();\n error.cause = err;\n return error;\n}\n\ninterface TaskRunner {\n /**\n * Manages the running of a Task or ReadTask in two phases:\n *\n * - If the task involves blocking, this is done in the worker. Once the\n * blocking is done, `freeWorker()` is invoked to signal that the worker\n * is available to run another task. Note that this should be invoked\n * *before* resolving the result to the calling thread so that a\n * subsequent task can reuse the same worker.\n *\n * - Task statements are executed on the database asynchronously. The final\n * result of this processing is encapsulated in the returned `pending`\n * Promise. The worker will await the last pending Promise before closing\n * the transaction.\n *\n * @param freeWorker should be called as soon as all blocking operations are\n * completed in order to return the transaction to the pool.\n * @returns A `pending` Promise indicating when the statements have been\n * processed by the database, allowing the transaction to be closed.\n * This should be `null` if there are no transaction-dependent\n * statements to await.\n */\n run(\n tx: PostgresTransaction,\n lc: LogContext,\n freeWorker: () => void,\n ): Promise<{pending: Promise<void> | null}>;\n\n /**\n * Invoked if the TransactionPool is already in a failed state when the task\n * is requested.\n */\n rejected(reason: unknown): void;\n}\n\nconst IDLE_TIMEOUT_MS = 5_000;\n\n// The keepalive interval is settable by ZERO_TRANSACTION_POOL_KEEPALIVE_MS\n// as an emergency measure and is explicitly not made available as a server\n// option. This value is function of how the zero-cache uses transactions, and\n// should never need to be \"tuned\" or adjusted for different environments.\n//\n// Note that it must be shorter than IDLE_IN_TRANSACTION_SESSION_TIMEOUT_MS\n// with sufficient buffering to account for when the process is blocked by\n// synchronous calls (e.g. to the replica).\nconst KEEPALIVE_TIMEOUT_MS = parseInt(\n process.env.ZERO_TRANSACTION_POOL_KEEPALIVE_MS ?? '5000',\n);\n\nconst KEEPALIVE_TASK: Task = (tx, lc) => {\n lc.debug?.(`sending tx keepalive`);\n return [tx`SELECT 1`.simple()];\n};\n\ntype TimeoutTask = {\n timeoutMs: number;\n task: Task | 'done';\n};\n\ntype TimeoutTasks = {\n forInitialWorkers: TimeoutTask;\n forExtraWorkers: TimeoutTask;\n};\n\n// Production timeout tasks. Overridden in tests.\nexport const TIMEOUT_TASKS: TimeoutTasks = {\n forInitialWorkers: {\n timeoutMs: KEEPALIVE_TIMEOUT_MS,\n task: KEEPALIVE_TASK,\n },\n forExtraWorkers: {\n timeoutMs: IDLE_TIMEOUT_MS,\n task: 'done',\n },\n};\n\n// The slice of information from the Query object in Postgres.js that gets logged for debugging.\n// https://github.com/porsager/postgres/blob/f58cd4f3affd3e8ce8f53e42799672d86cd2c70b/src/connection.js#L219\ntype Query = {string: string; parameters: object[]};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"change-streamer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"names":[],"mappings":"AA0BA,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAK/B,wBAA8B,SAAS,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAsJf"}
1
+ {"version":3,"file":"change-streamer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"names":[],"mappings":"AA0BA,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAK/B,wBAA8B,SAAS,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAyJf"}
@@ -43,7 +43,10 @@ async function runWorker(parent, env, ...args) {
43
43
  let changeStreamer;
44
44
  const context = getServerContext(config);
45
45
  for (const first of [true, false]) try {
46
- const { changeSource, subscriptionState } = upstream.type === "pg" ? await initializePostgresChangeSource(lc, upstream.db, shard, replica.file, initialSync, context, replicationLag.reportIntervalMs) : await initializeCustomChangeSource(lc, upstream.db, shard, replica.file, context);
46
+ const { changeSource, subscriptionState } = upstream.type === "pg" ? await initializePostgresChangeSource(lc, upstream.db, shard, replica.file, {
47
+ ...initialSync,
48
+ replicationSlotFailover: upstream.pgReplicationSlotFailover
49
+ }, context, replicationLag.reportIntervalMs) : await initializeCustomChangeSource(lc, upstream.db, shard, replica.file, context);
47
50
  changeStreamer = await initializeStreamer(lc, shard, taskID, address, protocol, changeDB, changeSource, ReplicationStatusPublisher.forReplicaFile(replica.file), subscriptionState, autoReset ?? false, backPressureLimitHeapProportion, flowControlConsensusPaddingSeconds, setTimeout);
48
51
  break;
49
52
  } catch (e) {
@@ -1 +1 @@
1
- {"version":3,"file":"change-streamer.js","names":[],"sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {DatabaseInitError} from '../../../zqlite/src/db.ts';\nimport {getServerContext} from '../config/server-context.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {deleteLiteDB} from '../db/delete-lite-db.ts';\nimport {warmupConnections} from '../db/warmup.ts';\nimport {initEventSink, publishCriticalEvent} from '../observability/events.ts';\nimport {upgradeReplica} from '../services/change-source/common/replica-schema.ts';\nimport {initializeCustomChangeSource} from '../services/change-source/custom/change-source.ts';\nimport {initializePostgresChangeSource} from '../services/change-source/pg/change-source.ts';\nimport {BackupMonitor} from '../services/change-streamer/backup-monitor.ts';\nimport {ChangeStreamerHttpServer} from '../services/change-streamer/change-streamer-http.ts';\nimport {initializeStreamer} from '../services/change-streamer/change-streamer-service.ts';\nimport type {ChangeStreamerService} from '../services/change-streamer/change-streamer.ts';\nimport {ReplicaMonitor} from '../services/change-streamer/replica-monitor.ts';\nimport {\n AutoResetSignal,\n CHANGE_STREAMER_APP_NAME,\n} from '../services/change-streamer/schema/tables.ts';\nimport {exitAfter, runUntilKilled} from '../services/life-cycle.ts';\nimport {\n replicationStatusError,\n ReplicationStatusPublisher,\n} from '../services/replicator/replication-status.ts';\nimport {pgClient} from '../types/pg.ts';\nimport {\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {getShardConfig} from '../types/shards.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...args: string[]\n): Promise<void> {\n assert(args.length > 0, `parent startMs not specified`);\n const parentStartMs = parseInt(args[0]);\n\n const config = getNormalizedZeroConfig({env, argv: args.slice(1)});\n const {\n taskID,\n changeStreamer: {\n port,\n address,\n protocol,\n startupDelayMs,\n backPressureLimitHeapProportion,\n flowControlConsensusPaddingSeconds,\n },\n upstream,\n change,\n replica,\n initialSync,\n litestream,\n } = config;\n\n startOtelAuto(createLogContext(config, {worker: 'change-streamer'}, false));\n const lc = createLogContext(config, {worker: 'change-streamer'}, true);\n initEventSink(lc, config);\n\n // Kick off DB connection warmup in the background.\n const changeDB = pgClient(\n lc,\n change.db,\n {\n max: change.maxConns,\n connection: {['application_name']: CHANGE_STREAMER_APP_NAME},\n },\n {sendStringAsJson: true},\n );\n void warmupConnections(lc, changeDB, 'change');\n\n const {autoReset, replicationLag} = config;\n const shard = getShardConfig(config);\n\n let changeStreamer: ChangeStreamerService | undefined;\n\n const context = getServerContext(config);\n\n for (const first of [true, false]) {\n try {\n // Note: This performs initial sync of the replica if necessary.\n const {changeSource, subscriptionState} =\n upstream.type === 'pg'\n ? await initializePostgresChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n initialSync,\n context,\n replicationLag.reportIntervalMs,\n )\n : await initializeCustomChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n context,\n );\n\n const replicationStatusPublisher =\n ReplicationStatusPublisher.forReplicaFile(replica.file);\n\n changeStreamer = await initializeStreamer(\n lc,\n shard,\n taskID,\n address,\n protocol,\n changeDB,\n changeSource,\n replicationStatusPublisher,\n subscriptionState,\n autoReset ?? false,\n backPressureLimitHeapProportion,\n flowControlConsensusPaddingSeconds,\n setTimeout,\n );\n break;\n } catch (e) {\n if (first && e instanceof AutoResetSignal) {\n lc.warn?.(`resetting replica ${replica.file}`, e);\n // TODO: Make deleteLiteDB work with litestream. It will probably have to be\n // a semantic wipe instead of a file delete.\n deleteLiteDB(replica.file);\n continue; // execute again with a fresh initial-sync\n }\n await publishCriticalEvent(\n lc,\n replicationStatusError(lc, 'Initializing', e),\n );\n if (e instanceof DatabaseInitError) {\n throw new Error(\n `Cannot open ZERO_REPLICA_FILE at \"${replica.file}\". Please check that the path is valid.`,\n {cause: e},\n );\n }\n throw e;\n }\n }\n // impossible: upstream must have advanced in order for replication to be stuck.\n assert(changeStreamer, `resetting replica did not advance replicaVersion`);\n\n // Perform any upgrades to the replica in case it was restored from an\n // earlier version. Note that this upgrade is done by the replicator worker\n // as well (in both the replication-manager and the view-syncer), but the\n // change-streamer independently reads the replica, and it is fine run the\n // upgrade logic redundantly since it is idempotent.\n await upgradeReplica(lc, 'change-streamer-init', replica.file);\n\n const {backupURL, port: metricsPort} = litestream;\n const monitor = backupURL\n ? new BackupMonitor(\n lc,\n replica.file,\n backupURL,\n `http://localhost:${metricsPort}/metrics`,\n changeStreamer,\n // The time between when the zero-cache was started to when the\n // change-streamer is ready to start serves as the initial delay for\n // watermark cleanup (as it either includes a similar replica\n // restoration/preparation step, or an initial-sync, which\n // generally takes longer).\n //\n // Consider: Also account for permanent volumes?\n Date.now() - parentStartMs,\n )\n : new ReplicaMonitor(lc, replica.file, changeStreamer);\n\n const changeStreamerWebServer = new ChangeStreamerHttpServer(\n lc,\n config,\n {port, startupDelayMs},\n parent,\n changeStreamer,\n monitor instanceof BackupMonitor ? monitor : null,\n );\n\n parent.send(['ready', {ready: true}]);\n\n // Note: The changeStreamer itself is not started here; it is started by the\n // changeStreamerWebServer.\n return runUntilKilled(lc, parent, changeStreamerWebServer, monitor);\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(() =>\n runWorker(must(parentWorker), process.env, ...process.argv.slice(2)),\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAmCA,eAA8B,UAC5B,QACA,KACA,GAAG,MACY;AACf,QAAO,KAAK,SAAS,GAAG,+BAA+B;CACvD,MAAM,gBAAgB,SAAS,KAAK,GAAG;CAEvC,MAAM,SAAS,wBAAwB;EAAC;EAAK,MAAM,KAAK,MAAM,EAAE;EAAC,CAAC;CAClE,MAAM,EACJ,QACA,gBAAgB,EACd,MACA,SACA,UACA,gBACA,iCACA,sCAEF,UACA,QACA,SACA,aACA,eACE;AAEJ,eAAc,iBAAiB,QAAQ,EAAC,QAAQ,mBAAkB,EAAE,MAAM,CAAC;CAC3E,MAAM,KAAK,iBAAiB,QAAQ,EAAC,QAAQ,mBAAkB,EAAE,KAAK;AACtE,eAAc,IAAI,OAAO;CAGzB,MAAM,WAAW,SACf,IACA,OAAO,IACP;EACE,KAAK,OAAO;EACZ,YAAY,GAAE,qBAAqB,0BAAyB;EAC7D,EACD,EAAC,kBAAkB,MAAK,CACzB;AACI,mBAAkB,IAAI,UAAU,SAAS;CAE9C,MAAM,EAAC,WAAW,mBAAkB;CACpC,MAAM,QAAQ,eAAe,OAAO;CAEpC,IAAI;CAEJ,MAAM,UAAU,iBAAiB,OAAO;AAExC,MAAK,MAAM,SAAS,CAAC,MAAM,MAAM,CAC/B,KAAI;EAEF,MAAM,EAAC,cAAc,sBACnB,SAAS,SAAS,OACd,MAAM,+BACJ,IACA,SAAS,IACT,OACA,QAAQ,MACR,aACA,SACA,eAAe,iBAChB,GACD,MAAM,6BACJ,IACA,SAAS,IACT,OACA,QAAQ,MACR,QACD;AAKP,mBAAiB,MAAM,mBACrB,IACA,OACA,QACA,SACA,UACA,UACA,cATA,2BAA2B,eAAe,QAAQ,KAAK,EAWvD,mBACA,aAAa,OACb,iCACA,oCACA,WACD;AACD;UACO,GAAG;AACV,MAAI,SAAS,aAAa,iBAAiB;AACzC,MAAG,OAAO,qBAAqB,QAAQ,QAAQ,EAAE;AAGjD,gBAAa,QAAQ,KAAK;AAC1B;;AAEF,QAAM,qBACJ,IACA,uBAAuB,IAAI,gBAAgB,EAAE,CAC9C;AACD,MAAI,aAAa,kBACf,OAAM,IAAI,MACR,qCAAqC,QAAQ,KAAK,0CAClD,EAAC,OAAO,GAAE,CACX;AAEH,QAAM;;AAIV,QAAO,gBAAgB,mDAAmD;AAO1E,OAAM,eAAe,IAAI,wBAAwB,QAAQ,KAAK;CAE9D,MAAM,EAAC,WAAW,MAAM,gBAAe;CACvC,MAAM,UAAU,YACZ,IAAI,cACF,IACA,QAAQ,MACR,WACA,oBAAoB,YAAY,WAChC,gBAQA,KAAK,KAAK,GAAG,cACd,GACD,IAAI,eAAe,IAAI,QAAQ,MAAM,eAAe;CAExD,MAAM,0BAA0B,IAAI,yBAClC,IACA,QACA;EAAC;EAAM;EAAe,EACtB,QACA,gBACA,mBAAmB,gBAAgB,UAAU,KAC9C;AAED,QAAO,KAAK,CAAC,SAAS,EAAC,OAAO,MAAK,CAAC,CAAC;AAIrC,QAAO,eAAe,IAAI,QAAQ,yBAAyB,QAAQ;;AAIrE,IAAI,CAAC,mBAAmB,CACjB,iBACH,UAAU,KAAK,aAAa,EAAE,QAAQ,KAAK,GAAG,QAAQ,KAAK,MAAM,EAAE,CAAC,CACrE"}
1
+ {"version":3,"file":"change-streamer.js","names":[],"sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {DatabaseInitError} from '../../../zqlite/src/db.ts';\nimport {getServerContext} from '../config/server-context.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {deleteLiteDB} from '../db/delete-lite-db.ts';\nimport {warmupConnections} from '../db/warmup.ts';\nimport {initEventSink, publishCriticalEvent} from '../observability/events.ts';\nimport {upgradeReplica} from '../services/change-source/common/replica-schema.ts';\nimport {initializeCustomChangeSource} from '../services/change-source/custom/change-source.ts';\nimport {initializePostgresChangeSource} from '../services/change-source/pg/change-source.ts';\nimport {BackupMonitor} from '../services/change-streamer/backup-monitor.ts';\nimport {ChangeStreamerHttpServer} from '../services/change-streamer/change-streamer-http.ts';\nimport {initializeStreamer} from '../services/change-streamer/change-streamer-service.ts';\nimport type {ChangeStreamerService} from '../services/change-streamer/change-streamer.ts';\nimport {ReplicaMonitor} from '../services/change-streamer/replica-monitor.ts';\nimport {\n AutoResetSignal,\n CHANGE_STREAMER_APP_NAME,\n} from '../services/change-streamer/schema/tables.ts';\nimport {exitAfter, runUntilKilled} from '../services/life-cycle.ts';\nimport {\n replicationStatusError,\n ReplicationStatusPublisher,\n} from '../services/replicator/replication-status.ts';\nimport {pgClient} from '../types/pg.ts';\nimport {\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {getShardConfig} from '../types/shards.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...args: string[]\n): Promise<void> {\n assert(args.length > 0, `parent startMs not specified`);\n const parentStartMs = parseInt(args[0]);\n\n const config = getNormalizedZeroConfig({env, argv: args.slice(1)});\n const {\n taskID,\n changeStreamer: {\n port,\n address,\n protocol,\n startupDelayMs,\n backPressureLimitHeapProportion,\n flowControlConsensusPaddingSeconds,\n },\n upstream,\n change,\n replica,\n initialSync,\n litestream,\n } = config;\n\n startOtelAuto(createLogContext(config, {worker: 'change-streamer'}, false));\n const lc = createLogContext(config, {worker: 'change-streamer'}, true);\n initEventSink(lc, config);\n\n // Kick off DB connection warmup in the background.\n const changeDB = pgClient(\n lc,\n change.db,\n {\n max: change.maxConns,\n connection: {['application_name']: CHANGE_STREAMER_APP_NAME},\n },\n {sendStringAsJson: true},\n );\n void warmupConnections(lc, changeDB, 'change');\n\n const {autoReset, replicationLag} = config;\n const shard = getShardConfig(config);\n\n let changeStreamer: ChangeStreamerService | undefined;\n\n const context = getServerContext(config);\n\n for (const first of [true, false]) {\n try {\n // Note: This performs initial sync of the replica if necessary.\n const {changeSource, subscriptionState} =\n upstream.type === 'pg'\n ? await initializePostgresChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n {\n ...initialSync,\n replicationSlotFailover: upstream.pgReplicationSlotFailover,\n },\n context,\n replicationLag.reportIntervalMs,\n )\n : await initializeCustomChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n context,\n );\n\n const replicationStatusPublisher =\n ReplicationStatusPublisher.forReplicaFile(replica.file);\n\n changeStreamer = await initializeStreamer(\n lc,\n shard,\n taskID,\n address,\n protocol,\n changeDB,\n changeSource,\n replicationStatusPublisher,\n subscriptionState,\n autoReset ?? false,\n backPressureLimitHeapProportion,\n flowControlConsensusPaddingSeconds,\n setTimeout,\n );\n break;\n } catch (e) {\n if (first && e instanceof AutoResetSignal) {\n lc.warn?.(`resetting replica ${replica.file}`, e);\n // TODO: Make deleteLiteDB work with litestream. It will probably have to be\n // a semantic wipe instead of a file delete.\n deleteLiteDB(replica.file);\n continue; // execute again with a fresh initial-sync\n }\n await publishCriticalEvent(\n lc,\n replicationStatusError(lc, 'Initializing', e),\n );\n if (e instanceof DatabaseInitError) {\n throw new Error(\n `Cannot open ZERO_REPLICA_FILE at \"${replica.file}\". Please check that the path is valid.`,\n {cause: e},\n );\n }\n throw e;\n }\n }\n // impossible: upstream must have advanced in order for replication to be stuck.\n assert(changeStreamer, `resetting replica did not advance replicaVersion`);\n\n // Perform any upgrades to the replica in case it was restored from an\n // earlier version. Note that this upgrade is done by the replicator worker\n // as well (in both the replication-manager and the view-syncer), but the\n // change-streamer independently reads the replica, and it is fine run the\n // upgrade logic redundantly since it is idempotent.\n await upgradeReplica(lc, 'change-streamer-init', replica.file);\n\n const {backupURL, port: metricsPort} = litestream;\n const monitor = backupURL\n ? new BackupMonitor(\n lc,\n replica.file,\n backupURL,\n `http://localhost:${metricsPort}/metrics`,\n changeStreamer,\n // The time between when the zero-cache was started to when the\n // change-streamer is ready to start serves as the initial delay for\n // watermark cleanup (as it either includes a similar replica\n // restoration/preparation step, or an initial-sync, which\n // generally takes longer).\n //\n // Consider: Also account for permanent volumes?\n Date.now() - parentStartMs,\n )\n : new ReplicaMonitor(lc, replica.file, changeStreamer);\n\n const changeStreamerWebServer = new ChangeStreamerHttpServer(\n lc,\n config,\n {port, startupDelayMs},\n parent,\n changeStreamer,\n monitor instanceof BackupMonitor ? monitor : null,\n );\n\n parent.send(['ready', {ready: true}]);\n\n // Note: The changeStreamer itself is not started here; it is started by the\n // changeStreamerWebServer.\n return runUntilKilled(lc, parent, changeStreamerWebServer, monitor);\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(() =>\n runWorker(must(parentWorker), process.env, ...process.argv.slice(2)),\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAmCA,eAA8B,UAC5B,QACA,KACA,GAAG,MACY;AACf,QAAO,KAAK,SAAS,GAAG,+BAA+B;CACvD,MAAM,gBAAgB,SAAS,KAAK,GAAG;CAEvC,MAAM,SAAS,wBAAwB;EAAC;EAAK,MAAM,KAAK,MAAM,EAAE;EAAC,CAAC;CAClE,MAAM,EACJ,QACA,gBAAgB,EACd,MACA,SACA,UACA,gBACA,iCACA,sCAEF,UACA,QACA,SACA,aACA,eACE;AAEJ,eAAc,iBAAiB,QAAQ,EAAC,QAAQ,mBAAkB,EAAE,MAAM,CAAC;CAC3E,MAAM,KAAK,iBAAiB,QAAQ,EAAC,QAAQ,mBAAkB,EAAE,KAAK;AACtE,eAAc,IAAI,OAAO;CAGzB,MAAM,WAAW,SACf,IACA,OAAO,IACP;EACE,KAAK,OAAO;EACZ,YAAY,GAAE,qBAAqB,0BAAyB;EAC7D,EACD,EAAC,kBAAkB,MAAK,CACzB;AACI,mBAAkB,IAAI,UAAU,SAAS;CAE9C,MAAM,EAAC,WAAW,mBAAkB;CACpC,MAAM,QAAQ,eAAe,OAAO;CAEpC,IAAI;CAEJ,MAAM,UAAU,iBAAiB,OAAO;AAExC,MAAK,MAAM,SAAS,CAAC,MAAM,MAAM,CAC/B,KAAI;EAEF,MAAM,EAAC,cAAc,sBACnB,SAAS,SAAS,OACd,MAAM,+BACJ,IACA,SAAS,IACT,OACA,QAAQ,MACR;GACE,GAAG;GACH,yBAAyB,SAAS;GACnC,EACD,SACA,eAAe,iBAChB,GACD,MAAM,6BACJ,IACA,SAAS,IACT,OACA,QAAQ,MACR,QACD;AAKP,mBAAiB,MAAM,mBACrB,IACA,OACA,QACA,SACA,UACA,UACA,cATA,2BAA2B,eAAe,QAAQ,KAAK,EAWvD,mBACA,aAAa,OACb,iCACA,oCACA,WACD;AACD;UACO,GAAG;AACV,MAAI,SAAS,aAAa,iBAAiB;AACzC,MAAG,OAAO,qBAAqB,QAAQ,QAAQ,EAAE;AAGjD,gBAAa,QAAQ,KAAK;AAC1B;;AAEF,QAAM,qBACJ,IACA,uBAAuB,IAAI,gBAAgB,EAAE,CAC9C;AACD,MAAI,aAAa,kBACf,OAAM,IAAI,MACR,qCAAqC,QAAQ,KAAK,0CAClD,EAAC,OAAO,GAAE,CACX;AAEH,QAAM;;AAIV,QAAO,gBAAgB,mDAAmD;AAO1E,OAAM,eAAe,IAAI,wBAAwB,QAAQ,KAAK;CAE9D,MAAM,EAAC,WAAW,MAAM,gBAAe;CACvC,MAAM,UAAU,YACZ,IAAI,cACF,IACA,QAAQ,MACR,WACA,oBAAoB,YAAY,WAChC,gBAQA,KAAK,KAAK,GAAG,cACd,GACD,IAAI,eAAe,IAAI,QAAQ,MAAM,eAAe;CAExD,MAAM,0BAA0B,IAAI,yBAClC,IACA,QACA;EAAC;EAAM;EAAe,EACtB,QACA,gBACA,mBAAmB,gBAAgB,UAAU,KAC9C;AAED,QAAO,KAAK,CAAC,SAAS,EAAC,OAAO,MAAK,CAAC,CAAC;AAIrC,QAAO,eAAe,IAAI,QAAQ,yBAAyB,QAAQ;;AAIrE,IAAI,CAAC,mBAAmB,CACjB,iBACH,UAAU,KAAK,aAAa,EAAE,QAAQ,KAAK,GAAG,QAAQ,KAAK,MAAM,EAAE,CAAC,CACrE"}
@@ -4,7 +4,7 @@ import type { AST } from '../../../zero-protocol/src/ast.ts';
4
4
  import type { ServerMetrics as ServerMetricsJSON } from '../../../zero-protocol/src/inspect-down.ts';
5
5
  import { type MetricMap, type MetricsDelegate } from '../../../zql/src/query/metrics-delegate.ts';
6
6
  import type { CustomQueryTransformer } from '../custom-queries/transform-query.ts';
7
- import type { HeaderOptions } from '../custom/fetch.ts';
7
+ import type { ConnectionContext } from '../services/view-syncer/connection-context-manager.ts';
8
8
  /**
9
9
  * Server-side metrics collected for queries during materialization and update.
10
10
  * These metrics are reported via the inspector and complement client-side metrics.
@@ -38,7 +38,7 @@ export declare class InspectorDelegate implements MetricsDelegate {
38
38
  * CustomQueryTransformer. This is primarily used by the inspector to transform
39
39
  * queries for analysis.
40
40
  */
41
- transformCustomQuery(name: string, args: readonly ReadonlyJSONValue[], headerOptions: HeaderOptions, userQueryURL: string | undefined): Promise<AST>;
41
+ transformCustomQuery(name: string, args: readonly ReadonlyJSONValue[], ctx: ConnectionContext): Promise<AST>;
42
42
  }
43
43
  export {};
44
44
  //# sourceMappingURL=inspector-delegate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"inspector-delegate.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/inspector-delegate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,6BAA6B,CAAC;AAEnE,OAAO,EAAC,OAAO,EAAC,MAAM,gCAAgC,CAAC;AACvD,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,mCAAmC,CAAC;AAC3D,OAAO,KAAK,EAAC,aAAa,IAAI,iBAAiB,EAAC,MAAM,4CAA4C,CAAC;AAEnG,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,eAAe,EACrB,MAAM,4CAA4C,CAAC;AAEpD,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,sCAAsC,CAAC;AACjF,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,oBAAoB,CAAC;AAItD;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,8BAA8B,EAAE,OAAO,CAAC;IACxC,qBAAqB,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,KAAK,aAAa,GAAG,MAAM,CAAC;AAQ5B,qBAAa,iBAAkB,YAAW,eAAe;;gBAM3C,sBAAsB,EAAE,sBAAsB,GAAG,SAAS;IAItE,SAAS,CAAC,CAAC,SAAS,MAAM,SAAS,EACjC,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,MAAM,EACb,GAAG,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,GACpB,IAAI;IAYP,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAKjE,cAAc;;;;IAId,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS;IAIhD,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKlC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI;IAIzC;;;OAGG;IACH,eAAe,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO;IAMtD,gBAAgB,CAAC,aAAa,EAAE,aAAa,GAAG,IAAI;IAIpD,kBAAkB,CAAC,aAAa,EAAE,aAAa;IAI/C;;;;OAIG;IACG,oBAAoB,CACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,SAAS,iBAAiB,EAAE,EAClC,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,OAAO,CAAC,GAAG,CAAC;CA2ChB"}
1
+ {"version":3,"file":"inspector-delegate.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/inspector-delegate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,6BAA6B,CAAC;AAEnE,OAAO,EAAC,OAAO,EAAC,MAAM,gCAAgC,CAAC;AACvD,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,mCAAmC,CAAC;AAC3D,OAAO,KAAK,EAAC,aAAa,IAAI,iBAAiB,EAAC,MAAM,4CAA4C,CAAC;AAEnG,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,eAAe,EACrB,MAAM,4CAA4C,CAAC;AAEpD,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,sCAAsC,CAAC;AACjF,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,uDAAuD,CAAC;AAI7F;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,8BAA8B,EAAE,OAAO,CAAC;IACxC,qBAAqB,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,KAAK,aAAa,GAAG,MAAM,CAAC;AAQ5B,qBAAa,iBAAkB,YAAW,eAAe;;gBAM3C,sBAAsB,EAAE,sBAAsB,GAAG,SAAS;IAItE,SAAS,CAAC,CAAC,SAAS,MAAM,SAAS,EACjC,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,MAAM,EACb,GAAG,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,GACpB,IAAI;IAYP,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAKjE,cAAc;;;;IAId,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS;IAIhD,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKlC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI;IAIzC;;;OAGG;IACH,eAAe,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO;IAMtD,gBAAgB,CAAC,aAAa,EAAE,aAAa,GAAG,IAAI;IAIpD,kBAAkB,CAAC,aAAa,EAAE,aAAa;IAI/C;;;;OAIG;IACG,oBAAoB,CACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,SAAS,iBAAiB,EAAE,EAClC,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,GAAG,CAAC;CAuChB"}
@@ -65,7 +65,7 @@ var InspectorDelegate = class {
65
65
  * CustomQueryTransformer. This is primarily used by the inspector to transform
66
66
  * queries for analysis.
67
67
  */
68
- async transformCustomQuery(name, args, headerOptions, userQueryURL) {
68
+ async transformCustomQuery(name, args, ctx) {
69
69
  assert(this.#customQueryTransformer, "Custom query transformation requested but no CustomQueryTransformer is configured");
70
70
  const queries = [{
71
71
  id: hashOfNameAndArgs(name, args),
@@ -74,9 +74,9 @@ var InspectorDelegate = class {
74
74
  args,
75
75
  clientState: {}
76
76
  }];
77
- const results = await this.#customQueryTransformer.transform(headerOptions, queries, userQueryURL);
78
- if ("kind" in results) throw new ProtocolErrorWithLevel(results, "warn");
79
- const result = results[0];
77
+ const results = await this.#customQueryTransformer.transform(ctx, queries);
78
+ if ("kind" in results.result) throw new ProtocolErrorWithLevel(results.result, "warn");
79
+ const result = results.result[0];
80
80
  if (!result) throw new Error("No transformation result returned");
81
81
  if ("error" in result) {
82
82
  const message = result.message ?? "Unknown application error from custom query";
@@ -1 +1 @@
1
- {"version":3,"file":"inspector-delegate.js","names":["#globalMetrics","#perQueryServerMetrics","#queryIDToAST","#customQueryTransformer"],"sources":["../../../../../zero-cache/src/server/inspector-delegate.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport type {ReadonlyJSONValue} from '../../../shared/src/json.ts';\nimport {mapValues} from '../../../shared/src/objects.ts';\nimport {TDigest} from '../../../shared/src/tdigest.ts';\nimport type {AST} from '../../../zero-protocol/src/ast.ts';\nimport type {ServerMetrics as ServerMetricsJSON} from '../../../zero-protocol/src/inspect-down.ts';\nimport {hashOfNameAndArgs} from '../../../zero-protocol/src/query-hash.ts';\nimport {\n isServerMetric,\n type MetricMap,\n type MetricsDelegate,\n} from '../../../zql/src/query/metrics-delegate.ts';\nimport {isDevelopmentMode} from '../config/normalize.ts';\nimport type {CustomQueryTransformer} from '../custom-queries/transform-query.ts';\nimport type {HeaderOptions} from '../custom/fetch.ts';\nimport type {CustomQueryRecord} from '../services/view-syncer/schema/types.ts';\nimport {ProtocolErrorWithLevel} from '../types/error-with-level.ts';\n\n/**\n * Server-side metrics collected for queries during materialization and update.\n * These metrics are reported via the inspector and complement client-side metrics.\n */\nexport type ServerMetrics = {\n 'query-materialization-server': TDigest;\n 'query-update-server': TDigest;\n};\n\ntype ClientGroupID = string;\n\n/**\n * Set of authenticated client group IDs. We keep this outside of the class to\n * share this state across all instances of the InspectorDelegate.\n */\nconst authenticatedClientGroupIDs = new Set<ClientGroupID>();\n\nexport class InspectorDelegate implements MetricsDelegate {\n readonly #globalMetrics: ServerMetrics = newMetrics();\n readonly #perQueryServerMetrics = new Map<string, ServerMetrics>();\n readonly #queryIDToAST: Map<string, AST> = new Map();\n readonly #customQueryTransformer: CustomQueryTransformer | undefined;\n\n constructor(customQueryTransformer: CustomQueryTransformer | undefined) {\n this.#customQueryTransformer = customQueryTransformer;\n }\n\n addMetric<K extends keyof MetricMap>(\n metric: K,\n value: number,\n ...args: MetricMap[K]\n ): void {\n assert(isServerMetric(metric), `Invalid server metric: ${metric}`);\n const queryID = args[0];\n let serverMetrics = this.#perQueryServerMetrics.get(queryID);\n if (!serverMetrics) {\n serverMetrics = newMetrics();\n this.#perQueryServerMetrics.set(queryID, serverMetrics);\n }\n serverMetrics[metric].add(value);\n this.#globalMetrics[metric].add(value);\n }\n\n getMetricsJSONForQuery(queryID: string): ServerMetricsJSON | null {\n const serverMetrics = this.#perQueryServerMetrics.get(queryID);\n return serverMetrics ? mapValues(serverMetrics, v => v.toJSON()) : null;\n }\n\n getMetricsJSON() {\n return mapValues(this.#globalMetrics, v => v.toJSON());\n }\n\n getASTForQuery(queryID: string): AST | undefined {\n return this.#queryIDToAST.get(queryID);\n }\n\n removeQuery(queryID: string): void {\n this.#perQueryServerMetrics.delete(queryID);\n this.#queryIDToAST.delete(queryID);\n }\n\n addQuery(queryID: string, ast: AST): void {\n this.#queryIDToAST.set(queryID, ast);\n }\n\n /**\n * Check if the client is authenticated. We only require authentication once\n * per \"worker\".\n */\n isAuthenticated(clientGroupID: ClientGroupID): boolean {\n return (\n isDevelopmentMode() || authenticatedClientGroupIDs.has(clientGroupID)\n );\n }\n\n setAuthenticated(clientGroupID: ClientGroupID): void {\n authenticatedClientGroupIDs.add(clientGroupID);\n }\n\n clearAuthenticated(clientGroupID: ClientGroupID) {\n authenticatedClientGroupIDs.delete(clientGroupID);\n }\n\n /**\n * Transforms a single custom query by name and args using the configured\n * CustomQueryTransformer. This is primarily used by the inspector to transform\n * queries for analysis.\n */\n async transformCustomQuery(\n name: string,\n args: readonly ReadonlyJSONValue[],\n headerOptions: HeaderOptions,\n userQueryURL: string | undefined,\n ): Promise<AST> {\n assert(\n this.#customQueryTransformer,\n 'Custom query transformation requested but no CustomQueryTransformer is configured',\n );\n\n // Create a fake CustomQueryRecord for the single query\n const queryID = hashOfNameAndArgs(name, args);\n const queries: CustomQueryRecord[] = [\n {\n id: queryID,\n type: 'custom',\n name,\n args,\n clientState: {},\n },\n ];\n\n const results = await this.#customQueryTransformer.transform(\n headerOptions,\n queries,\n userQueryURL,\n );\n\n if ('kind' in results) {\n throw new ProtocolErrorWithLevel(results, 'warn');\n }\n\n const result = results[0];\n if (!result) {\n throw new Error('No transformation result returned');\n }\n\n if ('error' in result) {\n const message =\n result.message ?? 'Unknown application error from custom query';\n throw new Error(\n `Error transforming custom query ${name} (${result.error}): ${message} ${JSON.stringify(result.details)}`,\n );\n }\n\n return result.transformedAst;\n }\n}\n\nfunction newMetrics(): ServerMetrics {\n return {\n 'query-materialization-server': new TDigest(),\n 'query-update-server': new TDigest(),\n };\n}\n"],"mappings":";;;;;;;;;;;;AAiCA,IAAM,8CAA8B,IAAI,KAAoB;AAE5D,IAAa,oBAAb,MAA0D;CACxD,iBAAyC,YAAY;CACrD,yCAAkC,IAAI,KAA4B;CAClE,gCAA2C,IAAI,KAAK;CACpD;CAEA,YAAY,wBAA4D;AACtE,QAAA,yBAA+B;;CAGjC,UACE,QACA,OACA,GAAG,MACG;AACN,SAAO,eAAe,OAAO,EAAE,0BAA0B,SAAS;EAClE,MAAM,UAAU,KAAK;EACrB,IAAI,gBAAgB,MAAA,sBAA4B,IAAI,QAAQ;AAC5D,MAAI,CAAC,eAAe;AAClB,mBAAgB,YAAY;AAC5B,SAAA,sBAA4B,IAAI,SAAS,cAAc;;AAEzD,gBAAc,QAAQ,IAAI,MAAM;AAChC,QAAA,cAAoB,QAAQ,IAAI,MAAM;;CAGxC,uBAAuB,SAA2C;EAChE,MAAM,gBAAgB,MAAA,sBAA4B,IAAI,QAAQ;AAC9D,SAAO,gBAAgB,UAAU,gBAAe,MAAK,EAAE,QAAQ,CAAC,GAAG;;CAGrE,iBAAiB;AACf,SAAO,UAAU,MAAA,gBAAqB,MAAK,EAAE,QAAQ,CAAC;;CAGxD,eAAe,SAAkC;AAC/C,SAAO,MAAA,aAAmB,IAAI,QAAQ;;CAGxC,YAAY,SAAuB;AACjC,QAAA,sBAA4B,OAAO,QAAQ;AAC3C,QAAA,aAAmB,OAAO,QAAQ;;CAGpC,SAAS,SAAiB,KAAgB;AACxC,QAAA,aAAmB,IAAI,SAAS,IAAI;;;;;;CAOtC,gBAAgB,eAAuC;AACrD,SACE,mBAAmB,IAAI,4BAA4B,IAAI,cAAc;;CAIzE,iBAAiB,eAAoC;AACnD,8BAA4B,IAAI,cAAc;;CAGhD,mBAAmB,eAA8B;AAC/C,8BAA4B,OAAO,cAAc;;;;;;;CAQnD,MAAM,qBACJ,MACA,MACA,eACA,cACc;AACd,SACE,MAAA,wBACA,oFACD;EAID,MAAM,UAA+B,CACnC;GACE,IAHY,kBAAkB,MAAM,KAAK;GAIzC,MAAM;GACN;GACA;GACA,aAAa,EAAE;GAChB,CACF;EAED,MAAM,UAAU,MAAM,MAAA,uBAA6B,UACjD,eACA,SACA,aACD;AAED,MAAI,UAAU,QACZ,OAAM,IAAI,uBAAuB,SAAS,OAAO;EAGnD,MAAM,SAAS,QAAQ;AACvB,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,oCAAoC;AAGtD,MAAI,WAAW,QAAQ;GACrB,MAAM,UACJ,OAAO,WAAW;AACpB,SAAM,IAAI,MACR,mCAAmC,KAAK,IAAI,OAAO,MAAM,KAAK,QAAQ,GAAG,KAAK,UAAU,OAAO,QAAQ,GACxG;;AAGH,SAAO,OAAO;;;AAIlB,SAAS,aAA4B;AACnC,QAAO;EACL,gCAAgC,IAAI,SAAS;EAC7C,uBAAuB,IAAI,SAAS;EACrC"}
1
+ {"version":3,"file":"inspector-delegate.js","names":["#globalMetrics","#perQueryServerMetrics","#queryIDToAST","#customQueryTransformer"],"sources":["../../../../../zero-cache/src/server/inspector-delegate.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport type {ReadonlyJSONValue} from '../../../shared/src/json.ts';\nimport {mapValues} from '../../../shared/src/objects.ts';\nimport {TDigest} from '../../../shared/src/tdigest.ts';\nimport type {AST} from '../../../zero-protocol/src/ast.ts';\nimport type {ServerMetrics as ServerMetricsJSON} from '../../../zero-protocol/src/inspect-down.ts';\nimport {hashOfNameAndArgs} from '../../../zero-protocol/src/query-hash.ts';\nimport {\n isServerMetric,\n type MetricMap,\n type MetricsDelegate,\n} from '../../../zql/src/query/metrics-delegate.ts';\nimport {isDevelopmentMode} from '../config/normalize.ts';\nimport type {CustomQueryTransformer} from '../custom-queries/transform-query.ts';\nimport type {ConnectionContext} from '../services/view-syncer/connection-context-manager.ts';\nimport type {CustomQueryRecord} from '../services/view-syncer/schema/types.ts';\nimport {ProtocolErrorWithLevel} from '../types/error-with-level.ts';\n\n/**\n * Server-side metrics collected for queries during materialization and update.\n * These metrics are reported via the inspector and complement client-side metrics.\n */\nexport type ServerMetrics = {\n 'query-materialization-server': TDigest;\n 'query-update-server': TDigest;\n};\n\ntype ClientGroupID = string;\n\n/**\n * Set of authenticated client group IDs. We keep this outside of the class to\n * share this state across all instances of the InspectorDelegate.\n */\nconst authenticatedClientGroupIDs = new Set<ClientGroupID>();\n\nexport class InspectorDelegate implements MetricsDelegate {\n readonly #globalMetrics: ServerMetrics = newMetrics();\n readonly #perQueryServerMetrics = new Map<string, ServerMetrics>();\n readonly #queryIDToAST: Map<string, AST> = new Map();\n readonly #customQueryTransformer: CustomQueryTransformer | undefined;\n\n constructor(customQueryTransformer: CustomQueryTransformer | undefined) {\n this.#customQueryTransformer = customQueryTransformer;\n }\n\n addMetric<K extends keyof MetricMap>(\n metric: K,\n value: number,\n ...args: MetricMap[K]\n ): void {\n assert(isServerMetric(metric), `Invalid server metric: ${metric}`);\n const queryID = args[0];\n let serverMetrics = this.#perQueryServerMetrics.get(queryID);\n if (!serverMetrics) {\n serverMetrics = newMetrics();\n this.#perQueryServerMetrics.set(queryID, serverMetrics);\n }\n serverMetrics[metric].add(value);\n this.#globalMetrics[metric].add(value);\n }\n\n getMetricsJSONForQuery(queryID: string): ServerMetricsJSON | null {\n const serverMetrics = this.#perQueryServerMetrics.get(queryID);\n return serverMetrics ? mapValues(serverMetrics, v => v.toJSON()) : null;\n }\n\n getMetricsJSON() {\n return mapValues(this.#globalMetrics, v => v.toJSON());\n }\n\n getASTForQuery(queryID: string): AST | undefined {\n return this.#queryIDToAST.get(queryID);\n }\n\n removeQuery(queryID: string): void {\n this.#perQueryServerMetrics.delete(queryID);\n this.#queryIDToAST.delete(queryID);\n }\n\n addQuery(queryID: string, ast: AST): void {\n this.#queryIDToAST.set(queryID, ast);\n }\n\n /**\n * Check if the client is authenticated. We only require authentication once\n * per \"worker\".\n */\n isAuthenticated(clientGroupID: ClientGroupID): boolean {\n return (\n isDevelopmentMode() || authenticatedClientGroupIDs.has(clientGroupID)\n );\n }\n\n setAuthenticated(clientGroupID: ClientGroupID): void {\n authenticatedClientGroupIDs.add(clientGroupID);\n }\n\n clearAuthenticated(clientGroupID: ClientGroupID) {\n authenticatedClientGroupIDs.delete(clientGroupID);\n }\n\n /**\n * Transforms a single custom query by name and args using the configured\n * CustomQueryTransformer. This is primarily used by the inspector to transform\n * queries for analysis.\n */\n async transformCustomQuery(\n name: string,\n args: readonly ReadonlyJSONValue[],\n ctx: ConnectionContext,\n ): Promise<AST> {\n assert(\n this.#customQueryTransformer,\n 'Custom query transformation requested but no CustomQueryTransformer is configured',\n );\n\n // Create a fake CustomQueryRecord for the single query\n const queryID = hashOfNameAndArgs(name, args);\n const queries: CustomQueryRecord[] = [\n {\n id: queryID,\n type: 'custom',\n name,\n args,\n clientState: {},\n },\n ];\n\n const results = await this.#customQueryTransformer.transform(ctx, queries);\n\n if ('kind' in results.result) {\n throw new ProtocolErrorWithLevel(results.result, 'warn');\n }\n\n const result = results.result[0];\n if (!result) {\n throw new Error('No transformation result returned');\n }\n\n if ('error' in result) {\n const message =\n result.message ?? 'Unknown application error from custom query';\n throw new Error(\n `Error transforming custom query ${name} (${result.error}): ${message} ${JSON.stringify(result.details)}`,\n );\n }\n\n return result.transformedAst;\n }\n}\n\nfunction newMetrics(): ServerMetrics {\n return {\n 'query-materialization-server': new TDigest(),\n 'query-update-server': new TDigest(),\n };\n}\n"],"mappings":";;;;;;;;;;;;AAiCA,IAAM,8CAA8B,IAAI,KAAoB;AAE5D,IAAa,oBAAb,MAA0D;CACxD,iBAAyC,YAAY;CACrD,yCAAkC,IAAI,KAA4B;CAClE,gCAA2C,IAAI,KAAK;CACpD;CAEA,YAAY,wBAA4D;AACtE,QAAA,yBAA+B;;CAGjC,UACE,QACA,OACA,GAAG,MACG;AACN,SAAO,eAAe,OAAO,EAAE,0BAA0B,SAAS;EAClE,MAAM,UAAU,KAAK;EACrB,IAAI,gBAAgB,MAAA,sBAA4B,IAAI,QAAQ;AAC5D,MAAI,CAAC,eAAe;AAClB,mBAAgB,YAAY;AAC5B,SAAA,sBAA4B,IAAI,SAAS,cAAc;;AAEzD,gBAAc,QAAQ,IAAI,MAAM;AAChC,QAAA,cAAoB,QAAQ,IAAI,MAAM;;CAGxC,uBAAuB,SAA2C;EAChE,MAAM,gBAAgB,MAAA,sBAA4B,IAAI,QAAQ;AAC9D,SAAO,gBAAgB,UAAU,gBAAe,MAAK,EAAE,QAAQ,CAAC,GAAG;;CAGrE,iBAAiB;AACf,SAAO,UAAU,MAAA,gBAAqB,MAAK,EAAE,QAAQ,CAAC;;CAGxD,eAAe,SAAkC;AAC/C,SAAO,MAAA,aAAmB,IAAI,QAAQ;;CAGxC,YAAY,SAAuB;AACjC,QAAA,sBAA4B,OAAO,QAAQ;AAC3C,QAAA,aAAmB,OAAO,QAAQ;;CAGpC,SAAS,SAAiB,KAAgB;AACxC,QAAA,aAAmB,IAAI,SAAS,IAAI;;;;;;CAOtC,gBAAgB,eAAuC;AACrD,SACE,mBAAmB,IAAI,4BAA4B,IAAI,cAAc;;CAIzE,iBAAiB,eAAoC;AACnD,8BAA4B,IAAI,cAAc;;CAGhD,mBAAmB,eAA8B;AAC/C,8BAA4B,OAAO,cAAc;;;;;;;CAQnD,MAAM,qBACJ,MACA,MACA,KACc;AACd,SACE,MAAA,wBACA,oFACD;EAID,MAAM,UAA+B,CACnC;GACE,IAHY,kBAAkB,MAAM,KAAK;GAIzC,MAAM;GACN;GACA;GACA,aAAa,EAAE;GAChB,CACF;EAED,MAAM,UAAU,MAAM,MAAA,uBAA6B,UAAU,KAAK,QAAQ;AAE1E,MAAI,UAAU,QAAQ,OACpB,OAAM,IAAI,uBAAuB,QAAQ,QAAQ,OAAO;EAG1D,MAAM,SAAS,QAAQ,OAAO;AAC9B,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,oCAAoC;AAGtD,MAAI,WAAW,QAAQ;GACrB,MAAM,UACJ,OAAO,WAAW;AACpB,SAAM,IAAI,MACR,mCAAmC,KAAK,IAAI,OAAO,MAAM,KAAK,QAAQ,GAAG,KAAK,UAAU,OAAO,QAAQ,GACxG;;AAGH,SAAO,OAAO;;;AAIlB,SAAS,aAA4B;AACnC,QAAO;EACL,gCAAgC,IAAI,SAAS;EAC7C,uBAAuB,IAAI,SAAS;EACrC"}
@@ -1 +1 @@
1
- {"version":3,"file":"reaper.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/reaper.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAQ/B,wBAA8B,SAAS,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CA+Bf"}
1
+ {"version":3,"file":"reaper.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/reaper.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAQ/B,wBAA8B,SAAS,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAgCf"}
@@ -24,7 +24,10 @@ async function runWorker(parent, env, ...argv) {
24
24
  startAnonymousTelemetry(lc, config);
25
25
  const { cvr } = config;
26
26
  const shard = getShardID(config);
27
- const cvrDB = pgClient(lc, cvr.db, { connection: { ["application_name"]: `zero-sync-cvr-purger` } });
27
+ const cvrDB = pgClient(lc, cvr.db, {
28
+ max: 1,
29
+ connection: { ["application_name"]: `zero-sync-cvr-purger` }
30
+ });
28
31
  await initViewSyncerSchema(lc, cvrDB, shard);
29
32
  parent.send(["ready", { ready: true }]);
30
33
  return runUntilKilled(lc, parent, new CVRPurger(lc, cvrDB, shard, {
@@ -1 +1 @@
1
- {"version":3,"file":"reaper.js","names":[],"sources":["../../../../../zero-cache/src/server/reaper.ts"],"sourcesContent":["import {must} from '../../../shared/src/must.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {initEventSink} from '../observability/events.ts';\nimport {exitAfter, runUntilKilled} from '../services/life-cycle.ts';\nimport {CVRPurger} from '../services/view-syncer/cvr-purger.ts';\nimport {ActiveUsersGauge} from '../services/view-syncer/active-users-gauge.ts';\nimport {initViewSyncerSchema} from '../services/view-syncer/schema/init.ts';\nimport {pgClient} from '../types/pg.ts';\nimport {\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {getShardID} from '../types/shards.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\nimport {startAnonymousTelemetry} from './anonymous-otel-start.ts';\n\nconst MS_PER_HOUR = 1000 * 60 * 60;\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...argv: string[]\n): Promise<void> {\n const config = getNormalizedZeroConfig({env, argv});\n\n startOtelAuto(createLogContext(config, {worker: 'reaper'}, false));\n const lc = createLogContext(config, {worker: 'reaper'}, true);\n initEventSink(lc, config);\n startAnonymousTelemetry(lc, config);\n\n const {cvr} = config;\n const shard = getShardID(config);\n const cvrDB = pgClient(lc, cvr.db, {\n connection: {['application_name']: `zero-sync-cvr-purger`},\n });\n await initViewSyncerSchema(lc, cvrDB, shard);\n parent.send(['ready', {ready: true}]);\n\n return runUntilKilled(\n lc,\n parent,\n new CVRPurger(lc, cvrDB, shard, {\n inactivityThresholdMs:\n cvr.garbageCollectionInactivityThresholdHours * MS_PER_HOUR,\n initialBatchSize: cvr.garbageCollectionInitialBatchSize,\n initialIntervalMs: cvr.garbageCollectionInitialIntervalSeconds * 1000,\n }),\n // Periodically computes and exports active users gauge to anonymous telemetry\n new ActiveUsersGauge(lc, cvrDB, shard, {\n // Default 10minutes refresh; can be made configurable later if needed\n updateIntervalMs: 10 * 60 * 1000,\n }),\n );\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(() =>\n runWorker(must(parentWorker), process.env, ...process.argv.slice(2)),\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAkBA,IAAM,cAAc,MAAO,KAAK;AAEhC,eAA8B,UAC5B,QACA,KACA,GAAG,MACY;CACf,MAAM,SAAS,wBAAwB;EAAC;EAAK;EAAK,CAAC;AAEnD,eAAc,iBAAiB,QAAQ,EAAC,QAAQ,UAAS,EAAE,MAAM,CAAC;CAClE,MAAM,KAAK,iBAAiB,QAAQ,EAAC,QAAQ,UAAS,EAAE,KAAK;AAC7D,eAAc,IAAI,OAAO;AACzB,yBAAwB,IAAI,OAAO;CAEnC,MAAM,EAAC,QAAO;CACd,MAAM,QAAQ,WAAW,OAAO;CAChC,MAAM,QAAQ,SAAS,IAAI,IAAI,IAAI,EACjC,YAAY,GAAE,qBAAqB,wBAAuB,EAC3D,CAAC;AACF,OAAM,qBAAqB,IAAI,OAAO,MAAM;AAC5C,QAAO,KAAK,CAAC,SAAS,EAAC,OAAO,MAAK,CAAC,CAAC;AAErC,QAAO,eACL,IACA,QACA,IAAI,UAAU,IAAI,OAAO,OAAO;EAC9B,uBACE,IAAI,4CAA4C;EAClD,kBAAkB,IAAI;EACtB,mBAAmB,IAAI,0CAA0C;EAClE,CAAC,EAEF,IAAI,iBAAiB,IAAI,OAAO,OAAO,EAErC,kBAAkB,MAAU,KAC7B,CAAC,CACH;;AAIH,IAAI,CAAC,mBAAmB,CACjB,iBACH,UAAU,KAAK,aAAa,EAAE,QAAQ,KAAK,GAAG,QAAQ,KAAK,MAAM,EAAE,CAAC,CACrE"}
1
+ {"version":3,"file":"reaper.js","names":[],"sources":["../../../../../zero-cache/src/server/reaper.ts"],"sourcesContent":["import {must} from '../../../shared/src/must.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {initEventSink} from '../observability/events.ts';\nimport {exitAfter, runUntilKilled} from '../services/life-cycle.ts';\nimport {CVRPurger} from '../services/view-syncer/cvr-purger.ts';\nimport {ActiveUsersGauge} from '../services/view-syncer/active-users-gauge.ts';\nimport {initViewSyncerSchema} from '../services/view-syncer/schema/init.ts';\nimport {pgClient} from '../types/pg.ts';\nimport {\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {getShardID} from '../types/shards.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\nimport {startAnonymousTelemetry} from './anonymous-otel-start.ts';\n\nconst MS_PER_HOUR = 1000 * 60 * 60;\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...argv: string[]\n): Promise<void> {\n const config = getNormalizedZeroConfig({env, argv});\n\n startOtelAuto(createLogContext(config, {worker: 'reaper'}, false));\n const lc = createLogContext(config, {worker: 'reaper'}, true);\n initEventSink(lc, config);\n startAnonymousTelemetry(lc, config);\n\n const {cvr} = config;\n const shard = getShardID(config);\n const cvrDB = pgClient(lc, cvr.db, {\n max: 1,\n connection: {['application_name']: `zero-sync-cvr-purger`},\n });\n await initViewSyncerSchema(lc, cvrDB, shard);\n parent.send(['ready', {ready: true}]);\n\n return runUntilKilled(\n lc,\n parent,\n new CVRPurger(lc, cvrDB, shard, {\n inactivityThresholdMs:\n cvr.garbageCollectionInactivityThresholdHours * MS_PER_HOUR,\n initialBatchSize: cvr.garbageCollectionInitialBatchSize,\n initialIntervalMs: cvr.garbageCollectionInitialIntervalSeconds * 1000,\n }),\n // Periodically computes and exports active users gauge to anonymous telemetry\n new ActiveUsersGauge(lc, cvrDB, shard, {\n // Default 10minutes refresh; can be made configurable later if needed\n updateIntervalMs: 10 * 60 * 1000,\n }),\n );\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(() =>\n runWorker(must(parentWorker), process.env, ...process.argv.slice(2)),\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAkBA,IAAM,cAAc,MAAO,KAAK;AAEhC,eAA8B,UAC5B,QACA,KACA,GAAG,MACY;CACf,MAAM,SAAS,wBAAwB;EAAC;EAAK;EAAK,CAAC;AAEnD,eAAc,iBAAiB,QAAQ,EAAC,QAAQ,UAAS,EAAE,MAAM,CAAC;CAClE,MAAM,KAAK,iBAAiB,QAAQ,EAAC,QAAQ,UAAS,EAAE,KAAK;AAC7D,eAAc,IAAI,OAAO;AACzB,yBAAwB,IAAI,OAAO;CAEnC,MAAM,EAAC,QAAO;CACd,MAAM,QAAQ,WAAW,OAAO;CAChC,MAAM,QAAQ,SAAS,IAAI,IAAI,IAAI;EACjC,KAAK;EACL,YAAY,GAAE,qBAAqB,wBAAuB;EAC3D,CAAC;AACF,OAAM,qBAAqB,IAAI,OAAO,MAAM;AAC5C,QAAO,KAAK,CAAC,SAAS,EAAC,OAAO,MAAK,CAAC,CAAC;AAErC,QAAO,eACL,IACA,QACA,IAAI,UAAU,IAAI,OAAO,OAAO;EAC9B,uBACE,IAAI,4CAA4C;EAClD,kBAAkB,IAAI;EACtB,mBAAmB,IAAI,0CAA0C;EAClE,CAAC,EAEF,IAAI,iBAAiB,IAAI,OAAO,OAAO,EAErC,kBAAkB,MAAU,KAC7B,CAAC,CACH;;AAIH,IAAI,CAAC,mBAAmB,CACjB,iBACH,UAAU,KAAK,aAAa,EAAE,QAAQ,KAAK,GAAG,QAAQ,KAAK,MAAM,EAAE,CAAC,CACrE"}
@@ -28,7 +28,7 @@ async function runWorker(parent, env) {
28
28
  const { port, lazyStartup } = config;
29
29
  const serverVersion = getServerVersion(config);
30
30
  lc.info?.(`starting server${!serverVersion ? "" : `@${serverVersion}`} `, {
31
- protocolVersion: 49,
31
+ protocolVersion: 50,
32
32
  taskID: config.taskID,
33
33
  app: config.app,
34
34
  shard: config.shard,
@@ -1 +1 @@
1
- {"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/syncer.ts"],"names":[],"mappings":"AAyBA,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AA8B/B,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CA+Jf"}
1
+ {"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/syncer.ts"],"names":[],"mappings":"AA4BA,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAmC/B,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAoMf"}
@@ -17,11 +17,13 @@ import { initEventSink } from "../observability/events.js";
17
17
  import { replicaFileModeSchema, replicaFileName } from "../workers/replicator.js";
18
18
  import { startAnonymousTelemetry } from "./anonymous-otel-start.js";
19
19
  import { DatabaseStorage } from "../../../zqlite/src/database-storage.js";
20
- import { AuthSessionImpl } from "../auth/auth.js";
20
+ import { ProtocolErrorWithLevel } from "../types/error-with-level.js";
21
21
  import { CustomQueryTransformer } from "../custom-queries/transform-query.js";
22
22
  import { MutagenService } from "../services/mutagen/mutagen.js";
23
23
  import { PusherService } from "../services/mutagen/pusher.js";
24
+ import { ConnectionContextManagerImpl } from "../services/view-syncer/connection-context-manager.js";
24
25
  import { ViewSyncerService } from "../services/view-syncer/view-syncer.js";
26
+ import { tokenConfigOptions, verifyToken } from "../auth/jwt.js";
25
27
  import { Syncer } from "../workers/syncer.js";
26
28
  import { InspectorDelegate } from "./inspector-delegate.js";
27
29
  import { isPriorityOpRunning, runPriorityOp } from "./priority-op.js";
@@ -38,6 +40,8 @@ function getCustomQueryConfig(config) {
38
40
  if (!queryConfig?.url) return;
39
41
  return {
40
42
  url: queryConfig.url,
43
+ apiKey: queryConfig.apiKey,
44
+ allowedClientHeaders: queryConfig.allowedClientHeaders,
41
45
  forwardCookies: queryConfig.forwardCookies ?? false
42
46
  };
43
47
  }
@@ -67,22 +71,41 @@ function runWorker(parent, env, ...args) {
67
71
  const operatorStorage = DatabaseStorage.create(lc, path.join(tmpDir, `sync-worker-${randomUUID()}`));
68
72
  const writeAuthzStorage = DatabaseStorage.create(lc, path.join(tmpDir, `mutagen-${randomUUID()}`));
69
73
  const shard = getShardID(config);
70
- const viewSyncerFactory = (id, sub, drainCoordinator, validateLegacyJWT) => {
74
+ const customQueryConfig = getCustomQueryConfig(config);
75
+ const pushConfig = config.push.url === void 0 && config.mutate.url === void 0 ? void 0 : {
76
+ ...config.push,
77
+ ...config.mutate,
78
+ url: must(config.push.url ?? config.mutate.url, "No push or mutate URL configured")
79
+ };
80
+ /** @deprecated used in JWT validation */
81
+ let validateLegacyJWT = void 0;
82
+ if (tokenConfigOptions(config.auth ?? {}).length === 1) validateLegacyJWT = async (token, { userID }) => {
83
+ if (!userID) throw new ProtocolErrorWithLevel({
84
+ kind: "Unauthorized",
85
+ message: "UserID is required for JWT validation.",
86
+ origin: "zeroCache"
87
+ }, "warn");
88
+ return {
89
+ type: "jwt",
90
+ raw: token,
91
+ decoded: await verifyToken(config.auth, token, {
92
+ subject: userID,
93
+ ...config.auth?.issuer && { issuer: config.auth.issuer },
94
+ ...config.auth?.audience && { audience: config.auth.audience }
95
+ })
96
+ };
97
+ };
98
+ const viewSyncerFactory = (id, sub, drainCoordinator) => {
71
99
  const logger = lc.withContext("component", "view-syncer").withContext("clientGroupID", id).withContext("instance", randomID());
100
+ const customQueryTransformer = customQueryConfig && new CustomQueryTransformer(logger, shard);
101
+ const contextManager = new ConnectionContextManagerImpl(logger, config.auth.revalidateIntervalSeconds, config.auth.retransformIntervalSeconds, customQueryConfig, pushConfig, validateLegacyJWT);
72
102
  lc.debug?.(`creating view syncer. Query Planner Enabled: ${config.enableQueryPlanner}`);
73
- const customQueryConfig = getCustomQueryConfig(config);
74
- const customQueryTransformer = customQueryConfig && new CustomQueryTransformer(logger, customQueryConfig, shard);
75
103
  const inspectorDelegate = new InspectorDelegate(customQueryTransformer);
76
104
  const priorityOpRunningYieldThresholdMs = Math.max(config.yieldThresholdMs / 4, 2);
77
105
  const normalYieldThresholdMs = Math.max(config.yieldThresholdMs, 2);
78
- const authSession = new AuthSessionImpl(logger.withContext("component", "auth-session"), id, validateLegacyJWT);
79
- return new ViewSyncerService(config, logger, shard, config.taskID, id, cvrDB, new PipelineDriver(logger, config.log, new Snapshotter(logger, replicaFile, shard), shard, operatorStorage.createClientGroupStorage(id), id, inspectorDelegate, () => isPriorityOpRunning() ? priorityOpRunningYieldThresholdMs : normalYieldThresholdMs, config.enableQueryPlanner, config), sub, drainCoordinator, config.log.slowHydrateThreshold, inspectorDelegate, customQueryTransformer, runPriorityOp, authSession);
106
+ return new ViewSyncerService(config, logger, shard, config.taskID, id, cvrDB, new PipelineDriver(logger, config.log, new Snapshotter(logger, replicaFile, shard), shard, operatorStorage.createClientGroupStorage(id), id, inspectorDelegate, () => isPriorityOpRunning() ? priorityOpRunningYieldThresholdMs : normalYieldThresholdMs, config.enableQueryPlanner, config), sub, drainCoordinator, config.log.slowHydrateThreshold, inspectorDelegate, contextManager, customQueryTransformer, runPriorityOp);
80
107
  };
81
- const syncer = new Syncer(lc, config, viewSyncerFactory, upstreamDB ? (id) => new MutagenService(lc.withContext("component", "mutagen").withContext("clientGroupID", id), shard, id, upstreamDB, config, writeAuthzStorage) : void 0, config.push.url === void 0 && config.mutate.url === void 0 ? void 0 : (id) => new PusherService(config, {
82
- ...config.push,
83
- ...config.mutate,
84
- url: must(config.push.url ?? config.mutate.url, "No push or mutate URL configured")
85
- }, lc.withContext("clientGroupID", id), id), parent);
108
+ const syncer = new Syncer(lc, config, viewSyncerFactory, upstreamDB ? (id) => new MutagenService(lc.withContext("component", "mutagen").withContext("clientGroupID", id), shard, id, upstreamDB, config, writeAuthzStorage) : void 0, pushConfig === void 0 ? void 0 : (id, contextManager) => new PusherService(config, lc.withContext("clientGroupID", id), id, contextManager), parent, validateLegacyJWT);
86
109
  startAnonymousTelemetry(lc, config);
87
110
  dbWarmup.then(() => parent.send(["ready", { ready: true }]));
88
111
  return runUntilKilled(lc, parent, syncer);