@rocicorp/zero 1.11.3 → 1.11.5

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 (192) hide show
  1. package/out/replicache/src/sync/request-id.js +2 -2
  2. package/out/replicache/src/sync/request-id.js.map +1 -1
  3. package/out/shared/src/bigint-json.d.ts.map +1 -1
  4. package/out/shared/src/bigint-json.js +1 -0
  5. package/out/shared/src/bigint-json.js.map +1 -1
  6. package/out/shared/src/headers.d.ts +12 -0
  7. package/out/shared/src/headers.d.ts.map +1 -0
  8. package/out/shared/src/headers.js +20 -0
  9. package/out/shared/src/headers.js.map +1 -0
  10. package/out/shared/src/random-uint64.d.ts.map +1 -1
  11. package/out/shared/src/random-uint64.js +2 -2
  12. package/out/shared/src/random-uint64.js.map +1 -1
  13. package/out/shared/src/random-values.d.ts +14 -1
  14. package/out/shared/src/random-values.d.ts.map +1 -1
  15. package/out/shared/src/random-values.js +24 -4
  16. package/out/shared/src/random-values.js.map +1 -1
  17. package/out/zero/package.js +1 -1
  18. package/out/zero/package.js.map +1 -1
  19. package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
  20. package/out/zero-cache/src/auth/auth.js +5 -0
  21. package/out/zero-cache/src/auth/auth.js.map +1 -1
  22. package/out/zero-cache/src/config/zero-config.d.ts +21 -0
  23. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  24. package/out/zero-cache/src/config/zero-config.js +47 -9
  25. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  26. package/out/zero-cache/src/custom-queries/transform-query.js +2 -2
  27. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  28. package/out/zero-cache/src/db/pg-to-lite.d.ts.map +1 -1
  29. package/out/zero-cache/src/db/pg-to-lite.js +3 -2
  30. package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
  31. package/out/zero-cache/src/db/pg-type-parser.d.ts +13 -0
  32. package/out/zero-cache/src/db/pg-type-parser.d.ts.map +1 -1
  33. package/out/zero-cache/src/db/pg-type-parser.js +55 -2
  34. package/out/zero-cache/src/db/pg-type-parser.js.map +1 -1
  35. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  36. package/out/zero-cache/src/server/change-streamer.js +5 -2
  37. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  38. package/out/zero-cache/src/server/inspector-delegate.d.ts +2 -25
  39. package/out/zero-cache/src/server/inspector-delegate.d.ts.map +1 -1
  40. package/out/zero-cache/src/server/inspector-delegate.js +6 -29
  41. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  42. package/out/zero-cache/src/server/main.d.ts.map +1 -1
  43. package/out/zero-cache/src/server/main.js +5 -0
  44. package/out/zero-cache/src/server/main.js.map +1 -1
  45. package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
  46. package/out/zero-cache/src/server/replicator.js +3 -2
  47. package/out/zero-cache/src/server/replicator.js.map +1 -1
  48. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  49. package/out/zero-cache/src/server/syncer.js +5 -4
  50. package/out/zero-cache/src/server/syncer.js.map +1 -1
  51. package/out/zero-cache/src/server/worker-dispatcher.d.ts.map +1 -1
  52. package/out/zero-cache/src/server/worker-dispatcher.js +2 -1
  53. package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
  54. package/out/zero-cache/src/services/admin-auth.d.ts +22 -0
  55. package/out/zero-cache/src/services/admin-auth.d.ts.map +1 -0
  56. package/out/zero-cache/src/services/admin-auth.js +42 -0
  57. package/out/zero-cache/src/services/admin-auth.js.map +1 -0
  58. package/out/zero-cache/src/services/auth-attempt-counter.d.ts +33 -0
  59. package/out/zero-cache/src/services/auth-attempt-counter.d.ts.map +1 -0
  60. package/out/zero-cache/src/services/auth-attempt-counter.js +61 -0
  61. package/out/zero-cache/src/services/auth-attempt-counter.js.map +1 -0
  62. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +1 -1
  63. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +7 -1
  64. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  65. package/out/zero-cache/src/services/change-source/pg/change-source.js +110 -13
  66. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  67. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +1 -1
  68. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +1 -1
  69. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +15 -8
  70. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
  71. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +38 -5
  72. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  73. package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
  74. package/out/zero-cache/src/services/change-source/pg/schema/init.js +10 -1
  75. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  76. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +4 -0
  77. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
  78. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +10 -3
  79. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  80. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +7 -1
  81. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  82. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +23 -2
  83. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  84. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +2 -2
  85. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  86. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +30 -9
  87. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  88. package/out/zero-cache/src/services/change-streamer/change-streamer.js +10 -3
  89. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  90. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts +1 -0
  91. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts.map +1 -1
  92. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +1 -1
  93. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
  94. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  95. package/out/zero-cache/src/services/heapz.d.ts.map +1 -1
  96. package/out/zero-cache/src/services/heapz.js +4 -5
  97. package/out/zero-cache/src/services/heapz.js.map +1 -1
  98. package/out/zero-cache/src/services/mutagen/pusher.js +1 -1
  99. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  100. package/out/zero-cache/src/services/plannerz.d.ts.map +1 -1
  101. package/out/zero-cache/src/services/plannerz.js +3 -7
  102. package/out/zero-cache/src/services/plannerz.js.map +1 -1
  103. package/out/zero-cache/src/services/profz.d.ts.map +1 -1
  104. package/out/zero-cache/src/services/profz.js +4 -13
  105. package/out/zero-cache/src/services/profz.js.map +1 -1
  106. package/out/zero-cache/src/services/replicator/change-processor.js +1 -2
  107. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  108. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  109. package/out/zero-cache/src/services/replicator/incremental-sync.js +3 -3
  110. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  111. package/out/zero-cache/src/services/statz.d.ts.map +1 -1
  112. package/out/zero-cache/src/services/statz.js +2 -7
  113. package/out/zero-cache/src/services/statz.js.map +1 -1
  114. package/out/zero-cache/src/services/view-syncer/client-handler.d.ts +16 -0
  115. package/out/zero-cache/src/services/view-syncer/client-handler.d.ts.map +1 -1
  116. package/out/zero-cache/src/services/view-syncer/client-handler.js +22 -1
  117. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  118. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +29 -4
  119. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts.map +1 -1
  120. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +40 -2
  121. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  122. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts +10 -2
  123. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
  124. package/out/zero-cache/src/services/view-syncer/cvr-store.js +54 -17
  125. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  126. package/out/zero-cache/src/services/view-syncer/cvr.d.ts +8 -0
  127. package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
  128. package/out/zero-cache/src/services/view-syncer/cvr.js +11 -0
  129. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  130. package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.d.ts +35 -0
  131. package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.d.ts.map +1 -0
  132. package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js +107 -0
  133. package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js.map +1 -0
  134. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts +2 -2
  135. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts.map +1 -1
  136. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +11 -6
  137. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  138. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  139. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +2 -1
  140. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  141. package/out/zero-cache/src/services/view-syncer/schema/cvr.d.ts +9 -0
  142. package/out/zero-cache/src/services/view-syncer/schema/cvr.d.ts.map +1 -1
  143. package/out/zero-cache/src/services/view-syncer/schema/cvr.js +4 -1
  144. package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
  145. package/out/zero-cache/src/services/view-syncer/schema/init.d.ts.map +1 -1
  146. package/out/zero-cache/src/services/view-syncer/schema/init.js +3 -0
  147. package/out/zero-cache/src/services/view-syncer/schema/init.js.map +1 -1
  148. package/out/zero-cache/src/services/view-syncer/snapshotter.js +1 -1
  149. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  150. package/out/zero-cache/src/services/view-syncer/view-syncer.js +96 -44
  151. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  152. package/out/zero-cache/src/types/client-group-log-id.d.ts +9 -0
  153. package/out/zero-cache/src/types/client-group-log-id.d.ts.map +1 -0
  154. package/out/zero-cache/src/types/client-group-log-id.js +16 -0
  155. package/out/zero-cache/src/types/client-group-log-id.js.map +1 -0
  156. package/out/zero-cache/src/types/lite.d.ts.map +1 -1
  157. package/out/zero-cache/src/types/lite.js +1 -0
  158. package/out/zero-cache/src/types/lite.js.map +1 -1
  159. package/out/zero-cache/src/types/pg.d.ts +13 -2
  160. package/out/zero-cache/src/types/pg.d.ts.map +1 -1
  161. package/out/zero-cache/src/types/pg.js +2 -2
  162. package/out/zero-cache/src/types/pg.js.map +1 -1
  163. package/out/zero-cache/src/workers/connection.d.ts +1 -0
  164. package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
  165. package/out/zero-cache/src/workers/connection.js +5 -1
  166. package/out/zero-cache/src/workers/connection.js.map +1 -1
  167. package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
  168. package/out/zero-cache/src/workers/syncer-ws-message-handler.js +12 -3
  169. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  170. package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
  171. package/out/zero-cache/src/workers/syncer.js +15 -14
  172. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  173. package/out/zero-client/src/client/inspector/inspector.d.ts +4 -0
  174. package/out/zero-client/src/client/inspector/inspector.d.ts.map +1 -1
  175. package/out/zero-client/src/client/inspector/inspector.js +4 -0
  176. package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
  177. package/out/zero-client/src/client/inspector/lazy-inspector.d.ts +10 -2
  178. package/out/zero-client/src/client/inspector/lazy-inspector.d.ts.map +1 -1
  179. package/out/zero-client/src/client/inspector/lazy-inspector.js +38 -31
  180. package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
  181. package/out/zero-client/src/client/inspector/query.d.ts +2 -2
  182. package/out/zero-client/src/client/inspector/query.d.ts.map +1 -1
  183. package/out/zero-client/src/client/inspector/query.js +4 -4
  184. package/out/zero-client/src/client/inspector/query.js.map +1 -1
  185. package/out/zero-client/src/client/version.js +1 -1
  186. package/out/zero-client/src/util/nanoid.js +2 -2
  187. package/out/zero-client/src/util/nanoid.js.map +1 -1
  188. package/out/zero-server/src/process-mutations.d.ts +10 -0
  189. package/out/zero-server/src/process-mutations.d.ts.map +1 -1
  190. package/out/zero-server/src/process-mutations.js +38 -5
  191. package/out/zero-server/src/process-mutations.js.map +1 -1
  192. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"pg-to-lite.js","names":[],"sources":["../../../../../zero-cache/src/db/pg-to-lite.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {ZERO_VERSION_COLUMN_NAME} from '../services/replicator/schema/constants.ts';\nimport {\n liteTypeString,\n liteTypeToZqlValueType,\n upstreamDataType,\n type LiteTypeString,\n} from '../types/lite.ts';\nimport {liteTableName} from '../types/names.ts';\nimport * as PostgresTypeClass from './postgres-type-class-enum.ts';\nimport {\n type ColumnSpec,\n type IndexSpec,\n type LiteIndexSpec,\n type LiteTableSpec,\n type TableSpec,\n} from './specs.ts';\n\n/**\n * Determines if a PostgreSQL column is an enum type.\n * This checks both the element type class (for arrays of enums) and the main type class.\n */\nexport function isEnumColumn(\n spec: Pick<ColumnSpec, 'pgTypeClass' | 'elemPgTypeClass'>,\n): boolean {\n return (spec.elemPgTypeClass ?? spec.pgTypeClass) === PostgresTypeClass.Enum;\n}\n\n/**\n * Determines if a PostgreSQL column is an array type.\n * In PostgreSQL's system, array columns have a non-null elemPgTypeClass.\n */\nexport function isArrayColumn(\n spec: Pick<ColumnSpec, 'elemPgTypeClass'>,\n): boolean {\n return spec.elemPgTypeClass !== null && spec.elemPgTypeClass !== undefined;\n}\n\nfunction zeroVersionColumnSpec(defaultVersion: string | undefined): ColumnSpec {\n return {\n pos: Number.MAX_SAFE_INTEGER, // i.e. last\n characterMaximumLength: null,\n dataType: 'text',\n notNull: false,\n dflt: !defaultVersion ? null : `'${defaultVersion}'`,\n elemPgTypeClass: null,\n };\n}\n\nexport function warnIfDataTypeSupported(\n lc: LogContext,\n liteTypeString: LiteTypeString,\n table: string,\n column: string,\n) {\n if (liteTypeToZqlValueType(liteTypeString) === undefined) {\n lc.warn?.(\n `\\n\\nWARNING: zero does not yet support the \"${upstreamDataType(\n liteTypeString,\n )}\" data type.\\n` +\n `The \"${table}\".\"${column}\" column will not be synced to clients.\\n\\n`,\n );\n }\n}\n\n// Numeric literals: integers and decimals, optionally negative\nconst NUMERIC_LITERAL_REGEX = /^-?\\d+(\\.\\d+)?$/;\n\n// Boolean literals (PG emits lowercase)\nconst BOOLEAN_LITERAL_REGEX = /^(true|false)$/;\n\n// Quoted string with type cast to a simple scalar type: 'value'::typename\n// For strings and certain incarnations of primitives (e.g. integers greater\n// than 2^31-1, Postgres' nodeToString() represents the values as type-casted\n// 'string' values, e.g. `'2147483648'::bigint`, `'foo'::text`.\n// Only matches simple type names (word characters) - array types like\n// `::text[]` won't match and will trigger backfill.\nconst QUOTED_STRING_WITH_CAST_REGEX = /^('.*')::(\\w+)$/;\n\n// Empty array constructor syntax: ARRAY[]::text[], ARRAY[]::integer[], etc.\n// Maps to '[]' (JSON empty array) in SQLite.\nconst EMPTY_ARRAY_CONSTRUCTOR_REGEX = /^ARRAY\\s*\\[\\s*\\]::\\w+\\[\\]$/i;\n\n// Empty array literal syntax: '{}'::text[], '{}'::integer[], etc.\n// Maps to '[]' (JSON empty array) in SQLite.\nconst EMPTY_ARRAY_LITERAL_REGEX = /^'\\{\\}'::\\w+\\[\\]$/;\n\n// Conservative allowlist approach for SQLite ADD COLUMN defaults.\n// We only allow patterns we know are safe. Everything else triggers\n// backfill from PostgreSQL, which correctly handles complex defaults.\n//\n// Note: We don't validate that the default value matches the column type\n// (e.g., that a numeric literal is used with a numeric column). PostgreSQL\n// already enforces this at schema definition time - you can't define\n// `ALTER TABLE foo ADD bar TEXT DEFAULT 123` in PG. So we trust that any\n// default we receive from the replication stream is type-compatible with\n// whatever we map the type to in SQLite.\n//\n// Example: `true`/`false` literals can only appear as defaults for boolean\n// columns in PG, so we don't need to check the column type before converting\n// to 1/0.\n//\n// See: https://www.sqlite.org/lang_altertable.html#altertabaddcol\n//\n// Exported for testing.\nexport function mapPostgresToLiteDefault(\n table: string,\n column: string,\n defaultExpression: string | null | undefined,\n): string | null {\n if (!defaultExpression) {\n return null;\n }\n\n // Numeric literals pass through unchanged\n if (NUMERIC_LITERAL_REGEX.test(defaultExpression)) {\n return defaultExpression;\n }\n\n // Boolean literals convert to SQLite's 1/0\n if (BOOLEAN_LITERAL_REGEX.test(defaultExpression)) {\n return defaultExpression === 'true' ? '1' : '0';\n }\n\n // Quoted strings with type casts: extract just the quoted part\n const match = QUOTED_STRING_WITH_CAST_REGEX.exec(defaultExpression);\n if (match) {\n return match[1];\n }\n\n // Empty arrays: ARRAY[]::type[] or '{}'::type[] → '[]'\n if (\n EMPTY_ARRAY_CONSTRUCTOR_REGEX.test(defaultExpression) ||\n EMPTY_ARRAY_LITERAL_REGEX.test(defaultExpression)\n ) {\n return \"'[]'\";\n }\n\n // Everything else triggers backfill\n throw new UnsupportedColumnDefaultError(\n `Unsupported default value for ${table}.${column}: ${defaultExpression}`,\n );\n}\n\nexport function mapPostgresToLiteColumn(\n table: string,\n column: {name: string; spec: ColumnSpec},\n ignoreDefault?: 'ignore-default',\n): ColumnSpec {\n const {pos, dataType, notNull, dflt, elemPgTypeClass = null} = column.spec;\n\n // PostgreSQL includes [] in dataType for array types (e.g., 'int4[]',\n // 'int4[][]'). liteTypeString() appends attributes:\n // \"varchar[]|NOT_NULL|TEXT_ARRAY\", \"my_enum[][]|TEXT_ENUM|TEXT_ARRAY\"\n const liteType = liteTypeString(\n dataType,\n notNull,\n isEnumColumn(column.spec),\n isArrayColumn(column.spec),\n );\n\n return {\n pos,\n dataType: liteType,\n characterMaximumLength: null,\n // Note: NOT NULL constraints are always ignored for SQLite (replica) tables.\n // 1. They are enforced by the replication stream.\n // 2. We need nullability for columns with defaults to support\n // write permissions on the \"proposed mutation\" state. Proposed\n // mutations are written to SQLite in a `BEGIN CONCURRENT` transaction in mutagen.\n // Permission policies are run against that state (to get their ruling) then the\n // transaction is rolled back.\n notNull: false,\n // Note: DEFAULT constraints are ignored when creating new tables, but are\n // necessary for adding columns to tables with existing rows.\n dflt:\n ignoreDefault === 'ignore-default'\n ? null\n : mapPostgresToLiteDefault(table, column.name, dflt),\n elemPgTypeClass,\n };\n}\n\nexport function mapPostgresToLite(\n t: TableSpec,\n defaultVersion?: string,\n): LiteTableSpec {\n // PRIMARY KEYS are not written to the replica. Instead, we rely\n // UNIQUE indexes, including those created for upstream PRIMARY KEYs.\n const {schema: _, primaryKey: _dropped, ...liteSpec} = t;\n const name = liteTableName(t);\n return {\n ...liteSpec,\n name,\n columns: {\n ...Object.fromEntries(\n Object.entries(t.columns).map(([col, spec]) => [\n col,\n // `ignore-default` for create table statements because\n // there are no rows to set the default for.\n mapPostgresToLiteColumn(name, {name: col, spec}, 'ignore-default'),\n ]),\n ),\n [ZERO_VERSION_COLUMN_NAME]: zeroVersionColumnSpec(defaultVersion),\n },\n };\n}\n\nexport function mapPostgresToLiteIndex(index: IndexSpec): LiteIndexSpec {\n const {schema, tableName, name, ...liteIndex} = index;\n return {\n tableName: liteTableName({schema, name: tableName}),\n name: liteTableName({schema, name}),\n ...liteIndex,\n };\n}\n\nexport class UnsupportedColumnDefaultError extends Error {\n readonly name = 'UnsupportedColumnDefaultError';\n}\n"],"mappings":";;;;;;;;;AAsBA,SAAgB,aACd,MACS;CACT,QAAQ,KAAK,mBAAmB,KAAK,iBAAiB;AACxD;;;;;AAMA,SAAgB,cACd,MACS;CACT,OAAO,KAAK,oBAAoB,QAAQ,KAAK,oBAAoB,KAAA;AACnE;AAEA,SAAS,sBAAsB,gBAAgD;CAC7E,OAAO;EACL,KAAK,OAAO;EACZ,wBAAwB;EACxB,UAAU;EACV,SAAS;EACT,MAAM,CAAC,iBAAiB,OAAO,IAAI,eAAe;EAClD,iBAAiB;CACnB;AACF;AAEA,SAAgB,wBACd,IACA,gBACA,OACA,QACA;CACA,IAAI,uBAAuB,cAAc,MAAM,KAAA,GAC7C,GAAG,OACD,+CAA+C,iBAC7C,cACF,EAAE,qBACQ,MAAM,KAAK,OAAO,4CAC9B;AAEJ;AAGA,IAAM,wBAAwB;AAG9B,IAAM,wBAAwB;AAQ9B,IAAM,gCAAgC;AAItC,IAAM,gCAAgC;AAItC,IAAM,4BAA4B;AAoBlC,SAAgB,yBACd,OACA,QACA,mBACe;CACf,IAAI,CAAC,mBACH,OAAO;CAIT,IAAI,sBAAsB,KAAK,iBAAiB,GAC9C,OAAO;CAIT,IAAI,sBAAsB,KAAK,iBAAiB,GAC9C,OAAO,sBAAsB,SAAS,MAAM;CAI9C,MAAM,QAAQ,8BAA8B,KAAK,iBAAiB;CAClE,IAAI,OACF,OAAO,MAAM;CAIf,IACE,8BAA8B,KAAK,iBAAiB,KACpD,0BAA0B,KAAK,iBAAiB,GAEhD,OAAO;CAIT,MAAM,IAAI,8BACR,iCAAiC,MAAM,GAAG,OAAO,IAAI,mBACvD;AACF;AAEA,SAAgB,wBACd,OACA,QACA,eACY;CACZ,MAAM,EAAC,KAAK,UAAU,SAAS,MAAM,kBAAkB,SAAQ,OAAO;CAYtE,OAAO;EACL;EACA,UATe,eACf,UACA,SACA,aAAa,OAAO,IAAI,GACxB,cAAc,OAAO,IAAI,CAKf;EACV,wBAAwB;EAQxB,SAAS;EAGT,MACE,kBAAkB,mBACd,OACA,yBAAyB,OAAO,OAAO,MAAM,IAAI;EACvD;CACF;AACF;AAEA,SAAgB,kBACd,GACA,gBACe;CAGf,MAAM,EAAC,QAAQ,GAAG,YAAY,UAAU,GAAG,aAAY;CACvD,MAAM,OAAO,cAAc,CAAC;CAC5B,OAAO;EACL,GAAG;EACH;EACA,SAAS;GACP,GAAG,OAAO,YACR,OAAO,QAAQ,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,KAAK,UAAU,CAC7C,KAGA,wBAAwB,MAAM;IAAC,MAAM;IAAK;GAAI,GAAG,gBAAgB,CACnE,CAAC,CACH;IACC,2BAA2B,sBAAsB,cAAc;EAClE;CACF;AACF;AAEA,SAAgB,uBAAuB,OAAiC;CACtE,MAAM,EAAC,QAAQ,WAAW,MAAM,GAAG,cAAa;CAChD,OAAO;EACL,WAAW,cAAc;GAAC;GAAQ,MAAM;EAAS,CAAC;EAClD,MAAM,cAAc;GAAC;GAAQ;EAAI,CAAC;EAClC,GAAG;CACL;AACF;AAEA,IAAa,gCAAb,cAAmD,MAAM;CACvD,OAAgB;AAClB"}
1
+ {"version":3,"file":"pg-to-lite.js","names":[],"sources":["../../../../../zero-cache/src/db/pg-to-lite.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {ZERO_VERSION_COLUMN_NAME} from '../services/replicator/schema/constants.ts';\nimport {\n liteTypeString,\n liteTypeToZqlValueType,\n upstreamDataType,\n type LiteTypeString,\n} from '../types/lite.ts';\nimport {liteTableName} from '../types/names.ts';\nimport {lit} from '../types/sql.ts';\nimport * as PostgresTypeClass from './postgres-type-class-enum.ts';\nimport {\n type ColumnSpec,\n type IndexSpec,\n type LiteIndexSpec,\n type LiteTableSpec,\n type TableSpec,\n} from './specs.ts';\n\n/**\n * Determines if a PostgreSQL column is an enum type.\n * This checks both the element type class (for arrays of enums) and the main type class.\n */\nexport function isEnumColumn(\n spec: Pick<ColumnSpec, 'pgTypeClass' | 'elemPgTypeClass'>,\n): boolean {\n return (spec.elemPgTypeClass ?? spec.pgTypeClass) === PostgresTypeClass.Enum;\n}\n\n/**\n * Determines if a PostgreSQL column is an array type.\n * In PostgreSQL's system, array columns have a non-null elemPgTypeClass.\n */\nexport function isArrayColumn(\n spec: Pick<ColumnSpec, 'elemPgTypeClass'>,\n): boolean {\n return spec.elemPgTypeClass !== null && spec.elemPgTypeClass !== undefined;\n}\n\nfunction zeroVersionColumnSpec(defaultVersion: string | undefined): ColumnSpec {\n return {\n pos: Number.MAX_SAFE_INTEGER, // i.e. last\n characterMaximumLength: null,\n dataType: 'text',\n notNull: false,\n dflt: !defaultVersion ? null : `'${defaultVersion}'`,\n elemPgTypeClass: null,\n };\n}\n\nexport function warnIfDataTypeSupported(\n lc: LogContext,\n liteTypeString: LiteTypeString,\n table: string,\n column: string,\n) {\n if (liteTypeToZqlValueType(liteTypeString) === undefined) {\n lc.warn?.(\n `\\n\\nWARNING: zero does not yet support the \"${upstreamDataType(\n liteTypeString,\n )}\" data type.\\n` +\n `The \"${table}\".\"${column}\" column will not be synced to clients.\\n\\n`,\n );\n }\n}\n\n// Numeric literals: integers and decimals, optionally negative\nconst NUMERIC_LITERAL_REGEX = /^-?\\d+(\\.\\d+)?$/;\n\n// Boolean literals (PG emits lowercase)\nconst BOOLEAN_LITERAL_REGEX = /^(true|false)$/;\n\n// Quoted string with type cast to a simple scalar type: 'value'::typename\n// For strings and certain incarnations of primitives (e.g. integers greater\n// than 2^31-1, Postgres' nodeToString() represents the values as type-casted\n// 'string' values, e.g. `'2147483648'::bigint`, `'foo'::text`.\n// Only matches simple type names (word characters) - array types like\n// `::text[]` won't match and will trigger backfill.\n//\n// The quoted part must be exactly one string literal: a quote inside it can\n// only appear as the `''` escape, so the expression cannot contain anything\n// but the literal and its cast. The default is interpolated into DDL (SQLite\n// has no parameters in `ALTER TABLE ... ADD ... DEFAULT`), so the literal is\n// re-quoted from its decoded value rather than copied from the expression.\nconst QUOTED_STRING_WITH_CAST_REGEX = /^'((?:[^']|'')*)'::\\w+$/;\n\n// Empty array constructor syntax: ARRAY[]::text[], ARRAY[]::integer[], etc.\n// Maps to '[]' (JSON empty array) in SQLite.\nconst EMPTY_ARRAY_CONSTRUCTOR_REGEX = /^ARRAY\\s*\\[\\s*\\]::\\w+\\[\\]$/i;\n\n// Empty array literal syntax: '{}'::text[], '{}'::integer[], etc.\n// Maps to '[]' (JSON empty array) in SQLite.\nconst EMPTY_ARRAY_LITERAL_REGEX = /^'\\{\\}'::\\w+\\[\\]$/;\n\n// Conservative allowlist approach for SQLite ADD COLUMN defaults.\n// We only allow patterns we know are safe. Everything else triggers\n// backfill from PostgreSQL, which correctly handles complex defaults.\n//\n// Note: We don't validate that the default value matches the column type\n// (e.g., that a numeric literal is used with a numeric column). PostgreSQL\n// already enforces this at schema definition time - you can't define\n// `ALTER TABLE foo ADD bar TEXT DEFAULT 123` in PG. So we trust that any\n// default we receive from the replication stream is type-compatible with\n// whatever we map the type to in SQLite.\n//\n// Example: `true`/`false` literals can only appear as defaults for boolean\n// columns in PG, so we don't need to check the column type before converting\n// to 1/0.\n//\n// See: https://www.sqlite.org/lang_altertable.html#altertabaddcol\n//\n// Exported for testing.\nexport function mapPostgresToLiteDefault(\n table: string,\n column: string,\n defaultExpression: string | null | undefined,\n): string | null {\n if (!defaultExpression) {\n return null;\n }\n\n // Numeric literals pass through unchanged\n if (NUMERIC_LITERAL_REGEX.test(defaultExpression)) {\n return defaultExpression;\n }\n\n // Boolean literals convert to SQLite's 1/0\n if (BOOLEAN_LITERAL_REGEX.test(defaultExpression)) {\n return defaultExpression === 'true' ? '1' : '0';\n }\n\n // Quoted strings with type casts: extract just the quoted part\n const match = QUOTED_STRING_WITH_CAST_REGEX.exec(defaultExpression);\n if (match) {\n return lit(match[1].replaceAll(\"''\", \"'\"));\n }\n\n // Empty arrays: ARRAY[]::type[] or '{}'::type[] → '[]'\n if (\n EMPTY_ARRAY_CONSTRUCTOR_REGEX.test(defaultExpression) ||\n EMPTY_ARRAY_LITERAL_REGEX.test(defaultExpression)\n ) {\n return \"'[]'\";\n }\n\n // Everything else triggers backfill\n throw new UnsupportedColumnDefaultError(\n `Unsupported default value for ${table}.${column}: ${defaultExpression}`,\n );\n}\n\nexport function mapPostgresToLiteColumn(\n table: string,\n column: {name: string; spec: ColumnSpec},\n ignoreDefault?: 'ignore-default',\n): ColumnSpec {\n const {pos, dataType, notNull, dflt, elemPgTypeClass = null} = column.spec;\n\n // PostgreSQL includes [] in dataType for array types (e.g., 'int4[]',\n // 'int4[][]'). liteTypeString() appends attributes:\n // \"varchar[]|NOT_NULL|TEXT_ARRAY\", \"my_enum[][]|TEXT_ENUM|TEXT_ARRAY\"\n const liteType = liteTypeString(\n dataType,\n notNull,\n isEnumColumn(column.spec),\n isArrayColumn(column.spec),\n );\n\n return {\n pos,\n dataType: liteType,\n characterMaximumLength: null,\n // Note: NOT NULL constraints are always ignored for SQLite (replica) tables.\n // 1. They are enforced by the replication stream.\n // 2. We need nullability for columns with defaults to support\n // write permissions on the \"proposed mutation\" state. Proposed\n // mutations are written to SQLite in a `BEGIN CONCURRENT` transaction in mutagen.\n // Permission policies are run against that state (to get their ruling) then the\n // transaction is rolled back.\n notNull: false,\n // Note: DEFAULT constraints are ignored when creating new tables, but are\n // necessary for adding columns to tables with existing rows.\n dflt:\n ignoreDefault === 'ignore-default'\n ? null\n : mapPostgresToLiteDefault(table, column.name, dflt),\n elemPgTypeClass,\n };\n}\n\nexport function mapPostgresToLite(\n t: TableSpec,\n defaultVersion?: string,\n): LiteTableSpec {\n // PRIMARY KEYS are not written to the replica. Instead, we rely\n // UNIQUE indexes, including those created for upstream PRIMARY KEYs.\n const {schema: _, primaryKey: _dropped, ...liteSpec} = t;\n const name = liteTableName(t);\n return {\n ...liteSpec,\n name,\n columns: {\n ...Object.fromEntries(\n Object.entries(t.columns).map(([col, spec]) => [\n col,\n // `ignore-default` for create table statements because\n // there are no rows to set the default for.\n mapPostgresToLiteColumn(name, {name: col, spec}, 'ignore-default'),\n ]),\n ),\n [ZERO_VERSION_COLUMN_NAME]: zeroVersionColumnSpec(defaultVersion),\n },\n };\n}\n\nexport function mapPostgresToLiteIndex(index: IndexSpec): LiteIndexSpec {\n const {schema, tableName, name, ...liteIndex} = index;\n return {\n tableName: liteTableName({schema, name: tableName}),\n name: liteTableName({schema, name}),\n ...liteIndex,\n };\n}\n\nexport class UnsupportedColumnDefaultError extends Error {\n readonly name = 'UnsupportedColumnDefaultError';\n}\n"],"mappings":";;;;;;;;;;AAuBA,SAAgB,aACd,MACS;CACT,QAAQ,KAAK,mBAAmB,KAAK,iBAAiB;AACxD;;;;;AAMA,SAAgB,cACd,MACS;CACT,OAAO,KAAK,oBAAoB,QAAQ,KAAK,oBAAoB,KAAA;AACnE;AAEA,SAAS,sBAAsB,gBAAgD;CAC7E,OAAO;EACL,KAAK,OAAO;EACZ,wBAAwB;EACxB,UAAU;EACV,SAAS;EACT,MAAM,CAAC,iBAAiB,OAAO,IAAI,eAAe;EAClD,iBAAiB;CACnB;AACF;AAEA,SAAgB,wBACd,IACA,gBACA,OACA,QACA;CACA,IAAI,uBAAuB,cAAc,MAAM,KAAA,GAC7C,GAAG,OACD,+CAA+C,iBAC7C,cACF,EAAE,qBACQ,MAAM,KAAK,OAAO,4CAC9B;AAEJ;AAGA,IAAM,wBAAwB;AAG9B,IAAM,wBAAwB;AAc9B,IAAM,gCAAgC;AAItC,IAAM,gCAAgC;AAItC,IAAM,4BAA4B;AAoBlC,SAAgB,yBACd,OACA,QACA,mBACe;CACf,IAAI,CAAC,mBACH,OAAO;CAIT,IAAI,sBAAsB,KAAK,iBAAiB,GAC9C,OAAO;CAIT,IAAI,sBAAsB,KAAK,iBAAiB,GAC9C,OAAO,sBAAsB,SAAS,MAAM;CAI9C,MAAM,QAAQ,8BAA8B,KAAK,iBAAiB;CAClE,IAAI,OACF,OAAO,IAAI,MAAM,EAAE,CAAC,WAAW,MAAM,GAAG,CAAC;CAI3C,IACE,8BAA8B,KAAK,iBAAiB,KACpD,0BAA0B,KAAK,iBAAiB,GAEhD,OAAO;CAIT,MAAM,IAAI,8BACR,iCAAiC,MAAM,GAAG,OAAO,IAAI,mBACvD;AACF;AAEA,SAAgB,wBACd,OACA,QACA,eACY;CACZ,MAAM,EAAC,KAAK,UAAU,SAAS,MAAM,kBAAkB,SAAQ,OAAO;CAYtE,OAAO;EACL;EACA,UATe,eACf,UACA,SACA,aAAa,OAAO,IAAI,GACxB,cAAc,OAAO,IAAI,CAKf;EACV,wBAAwB;EAQxB,SAAS;EAGT,MACE,kBAAkB,mBACd,OACA,yBAAyB,OAAO,OAAO,MAAM,IAAI;EACvD;CACF;AACF;AAEA,SAAgB,kBACd,GACA,gBACe;CAGf,MAAM,EAAC,QAAQ,GAAG,YAAY,UAAU,GAAG,aAAY;CACvD,MAAM,OAAO,cAAc,CAAC;CAC5B,OAAO;EACL,GAAG;EACH;EACA,SAAS;GACP,GAAG,OAAO,YACR,OAAO,QAAQ,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,KAAK,UAAU,CAC7C,KAGA,wBAAwB,MAAM;IAAC,MAAM;IAAK;GAAI,GAAG,gBAAgB,CACnE,CAAC,CACH;IACC,2BAA2B,sBAAsB,cAAc;EAClE;CACF;AACF;AAEA,SAAgB,uBAAuB,OAAiC;CACtE,MAAM,EAAC,QAAQ,WAAW,MAAM,GAAG,cAAa;CAChD,OAAO;EACL,WAAW,cAAc;GAAC;GAAQ,MAAM;EAAS,CAAC;EAClD,MAAM,cAAc;GAAC;GAAQ;EAAI,CAAC;EAClC,GAAG;CACL;AACF;AAEA,IAAa,gCAAb,cAAmD,MAAM;CACvD,OAAgB;AAClB"}
@@ -11,5 +11,18 @@ export type ParseOptions = {
11
11
  */
12
12
  returnJsonAsString?: boolean;
13
13
  };
14
+ /**
15
+ * Parses the text format of a Postgres array, e.g. `{1,NULL,"a b",{2,3}}`.
16
+ *
17
+ * This is used instead of the array parsers of postgres.js, which pass an
18
+ * unquoted `NULL` to the element parser as if it were a value. Only a null
19
+ * element is unquoted: Postgres quotes a string that spells `NULL`.
20
+ *
21
+ * postgres.js 4.0.0 fixes this with an equivalent parser, so this can be
22
+ * removed when we upgrade to it. The 3.4.x releases don't have the fix.
23
+ *
24
+ * https://www.postgresql.org/docs/current/arrays.html#ARRAYS-IO
25
+ */
26
+ export declare function parseArray(text: string, parseElement?: TypeParser, delimiter?: string): unknown[];
14
27
  export declare function getTypeParsers(db: PostgresDB, { returnJsonAsString }?: ParseOptions): Promise<TypeParsers>;
15
28
  //# sourceMappingURL=pg-type-parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pg-type-parser.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/db/pg-type-parser.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,gBAAgB,CAAC;AAK/C,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;AAClD,MAAM,WAAW,WAAW;IAC1B,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;CAC5C;AAED,MAAM,MAAM,YAAY,GAAG;IACzB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAeF,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,EAAC,kBAAkB,EAAC,GAAE,YAAiB,GACtC,OAAO,CAAC,WAAW,CAAC,CAsCtB"}
1
+ {"version":3,"file":"pg-type-parser.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/db/pg-type-parser.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,gBAAgB,CAAC;AAK/C,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;AAClD,MAAM,WAAW,WAAW;IAC1B,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;CAC5C;AAED,MAAM,MAAM,YAAY,GAAG;IACzB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAQF;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,YAAY,GAAE,UAAmB,EACjC,SAAS,SAAM,GACd,OAAO,EAAE,CAgDX;AAYD,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,EAAC,kBAAkB,EAAC,GAAE,YAAiB,GACtC,OAAO,CAAC,WAAW,CAAC,CA+CtB"}
@@ -5,19 +5,72 @@ import { JSONB } from "../types/pg-types.js";
5
5
  var INT4_ARRAY_TYPE = 1007;
6
6
  var JSON_TYPE = String(114);
7
7
  var JSONB_TYPE = String(JSONB);
8
+ var BOX_ARRAY_TYPE = 1020;
9
+ /**
10
+ * Parses the text format of a Postgres array, e.g. `{1,NULL,"a b",{2,3}}`.
11
+ *
12
+ * This is used instead of the array parsers of postgres.js, which pass an
13
+ * unquoted `NULL` to the element parser as if it were a value. Only a null
14
+ * element is unquoted: Postgres quotes a string that spells `NULL`.
15
+ *
16
+ * postgres.js 4.0.0 fixes this with an equivalent parser, so this can be
17
+ * removed when we upgrade to it. The 3.4.x releases don't have the fix.
18
+ *
19
+ * https://www.postgresql.org/docs/current/arrays.html#ARRAYS-IO
20
+ */
21
+ function parseArray(text, parseElement = (x) => x, delimiter = ",") {
22
+ let pos = text.indexOf("{");
23
+ assert(pos >= 0, "array has no opening brace");
24
+ function parseQuoted() {
25
+ let value = "";
26
+ for (pos++; text[pos] !== "\""; pos++) {
27
+ assert(pos < text.length, "unterminated quoted array element");
28
+ if (text[pos] === "\\") pos++;
29
+ value += text[pos];
30
+ }
31
+ pos++;
32
+ return value;
33
+ }
34
+ function parseElements() {
35
+ const elements = [];
36
+ pos++;
37
+ while (text[pos] !== "}") {
38
+ assert(pos < text.length, "unterminated array");
39
+ if (text[pos] === "{") elements.push(parseElements());
40
+ else if (text[pos] === "\"") elements.push(parseElement(parseQuoted()));
41
+ else {
42
+ const start = pos;
43
+ while (pos < text.length && text[pos] !== delimiter && text[pos] !== "}") pos++;
44
+ const element = text.slice(start, pos);
45
+ elements.push(element === "NULL" ? null : parseElement(element));
46
+ }
47
+ if (text[pos] === delimiter) pos++;
48
+ }
49
+ pos++;
50
+ return elements;
51
+ }
52
+ return parseElements();
53
+ }
8
54
  async function getTypeParsers(db, { returnJsonAsString } = {}) {
9
55
  if (!db.options.parsers[INT4_ARRAY_TYPE]) {
10
56
  assert(db.options.fetch_types, `Supplied db must fetch_types`);
11
57
  await db`SELECT 1`.simple();
12
58
  assert(db.options.parsers[INT4_ARRAY_TYPE], `array types not fetched ${Object.keys(db.options.parsers)}`);
13
59
  }
60
+ const { typeArrayMap } = db.options.shared;
61
+ const elementTypes = new Map(Object.entries(typeArrayMap).map(([element, array]) => [String(array), Number(element)]));
14
62
  const parsers = mapEntries(db.options.parsers, (type, parse) => {
15
63
  if ((type === JSON_TYPE || type === JSONB_TYPE) && returnJsonAsString) return [type, (x) => x];
16
- return [type, parse.array ? (val) => parse(val.substring(1)) : parse];
64
+ if (!parse.array) return [type, parse];
65
+ const element = elementTypes.get(type);
66
+ assert(element !== void 0, `no element type for array type ${type}`);
67
+ const parseElement = db.options.parsers[element];
68
+ const delimiter = Number(type) === BOX_ARRAY_TYPE ? ";" : ",";
69
+ return [type, (val) => parseArray(val, parseElement, delimiter)];
17
70
  });
18
71
  return { getTypeParser: (typeOID) => parsers[typeOID] ?? String };
19
72
  }
20
73
  //#endregion
21
- export { getTypeParsers };
74
+ export { getTypeParsers, parseArray };
22
75
 
23
76
  //# sourceMappingURL=pg-type-parser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pg-type-parser.js","names":[],"sources":["../../../../../zero-cache/src/db/pg-type-parser.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport {mapEntries} from '../../../shared/src/objects.ts';\nimport {JSON, JSONB} from '../types/pg-types.ts';\nimport type {PostgresDB} from '../types/pg.ts';\n\n// Arbitrary array type to test if the PostgresDB client has fetched types.\nconst INT4_ARRAY_TYPE = 1007;\n\nexport type TypeParser = (val: string) => unknown;\nexport interface TypeParsers {\n getTypeParser(typeOID: number): TypeParser;\n}\n\nexport type ParseOptions = {\n /**\n * Returns JSON and JSONB values as raw JSON strings (i.e. unparsed).\n * Note that JSON[] and JSONB[] values are returned as arrays of (parsed)\n * JSON values.\n */\n returnJsonAsString?: boolean;\n};\n\nconst JSON_TYPE = String(JSON);\nconst JSONB_TYPE = String(JSONB);\n\n// postgres.js has default type parsers with user-defined overrides\n// configurable per-client (see `postgresTypeConfig` in types/pg.ts).\n//\n// From these, the postgres.js client will automatically derive parsers\n// for array versions of these types, provided that the client was\n// configured with `fetch_types: true` (which is the default).\n//\n// A replication session (with `database: replication`), however, does\n// not support this type fetching, so it is done on a connection from\n// a default client.\nexport async function getTypeParsers(\n db: PostgresDB,\n {returnJsonAsString}: ParseOptions = {},\n): Promise<TypeParsers> {\n if (!db.options.parsers[INT4_ARRAY_TYPE]) {\n assert(db.options.fetch_types, `Supplied db must fetch_types`);\n\n // Execute a query to ensure that fetchArrayTypes() gets executed:\n // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/connection.js#L536\n await db`SELECT 1`.simple();\n assert(\n db.options.parsers[INT4_ARRAY_TYPE],\n `array types not fetched ${Object.keys(db.options.parsers)}`,\n );\n }\n const parsers = mapEntries(db.options.parsers, (type, parse) => {\n if ((type === JSON_TYPE || type === JSONB_TYPE) && returnJsonAsString) {\n return [type, (x: string) => x];\n }\n // The postgres.js library tags parsers for array types with an `array: true` field.\n // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/connection.js#L760\n const isArrayType = (parse as unknown as {array?: boolean}).array;\n\n // And then skips the first character when parsing the string,\n // e.g. an array parser will parse '{1,2,3}' from '1,2,3}'.\n // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/connection.js#L496\n return [\n type,\n isArrayType ? (val: string) => parse(val.substring(1)) : parse,\n ];\n });\n return {\n // A type OID for which a parser is not explicitly defined\n // is parsed as a String.\n // https://github.com/porsager/postgres/blob/b0d8c8f363e006a74472d76f859da60c52a80368/src/connection.js#L494\n //\n // This is also consistent with the `pg` library, in which the absence of a\n // TypeParser defaults to \"noParse\":\n // https://github.com/brianc/node-pg-types/blob/5b26b826466cff4a9092b8c9e31960fe293ef3d9/index.js#L15\n getTypeParser: typeOID => parsers[typeOID] ?? String,\n };\n}\n"],"mappings":";;;;AAMA,IAAM,kBAAkB;AAgBxB,IAAM,YAAY,OAAA,GAAW;AAC7B,IAAM,aAAa,OAAO,KAAK;AAY/B,eAAsB,eACpB,IACA,EAAC,uBAAoC,CAAC,GAChB;CACtB,IAAI,CAAC,GAAG,QAAQ,QAAQ,kBAAkB;EACxC,OAAO,GAAG,QAAQ,aAAa,8BAA8B;EAI7D,MAAM,EAAE,WAAW,OAAO;EAC1B,OACE,GAAG,QAAQ,QAAQ,kBACnB,2BAA2B,OAAO,KAAK,GAAG,QAAQ,OAAO,GAC3D;CACF;CACA,MAAM,UAAU,WAAW,GAAG,QAAQ,UAAU,MAAM,UAAU;EAC9D,KAAK,SAAS,aAAa,SAAS,eAAe,oBACjD,OAAO,CAAC,OAAO,MAAc,CAAC;EAShC,OAAO,CACL,MANmB,MAAuC,SAO3C,QAAgB,MAAM,IAAI,UAAU,CAAC,CAAC,IAAI,KAC3D;CACF,CAAC;CACD,OAAO,EAQL,gBAAe,YAAW,QAAQ,YAAY,OAChD;AACF"}
1
+ {"version":3,"file":"pg-type-parser.js","names":[],"sources":["../../../../../zero-cache/src/db/pg-type-parser.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport {mapEntries} from '../../../shared/src/objects.ts';\nimport {JSON, JSONB} from '../types/pg-types.ts';\nimport type {PostgresDB} from '../types/pg.ts';\n\n// Arbitrary array type to test if the PostgresDB client has fetched types.\nconst INT4_ARRAY_TYPE = 1007;\n\nexport type TypeParser = (val: string) => unknown;\nexport interface TypeParsers {\n getTypeParser(typeOID: number): TypeParser;\n}\n\nexport type ParseOptions = {\n /**\n * Returns JSON and JSONB values as raw JSON strings (i.e. unparsed).\n * Note that JSON[] and JSONB[] values are returned as arrays of (parsed)\n * JSON values.\n */\n returnJsonAsString?: boolean;\n};\n\nconst JSON_TYPE = String(JSON);\nconst JSONB_TYPE = String(JSONB);\n\n// The only array type whose elements are not delimited by a comma.\nconst BOX_ARRAY_TYPE = 1020;\n\n/**\n * Parses the text format of a Postgres array, e.g. `{1,NULL,\"a b\",{2,3}}`.\n *\n * This is used instead of the array parsers of postgres.js, which pass an\n * unquoted `NULL` to the element parser as if it were a value. Only a null\n * element is unquoted: Postgres quotes a string that spells `NULL`.\n *\n * postgres.js 4.0.0 fixes this with an equivalent parser, so this can be\n * removed when we upgrade to it. The 3.4.x releases don't have the fix.\n *\n * https://www.postgresql.org/docs/current/arrays.html#ARRAYS-IO\n */\nexport function parseArray(\n text: string,\n parseElement: TypeParser = x => x,\n delimiter = ',',\n): unknown[] {\n // An array with non-default bounds starts with them, e.g. `[2:3]={1,2}`.\n let pos = text.indexOf('{');\n assert(pos >= 0, 'array has no opening brace');\n\n function parseQuoted(): string {\n let value = '';\n for (pos++; text[pos] !== '\"'; pos++) {\n assert(pos < text.length, 'unterminated quoted array element');\n if (text[pos] === '\\\\') {\n pos++;\n }\n value += text[pos];\n }\n pos++;\n return value;\n }\n\n function parseElements(): unknown[] {\n const elements: unknown[] = [];\n pos++;\n while (text[pos] !== '}') {\n assert(pos < text.length, 'unterminated array');\n if (text[pos] === '{') {\n elements.push(parseElements());\n } else if (text[pos] === '\"') {\n elements.push(parseElement(parseQuoted()));\n } else {\n const start = pos;\n while (\n pos < text.length &&\n text[pos] !== delimiter &&\n text[pos] !== '}'\n ) {\n pos++;\n }\n const element = text.slice(start, pos);\n elements.push(element === 'NULL' ? null : parseElement(element));\n }\n if (text[pos] === delimiter) {\n pos++;\n }\n }\n pos++;\n return elements;\n }\n\n return parseElements();\n}\n\n// postgres.js has default type parsers with user-defined overrides\n// configurable per-client (see `postgresTypeConfig` in types/pg.ts).\n//\n// From these, the postgres.js client will automatically derive parsers\n// for array versions of these types, provided that the client was\n// configured with `fetch_types: true` (which is the default).\n//\n// A replication session (with `database: replication`), however, does\n// not support this type fetching, so it is done on a connection from\n// a default client.\nexport async function getTypeParsers(\n db: PostgresDB,\n {returnJsonAsString}: ParseOptions = {},\n): Promise<TypeParsers> {\n if (!db.options.parsers[INT4_ARRAY_TYPE]) {\n assert(db.options.fetch_types, `Supplied db must fetch_types`);\n\n // Execute a query to ensure that fetchArrayTypes() gets executed:\n // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/connection.js#L536\n await db`SELECT 1`.simple();\n assert(\n db.options.parsers[INT4_ARRAY_TYPE],\n `array types not fetched ${Object.keys(db.options.parsers)}`,\n );\n }\n // postgres.js records the array type of each element type that it fetched.\n const {typeArrayMap} = (\n db.options as unknown as {shared: {typeArrayMap: Record<string, number>}}\n ).shared;\n const elementTypes = new Map(\n Object.entries(typeArrayMap).map(([element, array]) => [\n String(array),\n Number(element),\n ]),\n );\n const parsers = mapEntries(db.options.parsers, (type, parse) => {\n if ((type === JSON_TYPE || type === JSONB_TYPE) && returnJsonAsString) {\n return [type, (x: string) => x];\n }\n // The postgres.js library tags parsers for array types with an `array: true` field.\n // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/connection.js#L760\n if (!(parse as unknown as {array?: boolean}).array) {\n return [type, parse];\n }\n const element = elementTypes.get(type);\n assert(element !== undefined, `no element type for array type ${type}`);\n const parseElement = db.options.parsers[element];\n const delimiter = Number(type) === BOX_ARRAY_TYPE ? ';' : ',';\n return [type, (val: string) => parseArray(val, parseElement, delimiter)];\n });\n return {\n // A type OID for which a parser is not explicitly defined\n // is parsed as a String.\n // https://github.com/porsager/postgres/blob/b0d8c8f363e006a74472d76f859da60c52a80368/src/connection.js#L494\n //\n // This is also consistent with the `pg` library, in which the absence of a\n // TypeParser defaults to \"noParse\":\n // https://github.com/brianc/node-pg-types/blob/5b26b826466cff4a9092b8c9e31960fe293ef3d9/index.js#L15\n getTypeParser: typeOID => parsers[typeOID] ?? String,\n };\n}\n"],"mappings":";;;;AAMA,IAAM,kBAAkB;AAgBxB,IAAM,YAAY,OAAA,GAAW;AAC7B,IAAM,aAAa,OAAO,KAAK;AAG/B,IAAM,iBAAiB;;;;;;;;;;;;;AAcvB,SAAgB,WACd,MACA,gBAA2B,MAAK,GAChC,YAAY,KACD;CAEX,IAAI,MAAM,KAAK,QAAQ,GAAG;CAC1B,OAAO,OAAO,GAAG,4BAA4B;CAE7C,SAAS,cAAsB;EAC7B,IAAI,QAAQ;EACZ,KAAK,OAAO,KAAK,SAAS,MAAK,OAAO;GACpC,OAAO,MAAM,KAAK,QAAQ,mCAAmC;GAC7D,IAAI,KAAK,SAAS,MAChB;GAEF,SAAS,KAAK;EAChB;EACA;EACA,OAAO;CACT;CAEA,SAAS,gBAA2B;EAClC,MAAM,WAAsB,CAAC;EAC7B;EACA,OAAO,KAAK,SAAS,KAAK;GACxB,OAAO,MAAM,KAAK,QAAQ,oBAAoB;GAC9C,IAAI,KAAK,SAAS,KAChB,SAAS,KAAK,cAAc,CAAC;QACxB,IAAI,KAAK,SAAS,MACvB,SAAS,KAAK,aAAa,YAAY,CAAC,CAAC;QACpC;IACL,MAAM,QAAQ;IACd,OACE,MAAM,KAAK,UACX,KAAK,SAAS,aACd,KAAK,SAAS,KAEd;IAEF,MAAM,UAAU,KAAK,MAAM,OAAO,GAAG;IACrC,SAAS,KAAK,YAAY,SAAS,OAAO,aAAa,OAAO,CAAC;GACjE;GACA,IAAI,KAAK,SAAS,WAChB;EAEJ;EACA;EACA,OAAO;CACT;CAEA,OAAO,cAAc;AACvB;AAYA,eAAsB,eACpB,IACA,EAAC,uBAAoC,CAAC,GAChB;CACtB,IAAI,CAAC,GAAG,QAAQ,QAAQ,kBAAkB;EACxC,OAAO,GAAG,QAAQ,aAAa,8BAA8B;EAI7D,MAAM,EAAE,WAAW,OAAO;EAC1B,OACE,GAAG,QAAQ,QAAQ,kBACnB,2BAA2B,OAAO,KAAK,GAAG,QAAQ,OAAO,GAC3D;CACF;CAEA,MAAM,EAAC,iBACL,GAAG,QACH;CACF,MAAM,eAAe,IAAI,IACvB,OAAO,QAAQ,YAAY,CAAC,CAAC,KAAK,CAAC,SAAS,WAAW,CACrD,OAAO,KAAK,GACZ,OAAO,OAAO,CAChB,CAAC,CACH;CACA,MAAM,UAAU,WAAW,GAAG,QAAQ,UAAU,MAAM,UAAU;EAC9D,KAAK,SAAS,aAAa,SAAS,eAAe,oBACjD,OAAO,CAAC,OAAO,MAAc,CAAC;EAIhC,IAAI,CAAE,MAAuC,OAC3C,OAAO,CAAC,MAAM,KAAK;EAErB,MAAM,UAAU,aAAa,IAAI,IAAI;EACrC,OAAO,YAAY,KAAA,GAAW,kCAAkC,MAAM;EACtE,MAAM,eAAe,GAAG,QAAQ,QAAQ;EACxC,MAAM,YAAY,OAAO,IAAI,MAAM,iBAAiB,MAAM;EAC1D,OAAO,CAAC,OAAO,QAAgB,WAAW,KAAK,cAAc,SAAS,CAAC;CACzE,CAAC;CACD,OAAO,EAQL,gBAAe,YAAW,QAAQ,YAAY,OAChD;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"change-streamer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"names":[],"mappings":"AA6CA,OAAO,EAML,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAgB/B,wBAA8B,SAAS,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAuaf"}
1
+ {"version":3,"file":"change-streamer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"names":[],"mappings":"AA6CA,OAAO,EAML,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAgB/B,wBAA8B,SAAS,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAkbf"}
@@ -43,7 +43,7 @@ async function runWorker(parent, env, ...argv) {
43
43
  env,
44
44
  argv
45
45
  });
46
- const { taskID, changeStreamer: { port, address, protocol, startupDelayMs, backPressureLimitHeapProportion, flowControlConsensusTimeoutProportion, flowControlSlowSubscriberGracePeriodSeconds, maxConcurrentSwitchoverRestores, pgChangeLogEnabled, sqliteChangeLogMode, sqliteChangeLogReadPercent, sqliteChangeLogColdReadPercent, sqliteChangeLogComparePercent, sqliteChangeLogRetentionMs, sqliteChangeLogReadBatchRows, sqliteChangeLogPurgeBatchRows, sqliteChangeLogBarrierTimeoutMs }, autoReset, replicationLag, litestream, upstream, change, replica, initialSync, keepaliveTimeoutMs, sqliteCorruptionChecks } = config;
46
+ const { taskID, changeStreamer: { port, address, protocol, secret, startupDelayMs, backPressureLimitHeapProportion, flowControlConsensusTimeoutProportion, flowControlSlowSubscriberGracePeriodSeconds, maxConcurrentSwitchoverRestores, pgChangeLogEnabled, sqliteChangeLogMode, sqliteChangeLogReadPercent, sqliteChangeLogColdReadPercent, sqliteChangeLogComparePercent, sqliteChangeLogRetentionMs, sqliteChangeLogReadBatchRows, sqliteChangeLogPurgeBatchRows, sqliteChangeLogBarrierTimeoutMs }, autoReset, replicationLag, litestream, upstream, change, replica, initialSync, keepaliveTimeoutMs, sqliteCorruptionChecks } = config;
47
47
  startOtelAuto(createLogContext(config, "change-streamer", 0, false), "change-streamer", 0);
48
48
  lc = createLogContext(config, "change-streamer");
49
49
  registerSQLiteCorruptionDiagnosticTarget({
@@ -199,13 +199,16 @@ async function runWorker(parent, env, ...argv) {
199
199
  }
200
200
  const readinessGate = pgChangeLogEnabled ? backupReady : Promise.all([backupReady, replicatorReady]);
201
201
  const profileWorker = profileSubWorkers.length > 0 ? broadcastWorker(profileSubWorkers) : void 0;
202
+ const getProfileWorker = profileWorker ? () => Promise.resolve(profileWorker) : void 0;
203
+ if (secret === void 0) lc.warn?.(`No --change-streamer-secret is configured: the change-streamer accepts replication connections from anything that can reach port ${port}. Set ZERO_CHANGE_STREAMER_SECRET to the same value on the replication-manager and on every view-syncer.`);
202
204
  const changeStreamerWebServer = new ChangeStreamerHttpServer(lc, {
203
205
  port,
206
+ secret,
204
207
  keepaliveTimeoutMs,
205
208
  startupDelayMs: pgChangeLogEnabled ? startupDelayMs : 0,
206
209
  readinessGate,
207
210
  config,
208
- getProfileWorker: profileWorker ? () => Promise.resolve(profileWorker) : void 0
211
+ getProfileWorker
209
212
  }, parent, changeStreamer);
210
213
  readinessGate.then(() => parent.send(["ready", { ready: true }]));
211
214
  running = true;
@@ -1 +1 @@
1
- {"version":3,"file":"change-streamer.js","names":[],"sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"sourcesContent":["import {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.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 {registerSQLiteCorruptionDiagnosticTarget} from '../db/sqlite-corruption.ts';\nimport {warmupConnections} from '../db/warmup.ts';\nimport {initEventSink, publishCriticalEvent} from '../observability/events.ts';\nimport {getOrCreateGauge} from '../observability/metrics.ts';\nimport {InitCleanup} from '../services/change-source/common/init-cleanup.ts';\nimport type {PgChangeLogPurgeLocker} from '../services/change-source/common/replica-restore.ts';\nimport {initializeCustomChangeSource} from '../services/change-source/custom/change-source.ts';\nimport {initializePostgresChangeSource} from '../services/change-source/pg/change-source-init.ts';\nimport {createBackupCleanupMonitor} from '../services/change-streamer/backup-cleanup-monitor-factory.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 {DEFAULT_READINESS_GRACE_PERIOD_MS} from '../services/change-streamer/restore-admission.ts';\nimport {initChangeStreamerSchema} from '../services/change-streamer/schema/init.ts';\nimport {AutoResetSignal} from '../services/change-streamer/schema/tables.ts';\nimport {\n PurgeLocker,\n type PurgeLock,\n} from '../services/change-streamer/storer.ts';\nimport {\n exitAfter,\n runUntilKilled,\n SupportingProcesses,\n} from '../services/life-cycle.ts';\nimport {deleteLitestreamMetaDir} from '../services/litestream/commands.ts';\nimport {\n changeLogFileName,\n deleteChangeLogDB,\n} from '../services/replicator/change-log-db.ts';\nimport {\n replicationStatusError,\n ReplicationStatusPublisher,\n} from '../services/replicator/replication-status.ts';\nimport {sqliteFileBytes} from '../services/replicator/sqlite-change-log-observability.ts';\nimport {warnIfPostgresUnpatched} from '../types/pg-patch.ts';\nimport {connectPgClient} from '../types/pg.ts';\nimport {\n broadcastWorker,\n childWorker,\n parentWorker,\n singleProcessMode,\n type ProfileResponseMessage,\n type Worker,\n} from '../types/processes.ts';\nimport {installProfileHandler} from '../types/profiler.ts';\nimport {getShardConfig} from '../types/shards.ts';\nimport type {ReplicaFileMode} from '../workers/replicator.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\nimport {REPLICATOR_URL} from './worker-urls.ts';\n\n// Default LogContext, overridden in runWorker\nlet lc = new LogContext('info', {}, consoleLogSink);\n\n// Errors thrown once the services are running are reported by the services\n// themselves (e.g. as 'Replicating' status events), and are thus not\n// published as 'Initializing' errors.\nlet running = false;\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...argv: string[]\n): Promise<void> {\n installProfileHandler(parent, 'change-streamer');\n const config = getNormalizedZeroConfig({env, argv});\n const {\n taskID,\n changeStreamer: {\n port,\n address,\n protocol,\n startupDelayMs,\n backPressureLimitHeapProportion,\n flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodSeconds,\n maxConcurrentSwitchoverRestores,\n pgChangeLogEnabled,\n sqliteChangeLogMode,\n sqliteChangeLogReadPercent,\n sqliteChangeLogColdReadPercent,\n sqliteChangeLogComparePercent,\n sqliteChangeLogRetentionMs,\n sqliteChangeLogReadBatchRows,\n sqliteChangeLogPurgeBatchRows,\n sqliteChangeLogBarrierTimeoutMs,\n },\n autoReset,\n replicationLag,\n litestream,\n upstream,\n change,\n replica,\n initialSync,\n keepaliveTimeoutMs,\n sqliteCorruptionChecks,\n } = config;\n\n startOtelAuto(\n createLogContext(config, 'change-streamer', 0, false),\n 'change-streamer',\n 0,\n );\n lc = createLogContext(config, 'change-streamer');\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: 'change-streamer replica',\n dbPath: replica.file,\n },\n sqliteCorruptionChecks,\n );\n initEventSink(lc, config);\n if (upstream.type === 'pg') {\n warnIfPostgresUnpatched(lc);\n }\n\n // Startup-time client used for for change-streamer initialization\n // and handoff / takeover. Steady-state clients are managed by the\n // change-streamer (Storer) implementation.\n const changeDB = await connectPgClient(\n lc,\n change.db,\n 'change-streamer-init',\n {max: 5},\n {sendStringAsJson: true},\n );\n void warmupConnections(lc, changeDB, 'change').catch(() => {});\n\n const shard = getShardConfig(config);\n\n // Ensure the change DB schema is initialized/up-to-date.\n await initChangeStreamerSchema(lc, changeDB, shard);\n\n let purgeLock = null as PurgeLock | null;\n let changeStreamer: ChangeStreamerService | undefined;\n let backupURL: string | undefined;\n\n const context = getServerContext(config);\n const sqliteChangeLogEnabled = sqliteChangeLogMode !== 'off';\n // The modes are cumulative, so `serve` implies `compare`.\n const sqliteChangeLogComparing =\n sqliteChangeLogMode === 'compare' || sqliteChangeLogMode === 'serve';\n if (sqliteChangeLogEnabled) {\n const changeLogFile = changeLogFileName(replica.file);\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: 'change-streamer change-log',\n dbPath: changeLogFile,\n },\n sqliteCorruptionChecks,\n );\n // The log's bytes are accounted for in the process that writes them. This\n // is local disk only — the log is excluded from the litestream backup — and\n // it is a whole-database total, because a table that left the replica left\n // through its wal: the replica's footprint is what shrank and this is where\n // it went.\n getOrCreateGauge('replica', 'sqlite_change_log.file_bytes', {\n description:\n `The SQLite change log's total on-disk footprint: its main db file ` +\n `plus its wal sidecars.`,\n unit: 'By',\n }).addCallback(async o =>\n o.observe(await sqliteFileBytes(lc, changeLogFile)),\n );\n }\n\n let waitForFirstBackupBeforeServing = false;\n let newBackupLineage = false;\n for (const first of [true, false]) {\n // Resources acquired by an initialization attempt (e.g. a claimed\n // replication slot, the purge lock) to be released if the attempt fails.\n const cleanup = new InitCleanup(lc);\n // When restoring from litestream, a lock is acquired to prevent change-log\n // purges. This ensures that (this) change-streamer will be able to resume\n // from the backup. The lock is acquired by the change source initialization\n // just before restoring (i.e. after a replication slot is created, if\n // applicable), and released once this change-streamer takes over the\n // change-log.\n const acquirePurgeLock: PgChangeLogPurgeLocker | undefined =\n pgChangeLogEnabled && litestream.backupURL && litestream.executable\n ? async (slotWatermark?: string) => {\n const lock = await new PurgeLocker(lc, shard, changeDB).acquire(\n slotWatermark,\n );\n purgeLock = lock === 'behind-slot' ? null : lock;\n if (purgeLock) {\n const acquired = purgeLock;\n cleanup.onFailure('purge lock', () => {\n purgeLock = null;\n return acquired.release();\n });\n }\n return lock;\n }\n : undefined;\n\n const restoreOptions = {litestream, acquirePurgeLock, cleanup};\n try {\n // Note: This performs initial sync of the replica if necessary.\n const {\n pgReplicationEpoch: epoch,\n pgReplicationSlotPerReplica: slotPerReplica,\n pgResumeOrphanedSlotGracePeriodMs: inactiveReplicaGracePeriodMs,\n } = upstream;\n const {\n changeSource,\n subscriptionState,\n destinationBackupURL,\n replicaID,\n waitForBackupBeforeServing,\n newBackupLineage: initNewBackupLineage,\n pgChangeLogBehindSlot,\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 installPartialIndexTriggers: upstream.pgPartialIndexTriggers,\n },\n context,\n replicationLag.reportIntervalMs,\n restoreOptions,\n {\n epoch,\n slotPerReplica,\n inactiveReplicaGracePeriodMs,\n backupV5: litestream.backupUsingV5,\n pgChangeLog: pgChangeLogEnabled,\n },\n upstream.pgStreamInboundTimeoutMs,\n )\n : await initializeCustomChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n context,\n restoreOptions,\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 destinationBackupURL\n ? {\n backupURL: destinationBackupURL,\n litestreamVersion: litestream.backupUsingV5 ? 'v5' : 'legacy',\n replicaFile: replica.file,\n }\n : null,\n purgeLock,\n autoReset ?? false,\n {\n pgChangeLogEnabled,\n pgChangeLogBehindSlot: pgChangeLogBehindSlot ?? false,\n backPressureLimitHeapProportion,\n flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodMs:\n flowControlSlowSubscriberGracePeriodSeconds > 0\n ? flowControlSlowSubscriberGracePeriodSeconds * 1000\n : undefined,\n maxConcurrentSwitchoverRestores,\n // When lag reporting is enabled, a restored view-syncer waits for\n // the next lag report (sent at an interval with ~1 second of\n // jitter) before becoming ready.\n switchoverRestoreReadinessGracePeriodMs:\n (replicationLag.reportIntervalMs > 0\n ? replicationLag.reportIntervalMs + 1000\n : 0) + DEFAULT_READINESS_GRACE_PERIOD_MS,\n statementTimeoutMs: change.statementTimeoutMs,\n changeLogBatchSize: change.logBatchSize,\n sqliteCatchup: {\n changeLogFile: changeLogFileName(replica.file),\n readBatchRows: sqliteChangeLogReadBatchRows,\n barrierTimeoutMs: sqliteChangeLogBarrierTimeoutMs,\n },\n // The presence of these options is the writer's gate. This process\n // performs the restore and the initial sync, so the replica's\n // identity is in hand at the moment the log is opened.\n sqliteChangeLogWriter: sqliteChangeLogEnabled\n ? {\n replicaFile: replica.file,\n identity: {\n // RMv2 supplies the epoch; until then the generation and the\n // replica ID are the whole of the identity.\n epoch: null,\n generation: subscriptionState.replicaVersion,\n replicaID,\n },\n }\n : undefined,\n // The purge scheduler shares the writer's gate -- mode, not the\n // read path -- because `write` mode, with no read selector at all,\n // is the configuration it ships in.\n sqliteChangeLogPurge: sqliteChangeLogEnabled\n ? {\n retentionMs: sqliteChangeLogRetentionMs,\n batchRows: sqliteChangeLogPurgeBatchRows,\n }\n : undefined,\n // Compare mode runs both advisory checks. Postgres remains authoritative.\n sqliteChangeLogCompare:\n pgChangeLogEnabled && sqliteChangeLogComparing\n ? {\n replicaFile: replica.file,\n comparePercent: sqliteChangeLogComparePercent,\n retentionMs: sqliteChangeLogRetentionMs,\n readBatchRows: sqliteChangeLogReadBatchRows,\n }\n : undefined,\n // Slice 11 lands dark by default: serve mode constructs the stable\n // router, while readPercent=0 keeps every catchup on PG and emits\n // eligibility metrics before any canary traffic is enabled.\n sqliteChangeLogServe:\n sqliteChangeLogMode === 'serve'\n ? {\n readPercent: sqliteChangeLogReadPercent,\n coldReadPercent: sqliteChangeLogColdReadPercent,\n retentionMs: sqliteChangeLogRetentionMs,\n }\n : undefined,\n },\n setTimeout,\n );\n backupURL = destinationBackupURL;\n waitForFirstBackupBeforeServing = waitForBackupBeforeServing;\n newBackupLineage = initNewBackupLineage;\n break;\n } catch (e) {\n // Release the resources acquired by the failed attempt, e.g.:\n // * The claimed replication slot, which would otherwise remain active\n // indefinitely, preventing a retry from resuming (or forking) it.\n // * The purge lock. This is safe because the purge lock exists to\n // preserve change-log entries so the new change-streamer can resume\n // from the backup replica's watermark. An AutoResetSignal means we\n // can't resume from the backup replica (e.g. its replication slot is\n // gone), so the change-log entries the lock was protecting are no\n // longer needed. The retry performs a fresh initial sync with a new\n // replication slot, independent of the old change-log. Releasing is\n // also necessary to avoid a self-deadlock when CHANGE_DB ==\n // UPSTREAM_DB: CREATE_REPLICATION_SLOT waits for all older\n // transactions to finish, including this lock's open transaction.\n await cleanup.release();\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 // The change log carries the identity of the replica it was written\n // beside, and the retry performs a fresh initial sync with a new\n // replicaVersion. Reconciliation would catch that on its own (as\n // 'identity-mismatch') but only after the writer opened a file that is\n // known here to be garbage.\n deleteChangeLogDB(replica.file);\n continue; // execute again with a fresh initial-sync\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 // The backup-replicator is stopped with the other services, i.e. after the\n // ChangeStreamerHttpServer has drained its subscribers.\n const processes = new SupportingProcesses(lc, parent);\n const profileSubWorkers: Worker[] = [];\n const {promise: replicatorReady, resolve} = resolver();\n if (!backupURL) {\n resolve(); // No backup replicator to wait for, so resolve immediately.\n } else {\n lc.info?.('setting up backup to', backupURL);\n config.litestream.backupURLOverride = backupURL;\n if (newBackupLineage) {\n deleteLitestreamMetaDir(replica.file);\n }\n // Start a backup replicator, which starts up the corresponding\n // litestream backup process.\n const backupReplicator = processes\n .add(\n childWorker(\n REPLICATOR_URL,\n {...env, ZERO_LITESTREAM_BACKUP_URL_OVERRIDE: backupURL},\n 'backup' satisfies ReplicaFileMode,\n ),\n 'backup-replicator',\n )\n .onceMessageType('ready', () => resolve());\n profileSubWorkers.push(backupReplicator);\n // Relay profileResponse messages from backup-replicator up to parent\n backupReplicator.onMessageType<ProfileResponseMessage>(\n 'profileResponse',\n res => parent.send(['profileResponse', res]),\n );\n }\n\n const backupMonitor = createBackupCleanupMonitor({\n lc,\n config,\n replicaFile: replica.file,\n changeStreamer,\n });\n\n let backupReady = promiseVoid;\n if (waitForFirstBackupBeforeServing) {\n const start = performance.now();\n lc.info?.(`awaiting initial backup ...`);\n\n backupReady = backupMonitor.firstBackupReceived().then(() => {\n const elapsed = performance.now() - start;\n lc.info?.(`initial backup confirmed after ${elapsed.toFixed(2)}ms`);\n });\n }\n\n // In RMv2, readiness additionally waits for the backup-replicator to catch\n // up to the replication stream. This is not done in RMv1 (i.e. when the\n // PG change-log is enabled), in which the change-streamer itself is only\n // started after the readinessGate (plus a startup delay), and thus the\n // backup-replicator cannot catch up before then.\n const readinessGate = pgChangeLogEnabled\n ? backupReady\n : Promise.all([backupReady, replicatorReady]);\n\n // Create the broadcast facade once: each broadcastWorker() adds permanent\n // 'message' forwarders to every sub worker, so creating one per /profz\n // request would leak a forwarder per request.\n const profileWorker =\n profileSubWorkers.length > 0\n ? broadcastWorker(profileSubWorkers)\n : undefined;\n const getProfileWorker = profileWorker\n ? () => Promise.resolve(profileWorker)\n : undefined;\n\n const changeStreamerWebServer = new ChangeStreamerHttpServer(\n lc,\n {\n port,\n keepaliveTimeoutMs,\n // The startup delay is only relevant when taking over the PG change-log\n // (RMv1 and RMv1.5), and is disabled for RMv2.\n startupDelayMs: pgChangeLogEnabled ? startupDelayMs : 0,\n readinessGate,\n config,\n getProfileWorker,\n },\n parent,\n changeStreamer,\n );\n\n void readinessGate.then(() => parent.send(['ready', {ready: true}]));\n\n // Note: The changeStreamer itself is not started here; it is started by the\n // changeStreamerWebServer after a delay to ensure that routing\n // routing elements have registered the server before the\n // change-streamer takes over the replication slot.\n // TODO: Remove this delay and start the changeStreamer normally here once\n // transitioned to RMv2, since changeStreamer startup will no longer\n // disrupt an existing task.\n running = true;\n await runUntilKilled(\n lc,\n parent,\n changeStreamerWebServer,\n backupMonitor,\n processes,\n );\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(\n () => lc,\n () =>\n runWorker(\n must(parentWorker),\n process.env,\n ...process.argv.slice(2),\n ).catch(async e => {\n if (!running) {\n await publishCriticalEvent(\n lc,\n replicationStatusError(lc, 'Initializing', e),\n );\n }\n throw e;\n }),\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"change-streamer.js","names":[],"sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"sourcesContent":["import {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.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 {registerSQLiteCorruptionDiagnosticTarget} from '../db/sqlite-corruption.ts';\nimport {warmupConnections} from '../db/warmup.ts';\nimport {initEventSink, publishCriticalEvent} from '../observability/events.ts';\nimport {getOrCreateGauge} from '../observability/metrics.ts';\nimport {InitCleanup} from '../services/change-source/common/init-cleanup.ts';\nimport type {PgChangeLogPurgeLocker} from '../services/change-source/common/replica-restore.ts';\nimport {initializeCustomChangeSource} from '../services/change-source/custom/change-source.ts';\nimport {initializePostgresChangeSource} from '../services/change-source/pg/change-source-init.ts';\nimport {createBackupCleanupMonitor} from '../services/change-streamer/backup-cleanup-monitor-factory.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 {DEFAULT_READINESS_GRACE_PERIOD_MS} from '../services/change-streamer/restore-admission.ts';\nimport {initChangeStreamerSchema} from '../services/change-streamer/schema/init.ts';\nimport {AutoResetSignal} from '../services/change-streamer/schema/tables.ts';\nimport {\n PurgeLocker,\n type PurgeLock,\n} from '../services/change-streamer/storer.ts';\nimport {\n exitAfter,\n runUntilKilled,\n SupportingProcesses,\n} from '../services/life-cycle.ts';\nimport {deleteLitestreamMetaDir} from '../services/litestream/commands.ts';\nimport {\n changeLogFileName,\n deleteChangeLogDB,\n} from '../services/replicator/change-log-db.ts';\nimport {\n replicationStatusError,\n ReplicationStatusPublisher,\n} from '../services/replicator/replication-status.ts';\nimport {sqliteFileBytes} from '../services/replicator/sqlite-change-log-observability.ts';\nimport {warnIfPostgresUnpatched} from '../types/pg-patch.ts';\nimport {connectPgClient} from '../types/pg.ts';\nimport {\n broadcastWorker,\n childWorker,\n parentWorker,\n singleProcessMode,\n type ProfileResponseMessage,\n type Worker,\n} from '../types/processes.ts';\nimport {installProfileHandler} from '../types/profiler.ts';\nimport {getShardConfig} from '../types/shards.ts';\nimport type {ReplicaFileMode} from '../workers/replicator.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\nimport {REPLICATOR_URL} from './worker-urls.ts';\n\n// Default LogContext, overridden in runWorker\nlet lc = new LogContext('info', {}, consoleLogSink);\n\n// Errors thrown once the services are running are reported by the services\n// themselves (e.g. as 'Replicating' status events), and are thus not\n// published as 'Initializing' errors.\nlet running = false;\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...argv: string[]\n): Promise<void> {\n installProfileHandler(parent, 'change-streamer');\n const config = getNormalizedZeroConfig({env, argv});\n const {\n taskID,\n changeStreamer: {\n port,\n address,\n protocol,\n secret,\n startupDelayMs,\n backPressureLimitHeapProportion,\n flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodSeconds,\n maxConcurrentSwitchoverRestores,\n pgChangeLogEnabled,\n sqliteChangeLogMode,\n sqliteChangeLogReadPercent,\n sqliteChangeLogColdReadPercent,\n sqliteChangeLogComparePercent,\n sqliteChangeLogRetentionMs,\n sqliteChangeLogReadBatchRows,\n sqliteChangeLogPurgeBatchRows,\n sqliteChangeLogBarrierTimeoutMs,\n },\n autoReset,\n replicationLag,\n litestream,\n upstream,\n change,\n replica,\n initialSync,\n keepaliveTimeoutMs,\n sqliteCorruptionChecks,\n } = config;\n\n startOtelAuto(\n createLogContext(config, 'change-streamer', 0, false),\n 'change-streamer',\n 0,\n );\n lc = createLogContext(config, 'change-streamer');\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: 'change-streamer replica',\n dbPath: replica.file,\n },\n sqliteCorruptionChecks,\n );\n initEventSink(lc, config);\n if (upstream.type === 'pg') {\n warnIfPostgresUnpatched(lc);\n }\n\n // Startup-time client used for for change-streamer initialization\n // and handoff / takeover. Steady-state clients are managed by the\n // change-streamer (Storer) implementation.\n const changeDB = await connectPgClient(\n lc,\n change.db,\n 'change-streamer-init',\n {max: 5},\n {sendStringAsJson: true},\n );\n void warmupConnections(lc, changeDB, 'change').catch(() => {});\n\n const shard = getShardConfig(config);\n\n // Ensure the change DB schema is initialized/up-to-date.\n await initChangeStreamerSchema(lc, changeDB, shard);\n\n let purgeLock = null as PurgeLock | null;\n let changeStreamer: ChangeStreamerService | undefined;\n let backupURL: string | undefined;\n\n const context = getServerContext(config);\n const sqliteChangeLogEnabled = sqliteChangeLogMode !== 'off';\n // The modes are cumulative, so `serve` implies `compare`.\n const sqliteChangeLogComparing =\n sqliteChangeLogMode === 'compare' || sqliteChangeLogMode === 'serve';\n if (sqliteChangeLogEnabled) {\n const changeLogFile = changeLogFileName(replica.file);\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: 'change-streamer change-log',\n dbPath: changeLogFile,\n },\n sqliteCorruptionChecks,\n );\n // The log's bytes are accounted for in the process that writes them. This\n // is local disk only — the log is excluded from the litestream backup — and\n // it is a whole-database total, because a table that left the replica left\n // through its wal: the replica's footprint is what shrank and this is where\n // it went.\n getOrCreateGauge('replica', 'sqlite_change_log.file_bytes', {\n description:\n `The SQLite change log's total on-disk footprint: its main db file ` +\n `plus its wal sidecars.`,\n unit: 'By',\n }).addCallback(async o =>\n o.observe(await sqliteFileBytes(lc, changeLogFile)),\n );\n }\n\n let waitForFirstBackupBeforeServing = false;\n let newBackupLineage = false;\n for (const first of [true, false]) {\n // Resources acquired by an initialization attempt (e.g. a claimed\n // replication slot, the purge lock) to be released if the attempt fails.\n const cleanup = new InitCleanup(lc);\n // When restoring from litestream, a lock is acquired to prevent change-log\n // purges. This ensures that (this) change-streamer will be able to resume\n // from the backup. The lock is acquired by the change source initialization\n // just before restoring (i.e. after a replication slot is created, if\n // applicable), and released once this change-streamer takes over the\n // change-log.\n const acquirePurgeLock: PgChangeLogPurgeLocker | undefined =\n pgChangeLogEnabled && litestream.backupURL && litestream.executable\n ? async (slotWatermark?: string) => {\n const lock = await new PurgeLocker(lc, shard, changeDB).acquire(\n slotWatermark,\n );\n purgeLock = lock === 'behind-slot' ? null : lock;\n if (purgeLock) {\n const acquired = purgeLock;\n cleanup.onFailure('purge lock', () => {\n purgeLock = null;\n return acquired.release();\n });\n }\n return lock;\n }\n : undefined;\n\n const restoreOptions = {litestream, acquirePurgeLock, cleanup};\n try {\n // Note: This performs initial sync of the replica if necessary.\n const {\n pgReplicationEpoch: epoch,\n pgReplicationSlotPerReplica: slotPerReplica,\n pgResumeOrphanedSlotGracePeriodMs: inactiveReplicaGracePeriodMs,\n } = upstream;\n const {\n changeSource,\n subscriptionState,\n destinationBackupURL,\n replicaID,\n waitForBackupBeforeServing,\n newBackupLineage: initNewBackupLineage,\n pgChangeLogBehindSlot,\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 installPartialIndexTriggers: upstream.pgPartialIndexTriggers,\n },\n context,\n replicationLag.reportIntervalMs,\n restoreOptions,\n {\n epoch,\n slotPerReplica,\n inactiveReplicaGracePeriodMs,\n backupV5: litestream.backupUsingV5,\n pgChangeLog: pgChangeLogEnabled,\n },\n upstream.pgStreamInboundTimeoutMs,\n )\n : await initializeCustomChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n context,\n restoreOptions,\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 destinationBackupURL\n ? {\n backupURL: destinationBackupURL,\n litestreamVersion: litestream.backupUsingV5 ? 'v5' : 'legacy',\n replicaFile: replica.file,\n }\n : null,\n purgeLock,\n autoReset ?? false,\n {\n pgChangeLogEnabled,\n pgChangeLogBehindSlot: pgChangeLogBehindSlot ?? false,\n backPressureLimitHeapProportion,\n flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodMs:\n flowControlSlowSubscriberGracePeriodSeconds > 0\n ? flowControlSlowSubscriberGracePeriodSeconds * 1000\n : undefined,\n maxConcurrentSwitchoverRestores,\n // When lag reporting is enabled, a restored view-syncer waits for\n // the next lag report (sent at an interval with ~1 second of\n // jitter) before becoming ready.\n switchoverRestoreReadinessGracePeriodMs:\n (replicationLag.reportIntervalMs > 0\n ? replicationLag.reportIntervalMs + 1000\n : 0) + DEFAULT_READINESS_GRACE_PERIOD_MS,\n statementTimeoutMs: change.statementTimeoutMs,\n changeLogBatchSize: change.logBatchSize,\n sqliteCatchup: {\n changeLogFile: changeLogFileName(replica.file),\n readBatchRows: sqliteChangeLogReadBatchRows,\n barrierTimeoutMs: sqliteChangeLogBarrierTimeoutMs,\n },\n // The presence of these options is the writer's gate. This process\n // performs the restore and the initial sync, so the replica's\n // identity is in hand at the moment the log is opened.\n sqliteChangeLogWriter: sqliteChangeLogEnabled\n ? {\n replicaFile: replica.file,\n identity: {\n // RMv2 supplies the epoch; until then the generation and the\n // replica ID are the whole of the identity.\n epoch: null,\n generation: subscriptionState.replicaVersion,\n replicaID,\n },\n }\n : undefined,\n // The purge scheduler shares the writer's gate -- mode, not the\n // read path -- because `write` mode, with no read selector at all,\n // is the configuration it ships in.\n sqliteChangeLogPurge: sqliteChangeLogEnabled\n ? {\n retentionMs: sqliteChangeLogRetentionMs,\n batchRows: sqliteChangeLogPurgeBatchRows,\n }\n : undefined,\n // Compare mode runs both advisory checks. Postgres remains authoritative.\n sqliteChangeLogCompare:\n pgChangeLogEnabled && sqliteChangeLogComparing\n ? {\n replicaFile: replica.file,\n comparePercent: sqliteChangeLogComparePercent,\n retentionMs: sqliteChangeLogRetentionMs,\n readBatchRows: sqliteChangeLogReadBatchRows,\n }\n : undefined,\n // Slice 11 lands dark by default: serve mode constructs the stable\n // router, while readPercent=0 keeps every catchup on PG and emits\n // eligibility metrics before any canary traffic is enabled.\n sqliteChangeLogServe:\n sqliteChangeLogMode === 'serve'\n ? {\n readPercent: sqliteChangeLogReadPercent,\n coldReadPercent: sqliteChangeLogColdReadPercent,\n retentionMs: sqliteChangeLogRetentionMs,\n }\n : undefined,\n },\n setTimeout,\n );\n backupURL = destinationBackupURL;\n waitForFirstBackupBeforeServing = waitForBackupBeforeServing;\n newBackupLineage = initNewBackupLineage;\n break;\n } catch (e) {\n // Release the resources acquired by the failed attempt, e.g.:\n // * The claimed replication slot, which would otherwise remain active\n // indefinitely, preventing a retry from resuming (or forking) it.\n // * The purge lock. This is safe because the purge lock exists to\n // preserve change-log entries so the new change-streamer can resume\n // from the backup replica's watermark. An AutoResetSignal means we\n // can't resume from the backup replica (e.g. its replication slot is\n // gone), so the change-log entries the lock was protecting are no\n // longer needed. The retry performs a fresh initial sync with a new\n // replication slot, independent of the old change-log. Releasing is\n // also necessary to avoid a self-deadlock when CHANGE_DB ==\n // UPSTREAM_DB: CREATE_REPLICATION_SLOT waits for all older\n // transactions to finish, including this lock's open transaction.\n await cleanup.release();\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 // The change log carries the identity of the replica it was written\n // beside, and the retry performs a fresh initial sync with a new\n // replicaVersion. Reconciliation would catch that on its own (as\n // 'identity-mismatch') but only after the writer opened a file that is\n // known here to be garbage.\n deleteChangeLogDB(replica.file);\n continue; // execute again with a fresh initial-sync\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 // The backup-replicator is stopped with the other services, i.e. after the\n // ChangeStreamerHttpServer has drained its subscribers.\n const processes = new SupportingProcesses(lc, parent);\n const profileSubWorkers: Worker[] = [];\n const {promise: replicatorReady, resolve} = resolver();\n if (!backupURL) {\n resolve(); // No backup replicator to wait for, so resolve immediately.\n } else {\n lc.info?.('setting up backup to', backupURL);\n config.litestream.backupURLOverride = backupURL;\n if (newBackupLineage) {\n deleteLitestreamMetaDir(replica.file);\n }\n // Start a backup replicator, which starts up the corresponding\n // litestream backup process.\n const backupReplicator = processes\n .add(\n childWorker(\n REPLICATOR_URL,\n {...env, ZERO_LITESTREAM_BACKUP_URL_OVERRIDE: backupURL},\n 'backup' satisfies ReplicaFileMode,\n ),\n 'backup-replicator',\n )\n .onceMessageType('ready', () => resolve());\n profileSubWorkers.push(backupReplicator);\n // Relay profileResponse messages from backup-replicator up to parent\n backupReplicator.onMessageType<ProfileResponseMessage>(\n 'profileResponse',\n res => parent.send(['profileResponse', res]),\n );\n }\n\n const backupMonitor = createBackupCleanupMonitor({\n lc,\n config,\n replicaFile: replica.file,\n changeStreamer,\n });\n\n let backupReady = promiseVoid;\n if (waitForFirstBackupBeforeServing) {\n const start = performance.now();\n lc.info?.(`awaiting initial backup ...`);\n\n backupReady = backupMonitor.firstBackupReceived().then(() => {\n const elapsed = performance.now() - start;\n lc.info?.(`initial backup confirmed after ${elapsed.toFixed(2)}ms`);\n });\n }\n\n // In RMv2, readiness additionally waits for the backup-replicator to catch\n // up to the replication stream. This is not done in RMv1 (i.e. when the\n // PG change-log is enabled), in which the change-streamer itself is only\n // started after the readinessGate (plus a startup delay), and thus the\n // backup-replicator cannot catch up before then.\n const readinessGate = pgChangeLogEnabled\n ? backupReady\n : Promise.all([backupReady, replicatorReady]);\n\n // Create the broadcast facade once: each broadcastWorker() adds permanent\n // 'message' forwarders to every sub worker, so creating one per /profz\n // request would leak a forwarder per request.\n const profileWorker =\n profileSubWorkers.length > 0\n ? broadcastWorker(profileSubWorkers)\n : undefined;\n const getProfileWorker = profileWorker\n ? () => Promise.resolve(profileWorker)\n : undefined;\n\n if (secret === undefined) {\n lc.warn?.(\n `No --change-streamer-secret is configured: the change-streamer ` +\n `accepts replication connections from anything that can reach port ` +\n `${port}. Set ZERO_CHANGE_STREAMER_SECRET to the same value on the ` +\n `replication-manager and on every view-syncer.`,\n );\n }\n\n const changeStreamerWebServer = new ChangeStreamerHttpServer(\n lc,\n {\n port,\n secret,\n keepaliveTimeoutMs,\n // The startup delay is only relevant when taking over the PG change-log\n // (RMv1 and RMv1.5), and is disabled for RMv2.\n startupDelayMs: pgChangeLogEnabled ? startupDelayMs : 0,\n readinessGate,\n config,\n getProfileWorker,\n },\n parent,\n changeStreamer,\n );\n\n void readinessGate.then(() => parent.send(['ready', {ready: true}]));\n\n // Note: The changeStreamer itself is not started here; it is started by the\n // changeStreamerWebServer after a delay to ensure that routing\n // routing elements have registered the server before the\n // change-streamer takes over the replication slot.\n // TODO: Remove this delay and start the changeStreamer normally here once\n // transitioned to RMv2, since changeStreamer startup will no longer\n // disrupt an existing task.\n running = true;\n await runUntilKilled(\n lc,\n parent,\n changeStreamerWebServer,\n backupMonitor,\n processes,\n );\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(\n () => lc,\n () =>\n runWorker(\n must(parentWorker),\n process.env,\n ...process.argv.slice(2),\n ).catch(async e => {\n if (!running) {\n await publishCriticalEvent(\n lc,\n replicationStatusError(lc, 'Initializing', e),\n );\n }\n throw e;\n }),\n );\n}\n"],"mappings":"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"}
@@ -5,6 +5,7 @@ import type { QueryServerMetrics as QueryServerMetricsJSON } from '../../../zero
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
7
  import type { ConnectionContext } from '../services/view-syncer/connection-context-manager.ts';
8
+ import { InspectorAuthLimiter } from '../services/view-syncer/inspect-auth-limiter.ts';
8
9
  /**
9
10
  * Server-side metrics collected for queries during materialization and update.
10
11
  * These metrics are reported via the inspector and complement client-side metrics.
@@ -14,12 +15,6 @@ export type ServerMetrics = {
14
15
  'query-materialization-server': TDigest;
15
16
  'query-update-server': TDigest;
16
17
  };
17
- type ClientGroupID = string;
18
- /**
19
- * The service that established a client group's authentication, i.e. the
20
- * ViewSyncer serving it. See {@link InspectorDelegate.clearAuthenticated}.
21
- */
22
- type AuthOwner = object;
23
18
  /**
24
19
  * `query-update-server` is reported for every change pushed into every
25
20
  * pipeline: millions of values a minute while a bulk write fans out across
@@ -42,24 +37,7 @@ export declare class InspectorDelegate implements MetricsDelegate {
42
37
  getASTForQuery(queryID: string): AST | undefined;
43
38
  removeQuery(queryID: string): void;
44
39
  addQuery(queryID: string, ast: AST): void;
45
- /**
46
- * Check if the client is authenticated. We only require authentication once
47
- * per "worker".
48
- */
49
- isAuthenticated(clientGroupID: ClientGroupID): boolean;
50
- /**
51
- * Marks the client group as authenticated on behalf of `owner`, the service
52
- * (ViewSyncer) that handled the authentication.
53
- */
54
- setAuthenticated(clientGroupID: ClientGroupID, owner?: AuthOwner): void;
55
- /**
56
- * Clears the client group's authentication. When `owner` is given, the
57
- * authentication is only cleared if it was established by that owner: a
58
- * replacement ViewSyncer can start for the same client group while the
59
- * previous one is still shutting down, and the previous one's cleanup must
60
- * not revoke what the replacement has since authenticated.
61
- */
62
- clearAuthenticated(clientGroupID: ClientGroupID, owner?: AuthOwner): void;
40
+ get authLimiter(): InspectorAuthLimiter;
63
41
  /**
64
42
  * Transforms a single custom query by name and args using the configured
65
43
  * CustomQueryTransformer. This is primarily used by the inspector to transform
@@ -67,5 +45,4 @@ export declare class InspectorDelegate implements MetricsDelegate {
67
45
  */
68
46
  transformCustomQuery(name: string, args: readonly ReadonlyJSONValue[], ctx: ConnectionContext): Promise<AST>;
69
47
  }
70
- export {};
71
48
  //# 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;AAGnE,OAAO,EAAC,OAAO,EAAC,MAAM,gCAAgC,CAAC;AACvD,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,mCAAmC,CAAC;AAC3D,OAAO,KAAK,EAAC,kBAAkB,IAAI,sBAAsB,EAAC,MAAM,4CAA4C,CAAC;AAE7G,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;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,8BAA8B,EAAE,OAAO,CAAC;IACxC,qBAAqB,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,KAAK,aAAa,GAAG,MAAM,CAAC;AAE5B;;;GAGG;AACH,KAAK,SAAS,GAAG,MAAM,CAAC;AAWxB;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAYrC,qBAAa,iBAAkB,YAAW,eAAe;;IAOvD,YAAY,sBAAsB,EAAE,sBAAsB,GAAG,SAAS,EAErE;IAED,SAAS,CAAC,CAAC,SAAS,MAAM,SAAS,EACjC,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,MAAM,EACb,GAAG,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,GACpB,IAAI,CAiBN;IAED,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAUrE;IAED,cAAc;;;MAEb;IAED,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAE/C;IAED,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIjC;IAED,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI,CAExC;IAED;;;OAGG;IACH,eAAe,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAIrD;IAED;;;OAGG;IACH,gBAAgB,CACd,aAAa,EAAE,aAAa,EAC5B,KAAK,GAAE,SAAoB,GAC1B,IAAI,CAEN;IAED;;;;;;OAMG;IACH,kBAAkB,CAAC,aAAa,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,SAAS,QAOjE;IAED;;;;OAIG;IACG,oBAAoB,CACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,SAAS,iBAAiB,EAAE,EAClC,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,GAAG,CAAC,CAsCd;CACF"}
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;AAGnE,OAAO,EAAC,OAAO,EAAC,MAAM,gCAAgC,CAAC;AACvD,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,mCAAmC,CAAC;AAC3D,OAAO,KAAK,EAAC,kBAAkB,IAAI,sBAAsB,EAAC,MAAM,4CAA4C,CAAC;AAE7G,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,eAAe,EACrB,MAAM,4CAA4C,CAAC;AACpD,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,sCAAsC,CAAC;AACjF,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,uDAAuD,CAAC;AAC7F,OAAO,EAAC,oBAAoB,EAAC,MAAM,iDAAiD,CAAC;AAIrF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,8BAA8B,EAAE,OAAO,CAAC;IACxC,qBAAqB,EAAE,OAAO,CAAC;CAChC,CAAC;AAQF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAYrC,qBAAa,iBAAkB,YAAW,eAAe;;IAOvD,YAAY,sBAAsB,EAAE,sBAAsB,GAAG,SAAS,EAErE;IAED,SAAS,CAAC,CAAC,SAAS,MAAM,SAAS,EACjC,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,MAAM,EACb,GAAG,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,GACpB,IAAI,CAiBN;IAED,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAUrE;IAED,cAAc;;;MAEb;IAED,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAE/C;IAED,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIjC;IAED,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI,CAExC;IAED,IAAI,WAAW,IAAI,oBAAoB,CAEtC;IAED;;;;OAIG;IACG,oBAAoB,CACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,SAAS,iBAAiB,EAAE,EAClC,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,GAAG,CAAC,CAsCd;CACF"}
@@ -5,16 +5,14 @@ import { mapValues } from "../../../shared/src/objects.js";
5
5
  import { isServerMetric } from "../../../zql/src/query/metrics-delegate.js";
6
6
  import "../../../zero-protocol/src/query-hash.js";
7
7
  import { TDigest } from "../../../shared/src/tdigest.js";
8
- import { isDevelopmentMode } from "../config/normalize.js";
9
8
  import { ProtocolErrorWithLevel } from "../types/error-with-level.js";
9
+ import { InspectorAuthLimiter } from "../services/view-syncer/inspect-auth-limiter.js";
10
10
  //#region ../zero-cache/src/server/inspector-delegate.ts
11
- var NO_OWNER = {};
12
11
  /**
13
- * Authenticated client group IDs, each mapped to the owner that authenticated
14
- * it. We keep this outside of the class to share this state across all
15
- * instances of the InspectorDelegate.
12
+ * Limits attempts at the admin password. Kept outside of the class so that the
13
+ * counters cover every client group served by the worker.
16
14
  */
17
- var authenticatedClientGroupIDs = /* @__PURE__ */ new Map();
15
+ var authLimiter = new InspectorAuthLimiter();
18
16
  function newUpdateMetrics() {
19
17
  return {
20
18
  digest: new TDigest(),
@@ -67,29 +65,8 @@ var InspectorDelegate = class {
67
65
  addQuery(queryID, ast) {
68
66
  this.#queryIDToAST.set(queryID, ast);
69
67
  }
70
- /**
71
- * Check if the client is authenticated. We only require authentication once
72
- * per "worker".
73
- */
74
- isAuthenticated(clientGroupID) {
75
- return isDevelopmentMode() || authenticatedClientGroupIDs.has(clientGroupID);
76
- }
77
- /**
78
- * Marks the client group as authenticated on behalf of `owner`, the service
79
- * (ViewSyncer) that handled the authentication.
80
- */
81
- setAuthenticated(clientGroupID, owner = NO_OWNER) {
82
- authenticatedClientGroupIDs.set(clientGroupID, owner);
83
- }
84
- /**
85
- * Clears the client group's authentication. When `owner` is given, the
86
- * authentication is only cleared if it was established by that owner: a
87
- * replacement ViewSyncer can start for the same client group while the
88
- * previous one is still shutting down, and the previous one's cleanup must
89
- * not revoke what the replacement has since authenticated.
90
- */
91
- clearAuthenticated(clientGroupID, owner) {
92
- if (owner === void 0 || authenticatedClientGroupIDs.get(clientGroupID) === owner) authenticatedClientGroupIDs.delete(clientGroupID);
68
+ get authLimiter() {
69
+ return authLimiter;
93
70
  }
94
71
  /**
95
72
  * Transforms a single custom query by name and args using the configured
@@ -1 +1 @@
1
- {"version":3,"file":"inspector-delegate.js","names":["hashOfNameAndArgs"],"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 {getOrInsertComputed} from '../../../shared/src/map.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 {QueryServerMetrics as QueryServerMetricsJSON} 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 * Used for the global aggregate (all queries combined).\n */\nexport type ServerMetrics = {\n 'query-materialization-server': TDigest;\n 'query-update-server': TDigest;\n};\n\ntype ClientGroupID = string;\n\n/**\n * The service that established a client group's authentication, i.e. the\n * ViewSyncer serving it. See {@link InspectorDelegate.clearAuthenticated}.\n */\ntype AuthOwner = object;\n\nconst NO_OWNER: AuthOwner = {};\n\n/**\n * Authenticated client group IDs, each mapped to the owner that authenticated\n * it. We keep this outside of the class to share this state across all\n * instances of the InspectorDelegate.\n */\nconst authenticatedClientGroupIDs = new Map<ClientGroupID, AuthOwner>();\n\n/**\n * `query-update-server` is reported for every change pushed into every\n * pipeline: millions of values a minute while a bulk write fans out across\n * many client groups. Inserting each into a TDigest (two allocations, and a\n * sort every few hundred inserts) cost ~16% of a busy syncer's CPU. Instead,\n * every UPDATE_SAMPLE_RATE-th value of each query is recorded, weighted by\n * the rate, so the digests keep describing the distribution and, to within\n * one sample, the count.\n */\nexport const UPDATE_SAMPLE_RATE = 32;\n\ntype UpdateMetrics = {\n readonly digest: TDigest;\n /** Values reported for the query, recorded or not. */\n reported: number;\n};\n\nfunction newUpdateMetrics(): UpdateMetrics {\n return {digest: new TDigest(), reported: 0};\n}\n\nexport class InspectorDelegate implements MetricsDelegate {\n readonly #globalMetrics: ServerMetrics = newMetrics();\n readonly #perQueryHydrateMs = new Map<string, number>();\n readonly #perQueryUpdateMetrics = new Map<string, UpdateMetrics>();\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 if (metric === 'query-materialization-server') {\n this.#perQueryHydrateMs.set(queryID, value);\n this.#globalMetrics[metric].add(value);\n return;\n }\n const update = getOrInsertComputed(\n this.#perQueryUpdateMetrics,\n queryID,\n newUpdateMetrics,\n );\n if (update.reported++ % UPDATE_SAMPLE_RATE === 0) {\n update.digest.add(value, UPDATE_SAMPLE_RATE);\n this.#globalMetrics[metric].add(value, UPDATE_SAMPLE_RATE);\n }\n }\n\n getMetricsJSONForQuery(queryID: string): QueryServerMetricsJSON | null {\n const hydrateMs = this.#perQueryHydrateMs.get(queryID);\n const updateMetrics = this.#perQueryUpdateMetrics.get(queryID);\n if (hydrateMs === undefined && updateMetrics === undefined) {\n return null;\n }\n return {\n 'query-hydration-server-ms': hydrateMs,\n 'query-update-server': (updateMetrics?.digest ?? new TDigest()).toJSON(),\n };\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.#perQueryHydrateMs.delete(queryID);\n this.#perQueryUpdateMetrics.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 /**\n * Marks the client group as authenticated on behalf of `owner`, the service\n * (ViewSyncer) that handled the authentication.\n */\n setAuthenticated(\n clientGroupID: ClientGroupID,\n owner: AuthOwner = NO_OWNER,\n ): void {\n authenticatedClientGroupIDs.set(clientGroupID, owner);\n }\n\n /**\n * Clears the client group's authentication. When `owner` is given, the\n * authentication is only cleared if it was established by that owner: a\n * replacement ViewSyncer can start for the same client group while the\n * previous one is still shutting down, and the previous one's cleanup must\n * not revoke what the replacement has since authenticated.\n */\n clearAuthenticated(clientGroupID: ClientGroupID, owner?: AuthOwner) {\n if (\n owner === undefined ||\n authenticatedClientGroupIDs.get(clientGroupID) === owner\n ) {\n authenticatedClientGroupIDs.delete(clientGroupID);\n }\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":";;;;;;;;;;AAqCA,IAAM,WAAsB,CAAC;;;;;;AAO7B,IAAM,8CAA8B,IAAI,IAA8B;AAmBtE,SAAS,mBAAkC;CACzC,OAAO;EAAC,QAAQ,IAAI,QAAQ;EAAG,UAAU;CAAC;AAC5C;AAEA,IAAa,oBAAb,MAA0D;CACxD,iBAAyC,WAAW;CACpD,qCAA8B,IAAI,IAAoB;CACtD,yCAAkC,IAAI,IAA2B;CACjE,gCAA2C,IAAI,IAAI;CACnD;CAEA,YAAY,wBAA4D;EACtE,KAAK,0BAA0B;CACjC;CAEA,UACE,QACA,OACA,GAAG,MACG;EACN,OAAO,eAAe,MAAM,GAAG,0BAA0B,QAAQ;EACjE,MAAM,UAAU,KAAK;EACrB,IAAI,WAAW,gCAAgC;GAC7C,KAAK,mBAAmB,IAAI,SAAS,KAAK;GAC1C,KAAK,eAAe,OAAO,CAAC,IAAI,KAAK;GACrC;EACF;EACA,MAAM,SAAS,oBACb,KAAK,wBACL,SACA,gBACF;EACA,IAAI,OAAO,aAAA,OAAoC,GAAG;GAChD,OAAO,OAAO,IAAI,OAAA,EAAyB;GAC3C,KAAK,eAAe,OAAO,CAAC,IAAI,OAAA,EAAyB;EAC3D;CACF;CAEA,uBAAuB,SAAgD;EACrE,MAAM,YAAY,KAAK,mBAAmB,IAAI,OAAO;EACrD,MAAM,gBAAgB,KAAK,uBAAuB,IAAI,OAAO;EAC7D,IAAI,cAAc,KAAA,KAAa,kBAAkB,KAAA,GAC/C,OAAO;EAET,OAAO;GACL,6BAA6B;GAC7B,wBAAwB,eAAe,UAAU,IAAI,QAAQ,EAAA,CAAG,OAAO;EACzE;CACF;CAEA,iBAAiB;EACf,OAAO,UAAU,KAAK,iBAAgB,MAAK,EAAE,OAAO,CAAC;CACvD;CAEA,eAAe,SAAkC;EAC/C,OAAO,KAAK,cAAc,IAAI,OAAO;CACvC;CAEA,YAAY,SAAuB;EACjC,KAAK,mBAAmB,OAAO,OAAO;EACtC,KAAK,uBAAuB,OAAO,OAAO;EAC1C,KAAK,cAAc,OAAO,OAAO;CACnC;CAEA,SAAS,SAAiB,KAAgB;EACxC,KAAK,cAAc,IAAI,SAAS,GAAG;CACrC;;;;;CAMA,gBAAgB,eAAuC;EACrD,OACE,kBAAkB,KAAK,4BAA4B,IAAI,aAAa;CAExE;;;;;CAMA,iBACE,eACA,QAAmB,UACb;EACN,4BAA4B,IAAI,eAAe,KAAK;CACtD;;;;;;;;CASA,mBAAmB,eAA8B,OAAmB;EAClE,IACE,UAAU,KAAA,KACV,4BAA4B,IAAI,aAAa,MAAM,OAEnD,4BAA4B,OAAO,aAAa;CAEpD;;;;;;CAOA,MAAM,qBACJ,MACA,MACA,KACc;EACd,OACE,KAAK,yBACL,mFACF;EAIA,MAAM,UAA+B,CACnC;GACE,IAHYA,gBAAkB,MAAM,IAGhC;GACJ,MAAM;GACN;GACA;GACA,aAAa,CAAC;EAChB,CACF;EAEA,MAAM,UAAU,MAAM,KAAK,wBAAwB,UAAU,KAAK,OAAO;EAEzE,IAAI,UAAU,QAAQ,QACpB,MAAM,IAAI,uBAAuB,QAAQ,QAAQ,MAAM;EAGzD,MAAM,SAAS,QAAQ,OAAO;EAC9B,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,mCAAmC;EAGrD,IAAI,WAAW,QAAQ;GACrB,MAAM,UACJ,OAAO,WAAW;GACpB,MAAM,IAAI,MACR,mCAAmC,KAAK,IAAI,OAAO,MAAM,KAAK,QAAQ,GAAG,KAAK,UAAU,OAAO,OAAO,GACxG;EACF;EAEA,OAAO,OAAO;CAChB;AACF;AAEA,SAAS,aAA4B;CACnC,OAAO;EACL,gCAAgC,IAAI,QAAQ;EAC5C,uBAAuB,IAAI,QAAQ;CACrC;AACF"}
1
+ {"version":3,"file":"inspector-delegate.js","names":["hashOfNameAndArgs"],"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 {getOrInsertComputed} from '../../../shared/src/map.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 {QueryServerMetrics as QueryServerMetricsJSON} 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 type {CustomQueryTransformer} from '../custom-queries/transform-query.ts';\nimport type {ConnectionContext} from '../services/view-syncer/connection-context-manager.ts';\nimport {InspectorAuthLimiter} from '../services/view-syncer/inspect-auth-limiter.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 * Used for the global aggregate (all queries combined).\n */\nexport type ServerMetrics = {\n 'query-materialization-server': TDigest;\n 'query-update-server': TDigest;\n};\n\n/**\n * Limits attempts at the admin password. Kept outside of the class so that the\n * counters cover every client group served by the worker.\n */\nconst authLimiter = new InspectorAuthLimiter();\n\n/**\n * `query-update-server` is reported for every change pushed into every\n * pipeline: millions of values a minute while a bulk write fans out across\n * many client groups. Inserting each into a TDigest (two allocations, and a\n * sort every few hundred inserts) cost ~16% of a busy syncer's CPU. Instead,\n * every UPDATE_SAMPLE_RATE-th value of each query is recorded, weighted by\n * the rate, so the digests keep describing the distribution and, to within\n * one sample, the count.\n */\nexport const UPDATE_SAMPLE_RATE = 32;\n\ntype UpdateMetrics = {\n readonly digest: TDigest;\n /** Values reported for the query, recorded or not. */\n reported: number;\n};\n\nfunction newUpdateMetrics(): UpdateMetrics {\n return {digest: new TDigest(), reported: 0};\n}\n\nexport class InspectorDelegate implements MetricsDelegate {\n readonly #globalMetrics: ServerMetrics = newMetrics();\n readonly #perQueryHydrateMs = new Map<string, number>();\n readonly #perQueryUpdateMetrics = new Map<string, UpdateMetrics>();\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 if (metric === 'query-materialization-server') {\n this.#perQueryHydrateMs.set(queryID, value);\n this.#globalMetrics[metric].add(value);\n return;\n }\n const update = getOrInsertComputed(\n this.#perQueryUpdateMetrics,\n queryID,\n newUpdateMetrics,\n );\n if (update.reported++ % UPDATE_SAMPLE_RATE === 0) {\n update.digest.add(value, UPDATE_SAMPLE_RATE);\n this.#globalMetrics[metric].add(value, UPDATE_SAMPLE_RATE);\n }\n }\n\n getMetricsJSONForQuery(queryID: string): QueryServerMetricsJSON | null {\n const hydrateMs = this.#perQueryHydrateMs.get(queryID);\n const updateMetrics = this.#perQueryUpdateMetrics.get(queryID);\n if (hydrateMs === undefined && updateMetrics === undefined) {\n return null;\n }\n return {\n 'query-hydration-server-ms': hydrateMs,\n 'query-update-server': (updateMetrics?.digest ?? new TDigest()).toJSON(),\n };\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.#perQueryHydrateMs.delete(queryID);\n this.#perQueryUpdateMetrics.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 get authLimiter(): InspectorAuthLimiter {\n return authLimiter;\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,cAAc,IAAI,qBAAqB;AAmB7C,SAAS,mBAAkC;CACzC,OAAO;EAAC,QAAQ,IAAI,QAAQ;EAAG,UAAU;CAAC;AAC5C;AAEA,IAAa,oBAAb,MAA0D;CACxD,iBAAyC,WAAW;CACpD,qCAA8B,IAAI,IAAoB;CACtD,yCAAkC,IAAI,IAA2B;CACjE,gCAA2C,IAAI,IAAI;CACnD;CAEA,YAAY,wBAA4D;EACtE,KAAK,0BAA0B;CACjC;CAEA,UACE,QACA,OACA,GAAG,MACG;EACN,OAAO,eAAe,MAAM,GAAG,0BAA0B,QAAQ;EACjE,MAAM,UAAU,KAAK;EACrB,IAAI,WAAW,gCAAgC;GAC7C,KAAK,mBAAmB,IAAI,SAAS,KAAK;GAC1C,KAAK,eAAe,OAAO,CAAC,IAAI,KAAK;GACrC;EACF;EACA,MAAM,SAAS,oBACb,KAAK,wBACL,SACA,gBACF;EACA,IAAI,OAAO,aAAA,OAAoC,GAAG;GAChD,OAAO,OAAO,IAAI,OAAA,EAAyB;GAC3C,KAAK,eAAe,OAAO,CAAC,IAAI,OAAA,EAAyB;EAC3D;CACF;CAEA,uBAAuB,SAAgD;EACrE,MAAM,YAAY,KAAK,mBAAmB,IAAI,OAAO;EACrD,MAAM,gBAAgB,KAAK,uBAAuB,IAAI,OAAO;EAC7D,IAAI,cAAc,KAAA,KAAa,kBAAkB,KAAA,GAC/C,OAAO;EAET,OAAO;GACL,6BAA6B;GAC7B,wBAAwB,eAAe,UAAU,IAAI,QAAQ,EAAA,CAAG,OAAO;EACzE;CACF;CAEA,iBAAiB;EACf,OAAO,UAAU,KAAK,iBAAgB,MAAK,EAAE,OAAO,CAAC;CACvD;CAEA,eAAe,SAAkC;EAC/C,OAAO,KAAK,cAAc,IAAI,OAAO;CACvC;CAEA,YAAY,SAAuB;EACjC,KAAK,mBAAmB,OAAO,OAAO;EACtC,KAAK,uBAAuB,OAAO,OAAO;EAC1C,KAAK,cAAc,OAAO,OAAO;CACnC;CAEA,SAAS,SAAiB,KAAgB;EACxC,KAAK,cAAc,IAAI,SAAS,GAAG;CACrC;CAEA,IAAI,cAAoC;EACtC,OAAO;CACT;;;;;;CAOA,MAAM,qBACJ,MACA,MACA,KACc;EACd,OACE,KAAK,yBACL,mFACF;EAIA,MAAM,UAA+B,CACnC;GACE,IAHYA,gBAAkB,MAAM,IAGhC;GACJ,MAAM;GACN;GACA;GACA,aAAa,CAAC;EAChB,CACF;EAEA,MAAM,UAAU,MAAM,KAAK,wBAAwB,UAAU,KAAK,OAAO;EAEzE,IAAI,UAAU,QAAQ,QACpB,MAAM,IAAI,uBAAuB,QAAQ,QAAQ,MAAM;EAGzD,MAAM,SAAS,QAAQ,OAAO;EAC9B,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,mCAAmC;EAGrD,IAAI,WAAW,QAAQ;GACrB,MAAM,UACJ,OAAO,WAAW;GACpB,MAAM,IAAI,MACR,mCAAmC,KAAK,IAAI,OAAO,MAAM,KAAK,QAAQ,GAAG,KAAK,UAAU,OAAO,OAAO,GACxG;EACF;EAEA,OAAO,OAAO;CAChB;AACF;AAEA,SAAS,aAA4B;CACnC,OAAO;EACL,gCAAgC,IAAI,QAAQ;EAC5C,uBAAuB,IAAI,QAAQ;CACrC;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/main.ts"],"names":[],"mappings":"AAeA,OAAO,EAOL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAwB/B,wBAA8B,SAAS,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CAyLf"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/main.ts"],"names":[],"mappings":"AAgBA,OAAO,EAOL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAwB/B,wBAA8B,SAAS,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CAwMf"}
@@ -12,6 +12,7 @@ import { createNotifierFrom, handleSubscriptionsFrom, subscribeTo } from "../wor
12
12
  import { WorkerDispatcher } from "./worker-dispatcher.js";
13
13
  import { resolver } from "@rocicorp/resolver";
14
14
  import { LogContext, consoleLogSink } from "@rocicorp/logger";
15
+ import { randomBytes } from "node:crypto";
15
16
  import path from "node:path";
16
17
  //#region ../zero-cache/src/server/main.ts
17
18
  var lc = new LogContext("info", {}, consoleLogSink);
@@ -35,6 +36,10 @@ async function runWorker(parent, env) {
35
36
  "--cvr-max-conns-per-worker",
36
37
  String(Math.floor(config.cvr.maxConns / numSyncers))
37
38
  ];
39
+ if (runsChangeStreamer(config) && numSyncers > 0 && config.changeStreamer.secret === void 0) env = {
40
+ ...env,
41
+ ZERO_CHANGE_STREAMER_SECRET: randomBytes(32).toString("base64url")
42
+ };
38
43
  const allSubWorkers = [];
39
44
  function loadWorker(moduleUrl, type, id, ...args) {
40
45
  const worker = childWorker(moduleUrl, env, ...args, ...internalFlags);
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","names":[],"sources":["../../../../../zero-cache/src/server/main.ts"],"sourcesContent":["import path from 'node:path';\nimport {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {must} from '../../../shared/src/must.ts';\nimport {runsChangeStreamer} from '../config/normalize.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {registerSQLiteCorruptionDiagnosticTarget} from '../db/sqlite-corruption.ts';\nimport {initEventSink} from '../observability/events.ts';\nimport {\n exitAfter,\n ProcessManager,\n recordStartupDurationMs,\n runUntilKilled,\n type WorkerType,\n} from '../services/life-cycle.ts';\nimport {\n childWorker,\n parentWorker,\n singleProcessMode,\n type DrainRequestedMessage,\n type ProfileMessage,\n type ProfileResponseMessage,\n type Worker,\n} from '../types/processes.ts';\nimport {\n createNotifierFrom,\n handleSubscriptionsFrom,\n subscribeTo,\n type ReplicaFileMode,\n} from '../workers/replicator.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\nimport {WorkerDispatcher} from './worker-dispatcher.ts';\nimport {\n CHANGE_STREAMER_URL,\n MUTATOR_URL,\n REAPER_URL,\n REPLICATOR_URL,\n SHADOW_SYNCER_URL,\n SYNCER_URL,\n} from './worker-urls.ts';\n\nconst clientConnectionBifurcated = false;\n\n// Default LogContext, overridden in runWorker\nlet lc = new LogContext('info', {}, consoleLogSink);\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n): Promise<void> {\n const startMs = Date.now();\n const config = getNormalizedZeroConfig({env});\n\n startOtelAuto(\n createLogContext(config, 'dispatcher', 0, false),\n 'dispatcher',\n 0,\n );\n lc = createLogContext(config, 'dispatcher');\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: 'dispatcher replica',\n dbPath: config.replica.file,\n },\n config.sqliteCorruptionChecks,\n );\n initEventSink(lc, config);\n\n const processes = new ProcessManager(lc, parent);\n\n const {numSyncWorkers: numSyncers} = config;\n if (config.enableCrudMutations && config.upstream.maxConns < numSyncers) {\n throw new Error(\n `Insufficient upstream connections (${config.upstream.maxConns}) for ${numSyncers} syncers.` +\n `Increase ZERO_UPSTREAM_MAX_CONNS or decrease ZERO_NUM_SYNC_WORKERS (which defaults to available cores).`,\n );\n }\n if (config.cvr.maxConns < numSyncers) {\n throw new Error(\n `Insufficient cvr connections (${config.cvr.maxConns}) for ${numSyncers} syncers.` +\n `Increase ZERO_CVR_MAX_CONNS or decrease ZERO_NUM_SYNC_WORKERS (which defaults to available cores).`,\n );\n }\n\n const internalFlags: string[] =\n numSyncers === 0\n ? []\n : [\n '--upstream-max-conns-per-worker',\n String(Math.floor(config.upstream.maxConns / numSyncers)),\n '--cvr-max-conns-per-worker',\n String(Math.floor(config.cvr.maxConns / numSyncers)),\n ];\n\n const allSubWorkers: Worker[] = [];\n function loadWorker(\n moduleUrl: URL,\n type: WorkerType,\n id?: string | number,\n ...args: string[]\n ): Worker {\n const worker = childWorker(moduleUrl, env, ...args, ...internalFlags);\n const name = path.basename(moduleUrl.pathname) + (id ? ` (${id})` : '');\n const w = processes.addWorker(worker, type, name);\n allSubWorkers.push(w);\n w.onMessageType<ProfileResponseMessage>('profileResponse', res =>\n parent.send(['profileResponse', res]),\n );\n return w;\n }\n\n const {taskID, litestream} = config;\n const runChangeStreamer = runsChangeStreamer(config);\n const sqliteChangeLogEnabled =\n config.changeStreamer.sqliteChangeLogMode !== 'off';\n if (sqliteChangeLogEnabled && !runChangeStreamer) {\n // A multi-node deployment configures every task from one environment, so\n // the change log's options reach the view-syncers too. The log lives in\n // the change-streamer, and a task that connects to one simply never writes\n // a log. Warning rather than refusing to start is what makes the\n // fleet-wide environment -- often the only knob an operator has -- able to\n // express the rollout at all.\n lc.warn?.(\n `ignoring --change-streamer-sqlite-change-log-mode=` +\n `${config.changeStreamer.sqliteChangeLogMode}: this task connects to ` +\n `a change-streamer rather than running one, and the SQLite change log ` +\n `lives in the change-streamer`,\n );\n }\n\n let changeStreamer: Worker | undefined;\n\n if (runChangeStreamer) {\n const {promise: changeStreamerReady, resolve: changeStreamerStarted} =\n resolver();\n // In a dedicated replication-manager (i.e. no syncers), the\n // change-streamer is the user-facing worker: it receives the SIGTERM\n // first so that it can drain subscribers before other supporting\n // workers are stopped. When syncers run in the same task, the\n // change-streamer supports them and thus must outlive them.\n changeStreamer = loadWorker(\n CHANGE_STREAMER_URL,\n numSyncers > 0 ? 'supporting' : 'user-facing',\n ).once('message', changeStreamerStarted);\n // Wait for the change-streamer to be ready to guarantee that a replica\n // file is present.\n await changeStreamerReady;\n }\n\n if (numSyncers > 0) {\n const {promise: reaperReady, resolve: reaperStarted} = resolver();\n loadWorker(REAPER_URL, 'supporting').once('message', reaperStarted);\n // Before starting the view-syncers, ensure that the reaper has started\n // up, indicating that any CVR db migrations have been performed.\n await reaperReady;\n }\n\n // Only run the shadow-sync canary on the replication-manager (or in\n // single-node mode, where it also owns upstream). Running on every\n // view-syncer would hammer the upstream with N redundant canaries.\n if (config.shadowSync.enabled && runChangeStreamer) {\n const {promise: shadowReady, resolve: shadowStarted} = resolver();\n loadWorker(SHADOW_SYNCER_URL, 'supporting').once('message', shadowStarted);\n await shadowReady;\n }\n\n const syncers: Worker[] = [];\n if (numSyncers) {\n const mode: ReplicaFileMode =\n runChangeStreamer && litestream.backupURL ? 'serving-copy' : 'serving';\n const {promise: replicaReady, resolve} = resolver();\n const replicator = loadWorker(\n REPLICATOR_URL,\n 'supporting',\n mode,\n mode,\n ).once('message', () => {\n subscribeTo(lc, replicator);\n resolve();\n });\n // Relay drain requests to the runner, which coordinates the SelfDrain.\n replicator.onMessageType<DrainRequestedMessage>('drainRequested', msg => {\n lc.info?.(`drain requested by the replicator`);\n parent.send<DrainRequestedMessage>(['drainRequested', msg]);\n });\n await replicaReady;\n\n const notifier = createNotifierFrom(lc, replicator);\n for (let i = 0; i < numSyncers; i++) {\n syncers.push(loadWorker(SYNCER_URL, 'user-facing', i, mode, String(i)));\n }\n syncers.forEach(syncer => handleSubscriptionsFrom(lc, syncer, notifier));\n }\n let mutator: Worker | undefined;\n if (clientConnectionBifurcated) {\n mutator = loadWorker(MUTATOR_URL, 'supporting', 'mutator');\n }\n\n lc.info?.('waiting for workers to be ready ...');\n const logWaiting = setInterval(\n () => lc.info?.(`still waiting for ${processes.initializing().join(', ')}`),\n 10_000,\n );\n await processes.allWorkersReady();\n clearInterval(logWaiting);\n const startupDurationMs = Date.now() - startMs;\n lc.info?.(`all workers ready (${startupDurationMs} ms)`);\n recordStartupDurationMs(startupDurationMs);\n\n parent.send(['ready', {ready: true}]);\n parent.onMessageType<ProfileMessage>('profile', req => {\n for (const w of allSubWorkers) {\n w.send(['profile', req]);\n }\n });\n\n try {\n await runUntilKilled(\n lc,\n parent,\n new WorkerDispatcher(\n lc,\n taskID,\n parent,\n syncers,\n mutator,\n changeStreamer,\n ),\n );\n } catch (err) {\n processes.logErrorAndExit(err, 'dispatcher');\n } finally {\n await processes.shutdown();\n }\n}\n\nif (!singleProcessMode()) {\n void exitAfter(\n () => lc,\n () => runWorker(must(parentWorker), process.env),\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"main.js","names":[],"sources":["../../../../../zero-cache/src/server/main.ts"],"sourcesContent":["import {randomBytes} from 'node:crypto';\nimport path from 'node:path';\nimport {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {must} from '../../../shared/src/must.ts';\nimport {runsChangeStreamer} from '../config/normalize.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {registerSQLiteCorruptionDiagnosticTarget} from '../db/sqlite-corruption.ts';\nimport {initEventSink} from '../observability/events.ts';\nimport {\n exitAfter,\n ProcessManager,\n recordStartupDurationMs,\n runUntilKilled,\n type WorkerType,\n} from '../services/life-cycle.ts';\nimport {\n childWorker,\n parentWorker,\n singleProcessMode,\n type DrainRequestedMessage,\n type ProfileMessage,\n type ProfileResponseMessage,\n type Worker,\n} from '../types/processes.ts';\nimport {\n createNotifierFrom,\n handleSubscriptionsFrom,\n subscribeTo,\n type ReplicaFileMode,\n} from '../workers/replicator.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\nimport {WorkerDispatcher} from './worker-dispatcher.ts';\nimport {\n CHANGE_STREAMER_URL,\n MUTATOR_URL,\n REAPER_URL,\n REPLICATOR_URL,\n SHADOW_SYNCER_URL,\n SYNCER_URL,\n} from './worker-urls.ts';\n\nconst clientConnectionBifurcated = false;\n\n// Default LogContext, overridden in runWorker\nlet lc = new LogContext('info', {}, consoleLogSink);\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n): Promise<void> {\n const startMs = Date.now();\n const config = getNormalizedZeroConfig({env});\n\n startOtelAuto(\n createLogContext(config, 'dispatcher', 0, false),\n 'dispatcher',\n 0,\n );\n lc = createLogContext(config, 'dispatcher');\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: 'dispatcher replica',\n dbPath: config.replica.file,\n },\n config.sqliteCorruptionChecks,\n );\n initEventSink(lc, config);\n\n const processes = new ProcessManager(lc, parent);\n\n const {numSyncWorkers: numSyncers} = config;\n if (config.enableCrudMutations && config.upstream.maxConns < numSyncers) {\n throw new Error(\n `Insufficient upstream connections (${config.upstream.maxConns}) for ${numSyncers} syncers.` +\n `Increase ZERO_UPSTREAM_MAX_CONNS or decrease ZERO_NUM_SYNC_WORKERS (which defaults to available cores).`,\n );\n }\n if (config.cvr.maxConns < numSyncers) {\n throw new Error(\n `Insufficient cvr connections (${config.cvr.maxConns}) for ${numSyncers} syncers.` +\n `Increase ZERO_CVR_MAX_CONNS or decrease ZERO_NUM_SYNC_WORKERS (which defaults to available cores).`,\n );\n }\n\n const internalFlags: string[] =\n numSyncers === 0\n ? []\n : [\n '--upstream-max-conns-per-worker',\n String(Math.floor(config.upstream.maxConns / numSyncers)),\n '--cvr-max-conns-per-worker',\n String(Math.floor(config.cvr.maxConns / numSyncers)),\n ];\n\n // In a single-node configuration the change-streamer only serves the\n // workers of this process tree, so a secret is generated for them when none\n // is configured. A dedicated replication-manager (no sync workers) serves\n // remote view-syncers, which must be configured with the same secret.\n if (\n runsChangeStreamer(config) &&\n numSyncers > 0 &&\n config.changeStreamer.secret === undefined\n ) {\n env = {\n ...env,\n ZERO_CHANGE_STREAMER_SECRET: randomBytes(32).toString('base64url'),\n };\n }\n\n const allSubWorkers: Worker[] = [];\n function loadWorker(\n moduleUrl: URL,\n type: WorkerType,\n id?: string | number,\n ...args: string[]\n ): Worker {\n const worker = childWorker(moduleUrl, env, ...args, ...internalFlags);\n const name = path.basename(moduleUrl.pathname) + (id ? ` (${id})` : '');\n const w = processes.addWorker(worker, type, name);\n allSubWorkers.push(w);\n w.onMessageType<ProfileResponseMessage>('profileResponse', res =>\n parent.send(['profileResponse', res]),\n );\n return w;\n }\n\n const {taskID, litestream} = config;\n const runChangeStreamer = runsChangeStreamer(config);\n const sqliteChangeLogEnabled =\n config.changeStreamer.sqliteChangeLogMode !== 'off';\n if (sqliteChangeLogEnabled && !runChangeStreamer) {\n // A multi-node deployment configures every task from one environment, so\n // the change log's options reach the view-syncers too. The log lives in\n // the change-streamer, and a task that connects to one simply never writes\n // a log. Warning rather than refusing to start is what makes the\n // fleet-wide environment -- often the only knob an operator has -- able to\n // express the rollout at all.\n lc.warn?.(\n `ignoring --change-streamer-sqlite-change-log-mode=` +\n `${config.changeStreamer.sqliteChangeLogMode}: this task connects to ` +\n `a change-streamer rather than running one, and the SQLite change log ` +\n `lives in the change-streamer`,\n );\n }\n\n let changeStreamer: Worker | undefined;\n\n if (runChangeStreamer) {\n const {promise: changeStreamerReady, resolve: changeStreamerStarted} =\n resolver();\n // In a dedicated replication-manager (i.e. no syncers), the\n // change-streamer is the user-facing worker: it receives the SIGTERM\n // first so that it can drain subscribers before other supporting\n // workers are stopped. When syncers run in the same task, the\n // change-streamer supports them and thus must outlive them.\n changeStreamer = loadWorker(\n CHANGE_STREAMER_URL,\n numSyncers > 0 ? 'supporting' : 'user-facing',\n ).once('message', changeStreamerStarted);\n // Wait for the change-streamer to be ready to guarantee that a replica\n // file is present.\n await changeStreamerReady;\n }\n\n if (numSyncers > 0) {\n const {promise: reaperReady, resolve: reaperStarted} = resolver();\n loadWorker(REAPER_URL, 'supporting').once('message', reaperStarted);\n // Before starting the view-syncers, ensure that the reaper has started\n // up, indicating that any CVR db migrations have been performed.\n await reaperReady;\n }\n\n // Only run the shadow-sync canary on the replication-manager (or in\n // single-node mode, where it also owns upstream). Running on every\n // view-syncer would hammer the upstream with N redundant canaries.\n if (config.shadowSync.enabled && runChangeStreamer) {\n const {promise: shadowReady, resolve: shadowStarted} = resolver();\n loadWorker(SHADOW_SYNCER_URL, 'supporting').once('message', shadowStarted);\n await shadowReady;\n }\n\n const syncers: Worker[] = [];\n if (numSyncers) {\n const mode: ReplicaFileMode =\n runChangeStreamer && litestream.backupURL ? 'serving-copy' : 'serving';\n const {promise: replicaReady, resolve} = resolver();\n const replicator = loadWorker(\n REPLICATOR_URL,\n 'supporting',\n mode,\n mode,\n ).once('message', () => {\n subscribeTo(lc, replicator);\n resolve();\n });\n // Relay drain requests to the runner, which coordinates the SelfDrain.\n replicator.onMessageType<DrainRequestedMessage>('drainRequested', msg => {\n lc.info?.(`drain requested by the replicator`);\n parent.send<DrainRequestedMessage>(['drainRequested', msg]);\n });\n await replicaReady;\n\n const notifier = createNotifierFrom(lc, replicator);\n for (let i = 0; i < numSyncers; i++) {\n syncers.push(loadWorker(SYNCER_URL, 'user-facing', i, mode, String(i)));\n }\n syncers.forEach(syncer => handleSubscriptionsFrom(lc, syncer, notifier));\n }\n let mutator: Worker | undefined;\n if (clientConnectionBifurcated) {\n mutator = loadWorker(MUTATOR_URL, 'supporting', 'mutator');\n }\n\n lc.info?.('waiting for workers to be ready ...');\n const logWaiting = setInterval(\n () => lc.info?.(`still waiting for ${processes.initializing().join(', ')}`),\n 10_000,\n );\n await processes.allWorkersReady();\n clearInterval(logWaiting);\n const startupDurationMs = Date.now() - startMs;\n lc.info?.(`all workers ready (${startupDurationMs} ms)`);\n recordStartupDurationMs(startupDurationMs);\n\n parent.send(['ready', {ready: true}]);\n parent.onMessageType<ProfileMessage>('profile', req => {\n for (const w of allSubWorkers) {\n w.send(['profile', req]);\n }\n });\n\n try {\n await runUntilKilled(\n lc,\n parent,\n new WorkerDispatcher(\n lc,\n taskID,\n parent,\n syncers,\n mutator,\n changeStreamer,\n ),\n );\n } catch (err) {\n processes.logErrorAndExit(err, 'dispatcher');\n } finally {\n await processes.shutdown();\n }\n}\n\nif (!singleProcessMode()) {\n void exitAfter(\n () => lc,\n () => runWorker(must(parentWorker), process.env),\n );\n}\n"],"mappings":"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"}