@rocicorp/zero 1.10.0-canary.23 → 1.10.0-canary.24

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 (175) hide show
  1. package/out/analyze-query/src/bin-analyze.js +8 -7
  2. package/out/analyze-query/src/bin-analyze.js.map +1 -1
  3. package/out/replicache/src/btree/node.d.ts.map +1 -1
  4. package/out/replicache/src/btree/node.js +3 -6
  5. package/out/replicache/src/btree/node.js.map +1 -1
  6. package/out/replicache/src/dag/gc.d.ts.map +1 -1
  7. package/out/replicache/src/dag/gc.js +6 -10
  8. package/out/replicache/src/dag/gc.js.map +1 -1
  9. package/out/replicache/src/kv/sqlite-store.d.ts.map +1 -1
  10. package/out/replicache/src/kv/sqlite-store.js +24 -2
  11. package/out/replicache/src/kv/sqlite-store.js.map +1 -1
  12. package/out/replicache/src/persist/persist.js +1 -1
  13. package/out/shared/src/arrays.d.ts +5 -0
  14. package/out/shared/src/arrays.d.ts.map +1 -1
  15. package/out/shared/src/arrays.js +8 -10
  16. package/out/shared/src/arrays.js.map +1 -1
  17. package/out/shared/src/map.d.ts +19 -2
  18. package/out/shared/src/map.d.ts.map +1 -1
  19. package/out/shared/src/map.js +18 -4
  20. package/out/shared/src/map.js.map +1 -1
  21. package/out/z2s/src/compiler.js +1 -1
  22. package/out/z2s/src/sql.d.ts.map +1 -1
  23. package/out/z2s/src/sql.js +2 -2
  24. package/out/z2s/src/sql.js.map +1 -1
  25. package/out/zero/package.js +1 -1
  26. package/out/zero/package.js.map +1 -1
  27. package/out/zero-cache/src/auth/write-authorizer.d.ts.map +1 -1
  28. package/out/zero-cache/src/auth/write-authorizer.js +1 -0
  29. package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
  30. package/out/zero-cache/src/config/zero-config.d.ts +15 -0
  31. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  32. package/out/zero-cache/src/config/zero-config.js +35 -0
  33. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  34. package/out/zero-cache/src/db/lite-tables.d.ts.map +1 -1
  35. package/out/zero-cache/src/db/lite-tables.js +3 -4
  36. package/out/zero-cache/src/db/lite-tables.js.map +1 -1
  37. package/out/zero-cache/src/observability/metrics.d.ts.map +1 -1
  38. package/out/zero-cache/src/observability/metrics.js +2 -7
  39. package/out/zero-cache/src/observability/metrics.js.map +1 -1
  40. package/out/zero-cache/src/server/inspector-delegate.d.ts.map +1 -1
  41. package/out/zero-cache/src/server/inspector-delegate.js +2 -8
  42. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  43. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  44. package/out/zero-cache/src/server/syncer.js +3 -1
  45. package/out/zero-cache/src/server/syncer.js.map +1 -1
  46. package/out/zero-cache/src/services/analyze.d.ts.map +1 -1
  47. package/out/zero-cache/src/services/analyze.js +8 -7
  48. package/out/zero-cache/src/services/analyze.js.map +1 -1
  49. package/out/zero-cache/src/services/mutagen/pusher.d.ts +32 -32
  50. package/out/zero-cache/src/services/mutagen/pusher.d.ts.map +1 -1
  51. package/out/zero-cache/src/services/mutagen/pusher.js +3 -5
  52. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  53. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  54. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +15 -9
  55. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  56. package/out/zero-cache/src/services/view-syncer/query-covering.d.ts.map +1 -1
  57. package/out/zero-cache/src/services/view-syncer/query-covering.js +2 -6
  58. package/out/zero-cache/src/services/view-syncer/query-covering.js.map +1 -1
  59. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.d.ts +95 -0
  60. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.d.ts.map +1 -0
  61. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js +155 -0
  62. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js.map +1 -0
  63. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +38 -3
  64. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
  65. package/out/zero-cache/src/services/view-syncer/snapshotter.js +110 -27
  66. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  67. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  68. package/out/zero-cache/src/services/view-syncer/view-syncer.js +4 -13
  69. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  70. package/out/zero-cache/src/types/row-key.d.ts.map +1 -1
  71. package/out/zero-cache/src/types/row-key.js +5 -5
  72. package/out/zero-cache/src/types/row-key.js.map +1 -1
  73. package/out/zero-client/src/client/query-manager.d.ts.map +1 -1
  74. package/out/zero-client/src/client/query-manager.js +2 -5
  75. package/out/zero-client/src/client/query-manager.js.map +1 -1
  76. package/out/zero-client/src/client/version.js +1 -1
  77. package/out/zero-protocol/src/analyze-query-result.d.ts +5 -5
  78. package/out/zero-protocol/src/analyze-query-result.d.ts.map +1 -1
  79. package/out/zero-protocol/src/analyze-query-result.js +1 -1
  80. package/out/zero-protocol/src/analyze-query-result.js.map +1 -1
  81. package/out/zero-protocol/src/ast.d.ts.map +1 -1
  82. package/out/zero-protocol/src/ast.js +13 -14
  83. package/out/zero-protocol/src/ast.js.map +1 -1
  84. package/out/zero-protocol/src/down.d.ts +2 -2
  85. package/out/zero-protocol/src/inspect-down.d.ts +6 -6
  86. package/out/zero-protocol/src/protocol-version.d.ts.map +1 -1
  87. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  88. package/out/zero-protocol/src/query-hash.d.ts.map +1 -1
  89. package/out/zero-protocol/src/query-hash.js +2 -5
  90. package/out/zero-protocol/src/query-hash.js.map +1 -1
  91. package/out/zql/src/builder/builder.d.ts +19 -0
  92. package/out/zql/src/builder/builder.d.ts.map +1 -1
  93. package/out/zql/src/builder/builder.js +28 -11
  94. package/out/zql/src/builder/builder.js.map +1 -1
  95. package/out/zql/src/builder/correlated-predicate-pushdown.d.ts +43 -0
  96. package/out/zql/src/builder/correlated-predicate-pushdown.d.ts.map +1 -0
  97. package/out/zql/src/builder/correlated-predicate-pushdown.js +161 -0
  98. package/out/zql/src/builder/correlated-predicate-pushdown.js.map +1 -0
  99. package/out/zql/src/ivm/connection-index.d.ts +68 -0
  100. package/out/zql/src/ivm/connection-index.d.ts.map +1 -0
  101. package/out/zql/src/ivm/connection-index.js +149 -0
  102. package/out/zql/src/ivm/connection-index.js.map +1 -0
  103. package/out/zql/src/ivm/exists.js +1 -1
  104. package/out/zql/src/ivm/filter-operators.d.ts +3 -2
  105. package/out/zql/src/ivm/filter-operators.d.ts.map +1 -1
  106. package/out/zql/src/ivm/filter-operators.js +19 -4
  107. package/out/zql/src/ivm/filter-operators.js.map +1 -1
  108. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  109. package/out/zql/src/ivm/memory-source.js +17 -4
  110. package/out/zql/src/ivm/memory-source.js.map +1 -1
  111. package/out/zql/src/ivm/operator.d.ts +40 -0
  112. package/out/zql/src/ivm/operator.d.ts.map +1 -1
  113. package/out/zql/src/ivm/operator.js.map +1 -1
  114. package/out/zql/src/planner/planner-builder.d.ts +12 -3
  115. package/out/zql/src/planner/planner-builder.d.ts.map +1 -1
  116. package/out/zql/src/planner/planner-builder.js +40 -20
  117. package/out/zql/src/planner/planner-builder.js.map +1 -1
  118. package/out/zql/src/planner/planner-connection.d.ts +28 -2
  119. package/out/zql/src/planner/planner-connection.d.ts.map +1 -1
  120. package/out/zql/src/planner/planner-connection.js +108 -8
  121. package/out/zql/src/planner/planner-connection.js.map +1 -1
  122. package/out/zql/src/planner/planner-debug.d.ts +1 -1
  123. package/out/zql/src/planner/planner-debug.d.ts.map +1 -1
  124. package/out/zql/src/planner/planner-debug.js +3 -8
  125. package/out/zql/src/planner/planner-debug.js.map +1 -1
  126. package/out/zql/src/planner/planner-fan-out.d.ts +1 -0
  127. package/out/zql/src/planner/planner-fan-out.d.ts.map +1 -1
  128. package/out/zql/src/planner/planner-fan-out.js +3 -0
  129. package/out/zql/src/planner/planner-fan-out.js.map +1 -1
  130. package/out/zql/src/planner/planner-filter.d.ts +59 -0
  131. package/out/zql/src/planner/planner-filter.d.ts.map +1 -0
  132. package/out/zql/src/planner/planner-filter.js +116 -0
  133. package/out/zql/src/planner/planner-filter.js.map +1 -0
  134. package/out/zql/src/planner/planner-graph.d.ts +2 -0
  135. package/out/zql/src/planner/planner-graph.d.ts.map +1 -1
  136. package/out/zql/src/planner/planner-graph.js +2 -0
  137. package/out/zql/src/planner/planner-graph.js.map +1 -1
  138. package/out/zql/src/planner/planner-join.d.ts +2 -0
  139. package/out/zql/src/planner/planner-join.d.ts.map +1 -1
  140. package/out/zql/src/planner/planner-join.js +7 -0
  141. package/out/zql/src/planner/planner-join.js.map +1 -1
  142. package/out/zql/src/planner/planner-node.d.ts +2 -1
  143. package/out/zql/src/planner/planner-node.d.ts.map +1 -1
  144. package/out/zql/src/planner/planner-node.js.map +1 -1
  145. package/out/zql/src/planner/planner-source.d.ts +2 -2
  146. package/out/zql/src/planner/planner-source.d.ts.map +1 -1
  147. package/out/zql/src/planner/planner-source.js +2 -2
  148. package/out/zql/src/planner/planner-source.js.map +1 -1
  149. package/out/zql/src/query/query-impl.d.ts.map +1 -1
  150. package/out/zql/src/query/query-impl.js +6 -23
  151. package/out/zql/src/query/query-impl.js.map +1 -1
  152. package/out/zql/src/query/query-transitions.d.ts.map +1 -1
  153. package/out/zql/src/query/query-transitions.js +2 -5
  154. package/out/zql/src/query/query-transitions.js.map +1 -1
  155. package/out/zql/src/query/runnable-query-impl.d.ts.map +1 -1
  156. package/out/zql/src/query/runnable-query-impl.js +2 -10
  157. package/out/zql/src/query/runnable-query-impl.js.map +1 -1
  158. package/out/zqlite/src/query-builder.d.ts +1 -1
  159. package/out/zqlite/src/query-builder.d.ts.map +1 -1
  160. package/out/zqlite/src/query-builder.js +2 -1
  161. package/out/zqlite/src/query-builder.js.map +1 -1
  162. package/out/zqlite/src/query-delegate.d.ts.map +1 -1
  163. package/out/zqlite/src/query-delegate.js +5 -6
  164. package/out/zqlite/src/query-delegate.js.map +1 -1
  165. package/out/zqlite/src/sqlite-cost-model.d.ts.map +1 -1
  166. package/out/zqlite/src/sqlite-cost-model.js +2 -30
  167. package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
  168. package/out/zqlite/src/sqlite-stat-fanout.d.ts.map +1 -1
  169. package/out/zqlite/src/sqlite-stat-fanout.js +3 -5
  170. package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
  171. package/out/zqlite/src/table-source.d.ts +14 -2
  172. package/out/zqlite/src/table-source.d.ts.map +1 -1
  173. package/out/zqlite/src/table-source.js +25 -12
  174. package/out/zqlite/src/table-source.js.map +1 -1
  175. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
1
  import { parse } from "../../../shared/src/valita.js";
2
+ import { getOrInsertComputed } from "../../../shared/src/map.js";
3
+ import { newArray } from "../../../shared/src/arrays.js";
2
4
  import { must } from "../../../shared/src/must.js";
3
5
  import { primaryKeySchema } from "../../../zero-protocol/src/primary-key.js";
4
6
  import { difference } from "../../../shared/src/set-utils.js";
@@ -113,10 +115,7 @@ function computeZqlSpecsFromLiteSpecs(tables, indexes, { includeBackfillingColum
113
115
  tableSpecs.clear();
114
116
  fullTables?.clear();
115
117
  const uniqueIndexColumns = /* @__PURE__ */ new Map();
116
- for (const { tableName, columns } of indexes.filter((idx) => idx.unique && !isPartialIndex(idx))) {
117
- if (!uniqueIndexColumns.has(tableName)) uniqueIndexColumns.set(tableName, []);
118
- uniqueIndexColumns.get(tableName)?.push(Object.keys(columns));
119
- }
118
+ for (const { tableName, columns } of indexes.filter((idx) => idx.unique && !isPartialIndex(idx))) getOrInsertComputed(uniqueIndexColumns, tableName, newArray).push(Object.keys(columns));
120
119
  tables.forEach((fullTable) => {
121
120
  fullTables?.set(fullTable.name, fullTable);
122
121
  const backfilling = new Set(fullTable.backfilling);
@@ -1 +1 @@
1
- {"version":3,"file":"lite-tables.js","names":[],"sources":["../../../../../zero-cache/src/db/lite-tables.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {must} from '../../../shared/src/must.ts';\nimport {difference} from '../../../shared/src/set-utils.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport {primaryKeySchema} from '../../../zero-protocol/src/primary-key.ts';\nimport type {Database} from '../../../zqlite/src/db.ts';\nimport {\n ColumnMetadataStore,\n metadataToLiteTypeString,\n} from '../services/replicator/schema/column-metadata.ts';\nimport {TableMetadataTracker} from '../services/replicator/schema/table-metadata.ts';\nimport {\n isArray,\n isEnum,\n liteTypeToZqlValueType,\n mapLiteDataTypeToZqlSchemaValue,\n nullableUpstream,\n} from '../types/lite.ts';\nimport * as PostgresTypeClass from './postgres-type-class-enum.ts';\nimport type {\n LiteAndZqlSpec,\n LiteIndexSpec,\n LiteTableSpec,\n MutableLiteIndexSpec,\n MutableLiteTableSpec,\n} from './specs.ts';\n\ntype ColumnInfo = {\n table: string;\n name: string;\n type: string;\n notNull: number;\n dflt: string | null;\n keyPos: number;\n};\n\nexport type LiteTableSpecWithReplicationStatus = LiteTableSpec & {\n readonly backfilling?: string[];\n readonly minRowVersion?: string | null;\n};\n\ntype MutableLiteTableSpecWithReplicationStatus = MutableLiteTableSpec & {\n backfilling: string[];\n minRowVersion: string | null;\n};\n\nexport function listTables(\n db: Database,\n useColumnMetadata = true,\n useTableMetadata = true,\n): LiteTableSpecWithReplicationStatus[] {\n const columns = db\n .prepare(\n `\n SELECT\n m.name as \"table\",\n p.name as name,\n p.type as type,\n p.\"notnull\" as \"notNull\",\n p.dflt_value as \"dflt\",\n p.pk as keyPos\n FROM sqlite_master as m\n LEFT JOIN pragma_table_info(m.name) as p\n WHERE m.type = 'table'\n AND m.name NOT LIKE 'sqlite_%'\n AND m.name NOT LIKE '_zero.%'\n AND m.name NOT LIKE '_litestream_%'\n `,\n )\n .all() as ColumnInfo[];\n\n const minRowVersions = useTableMetadata\n ? new TableMetadataTracker(db).getMinRowVersions()\n : new Map();\n const tables: LiteTableSpecWithReplicationStatus[] = [];\n let table: MutableLiteTableSpecWithReplicationStatus | undefined;\n\n columns.forEach(col => {\n if (col.table !== table?.name) {\n // New table\n table = {\n name: col.table,\n columns: {},\n backfilling: [],\n minRowVersion: minRowVersions.get(col.table) ?? null,\n };\n tables.push(table);\n }\n\n // Try to read from metadata table first, fall back to SQLite column type\n let dataType: string;\n let elemPgTypeClass:\n | typeof PostgresTypeClass.Base\n | typeof PostgresTypeClass.Enum\n | null;\n\n const metadata = useColumnMetadata\n ? ColumnMetadataStore.getInstance(db)?.getColumn(col.table, col.name)\n : undefined;\n if (metadata) {\n // Read from metadata table and convert to pipe notation\n dataType = metadataToLiteTypeString(metadata);\n elemPgTypeClass = metadata.isArray\n ? metadata.isEnum\n ? PostgresTypeClass.Enum\n : PostgresTypeClass.Base\n : null;\n } else {\n // Fall back to reading from SQLite column type (pipe notation)\n dataType = col.type;\n elemPgTypeClass = isArray(col.type)\n ? isEnum(col.type)\n ? PostgresTypeClass.Enum\n : PostgresTypeClass.Base\n : null;\n }\n\n table.columns[col.name] = {\n pos: Object.keys(table.columns).length + 1,\n dataType,\n characterMaximumLength: null,\n notNull: col.notNull !== 0,\n dflt: col.dflt,\n elemPgTypeClass,\n };\n if (metadata?.isBackfilling) {\n table.backfilling.push(col.name);\n }\n if (col.keyPos) {\n table.primaryKey ??= [];\n while (table.primaryKey.length < col.keyPos) {\n table.primaryKey.push('');\n }\n table.primaryKey[col.keyPos - 1] = col.name;\n }\n });\n\n return tables;\n}\n\n/**\n * An index as it exists on the replica. SQLite retains only the DDL text of a\n * partial index, not a structured predicate, so replica indexes report a\n * `partial` flag in place of the {@link LiteIndexSpec.predicate}.\n */\nexport type ReplicaIndexSpec = Omit<LiteIndexSpec, 'predicate'> & {\n readonly partial?: boolean | undefined;\n};\n\ntype MutableReplicaIndexSpec = Omit<MutableLiteIndexSpec, 'predicate'> & {\n partial?: boolean | undefined;\n};\n\nexport function listIndexes(db: Database): ReplicaIndexSpec[] {\n const indexes = db\n .prepare(\n `SELECT \n idx.name as indexName, \n idx.tbl_name as tableName, \n info.\"unique\" as \"unique\",\n info.partial as partial,\n col.name as column,\n CASE WHEN col.desc = 0 THEN 'ASC' ELSE 'DESC' END as dir\n FROM sqlite_master as idx\n JOIN pragma_index_list(idx.tbl_name) AS info ON info.name = idx.name\n JOIN pragma_index_xinfo(idx.name) as col\n WHERE idx.type = 'index' AND \n col.key = 1 AND\n idx.tbl_name NOT LIKE '_zero.%'\n ORDER BY idx.name, col.seqno ASC`,\n )\n .all() as {\n indexName: string;\n tableName: string;\n unique: number;\n partial: number;\n column: string;\n dir: 'ASC' | 'DESC';\n }[];\n\n const ret: MutableReplicaIndexSpec[] = [];\n for (const {\n indexName: name,\n tableName,\n unique,\n partial,\n column,\n dir,\n } of indexes) {\n if (ret.at(-1)?.name === name) {\n // Aggregate multiple column names into the array.\n must(ret.at(-1)).columns[column] = dir;\n } else {\n ret.push({\n tableName,\n name,\n columns: {[column]: dir},\n unique: unique !== 0,\n ...(partial === 0 ? {} : {partial: true}),\n });\n }\n }\n\n return ret;\n}\n\nexport type ZqlSpecOptions = {\n /**\n * Controls whether to include backfilling columns in the computed\n * LiteAndZqlSpec. In general, backfilling columns should be include\n * in \"replication\" logic (copying data from upstream to the replica),\n * and excluded from \"sync\" logic (sending data from the replica to\n * clients).\n */\n includeBackfillingColumns: boolean;\n};\n\n/**\n * Computes a TableSpec \"view\" of the replicated data that is\n * suitable for processing / consumption for the client. This\n * includes:\n * * excluding tables without a PRIMARY KEY or UNIQUE INDEX\n * * excluding columns with types that are not supported by ZQL\n * * choosing columns to use as the primary key amongst those\n * in unique indexes\n *\n * @param tableSpecs an optional map to reset and populate\n * @param fullTables an optional map to receive the full table specs,\n * which may include tables and columns that are not synced to\n * the client because they lack a primary key or are of unsupported\n * data types.\n */\nexport function computeZqlSpecs(\n lc: LogContext,\n replica: Database,\n opts: ZqlSpecOptions,\n tableSpecs: Map<string, LiteAndZqlSpec> = new Map(),\n fullTables?: Map<string, LiteTableSpec>,\n): Map<string, LiteAndZqlSpec> {\n return computeZqlSpecsFromLiteSpecs(\n listTables(replica),\n listIndexes(replica),\n opts,\n tableSpecs,\n fullTables,\n lc,\n );\n}\n\nfunction isPartialIndex(idx: LiteIndexSpec | ReplicaIndexSpec): boolean {\n return (\n ('predicate' in idx && idx.predicate !== undefined) ||\n ('partial' in idx && idx.partial === true)\n );\n}\n\nexport function computeZqlSpecsFromLiteSpecs(\n tables: LiteTableSpecWithReplicationStatus[],\n indexes: readonly (LiteIndexSpec | ReplicaIndexSpec)[],\n {includeBackfillingColumns}: ZqlSpecOptions,\n tableSpecs: Map<string, LiteAndZqlSpec> = new Map(),\n fullTables?: Map<string, LiteTableSpec>,\n lc?: LogContext,\n): Map<string, LiteAndZqlSpec> {\n tableSpecs.clear();\n fullTables?.clear();\n\n const uniqueIndexColumns = new Map<string, string[][]>();\n // A partial unique index only constrains the rows matching its predicate,\n // so it cannot serve as a row key.\n for (const {tableName, columns} of indexes.filter(\n idx => idx.unique && !isPartialIndex(idx),\n )) {\n if (!uniqueIndexColumns.has(tableName)) {\n uniqueIndexColumns.set(tableName, []);\n }\n uniqueIndexColumns.get(tableName)?.push(Object.keys(columns));\n }\n\n tables.forEach(fullTable => {\n fullTables?.set(fullTable.name, fullTable);\n\n const backfilling = new Set(fullTable.backfilling);\n // Only include columns that:\n // - have a defined ZQL Value\n // - aren't backfilling if `includeBackfillingColumns` is false\n const visibleColumns = Object.entries(fullTable.columns).filter(\n ([col, {dataType}]) =>\n liteTypeToZqlValueType(dataType) &&\n (includeBackfillingColumns || !backfilling.has(col)),\n );\n const notNullColumns = new Set(\n visibleColumns\n .filter(\n ([col, {dataType}]) =>\n !nullableUpstream(dataType) || fullTable.primaryKey?.includes(col),\n )\n .map(([col]) => col),\n );\n\n const uniqueKeys = uniqueIndexColumns.get(fullTable.name) ?? [];\n // Examine all column combinations that can serve as a primary key,\n // i.e. excluding indexes over nullable or unsynced columns.\n const keys = uniqueKeys.filter(\n key => difference(new Set(key), notNullColumns).size === 0,\n );\n if (keys.length === 0) {\n // Only include tables with a row key.\n //\n // Note that this will automatically exclude tables that are being\n // backfilled (when includeBackfillingColumns is `false`) since candidate\n // keys only include visible columns.\n lc?.debug?.(\n `not syncing table ${fullTable.name} because it ` +\n (backfilling.size ? 'is being backfilled' : 'has no primary key'),\n );\n return;\n }\n // Pick the \"best\" (i.e. shortest) key for default IVM operations.\n const primaryKey = keys.sort(keyCmp)[0];\n\n const tableSpec = {\n ...fullTable,\n columns: Object.fromEntries(visibleColumns),\n primaryKey: v.parse(primaryKey, primaryKeySchema),\n uniqueKeys: uniqueKeys.map(key => v.parse(key, primaryKeySchema)),\n allPotentialPrimaryKeys: keys.map(key => v.parse(key, primaryKeySchema)),\n minRowVersion: fullTable.minRowVersion ?? null,\n };\n\n tableSpecs.set(tableSpec.name, {\n tableSpec,\n zqlSpec: Object.fromEntries(\n Object.entries(tableSpec.columns).map(([name, {dataType}]) => [\n name,\n mapLiteDataTypeToZqlSchemaValue(dataType),\n ]),\n ),\n });\n });\n return tableSpecs;\n}\n\nexport function mustGetTableSpec(\n tableSpecs: Map<string, LiteAndZqlSpec>,\n tableName: string,\n): LiteAndZqlSpec {\n const tableSpec = tableSpecs.get(tableName);\n if (!tableSpec) {\n throw new Error(\n `table '${tableName}' is not one of: ${[...tableSpecs.keys()]\n .filter(t => !t.includes('.') && !t.startsWith('_litestream_'))\n .sort()}. ` +\n `Check the spelling and ensure that the table has a primary key.`,\n );\n }\n return tableSpec;\n}\n\n// Deterministic comparator for favoring shorter row keys.\nfunction keyCmp(a: string[], b: string[]) {\n if (a.length !== b.length) {\n return a.length - b.length; // Fewer columns are better.\n }\n for (let i = 0; i < a.length; i++) {\n if (a[i] < b[i]) {\n return -1;\n }\n if (a[i] > b[i]) {\n return 1;\n }\n }\n return 0;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"lite-tables.js","names":[],"sources":["../../../../../zero-cache/src/db/lite-tables.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {newArray} from '../../../shared/src/arrays.ts';\nimport {getOrInsertComputed} from '../../../shared/src/map.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {difference} from '../../../shared/src/set-utils.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport {primaryKeySchema} from '../../../zero-protocol/src/primary-key.ts';\nimport type {Database} from '../../../zqlite/src/db.ts';\nimport {\n ColumnMetadataStore,\n metadataToLiteTypeString,\n} from '../services/replicator/schema/column-metadata.ts';\nimport {TableMetadataTracker} from '../services/replicator/schema/table-metadata.ts';\nimport {\n isArray,\n isEnum,\n liteTypeToZqlValueType,\n mapLiteDataTypeToZqlSchemaValue,\n nullableUpstream,\n} from '../types/lite.ts';\nimport * as PostgresTypeClass from './postgres-type-class-enum.ts';\nimport type {\n LiteAndZqlSpec,\n LiteIndexSpec,\n LiteTableSpec,\n MutableLiteIndexSpec,\n MutableLiteTableSpec,\n} from './specs.ts';\n\ntype ColumnInfo = {\n table: string;\n name: string;\n type: string;\n notNull: number;\n dflt: string | null;\n keyPos: number;\n};\n\nexport type LiteTableSpecWithReplicationStatus = LiteTableSpec & {\n readonly backfilling?: string[];\n readonly minRowVersion?: string | null;\n};\n\ntype MutableLiteTableSpecWithReplicationStatus = MutableLiteTableSpec & {\n backfilling: string[];\n minRowVersion: string | null;\n};\n\nexport function listTables(\n db: Database,\n useColumnMetadata = true,\n useTableMetadata = true,\n): LiteTableSpecWithReplicationStatus[] {\n const columns = db\n .prepare(\n `\n SELECT\n m.name as \"table\",\n p.name as name,\n p.type as type,\n p.\"notnull\" as \"notNull\",\n p.dflt_value as \"dflt\",\n p.pk as keyPos\n FROM sqlite_master as m\n LEFT JOIN pragma_table_info(m.name) as p\n WHERE m.type = 'table'\n AND m.name NOT LIKE 'sqlite_%'\n AND m.name NOT LIKE '_zero.%'\n AND m.name NOT LIKE '_litestream_%'\n `,\n )\n .all() as ColumnInfo[];\n\n const minRowVersions = useTableMetadata\n ? new TableMetadataTracker(db).getMinRowVersions()\n : new Map();\n const tables: LiteTableSpecWithReplicationStatus[] = [];\n let table: MutableLiteTableSpecWithReplicationStatus | undefined;\n\n columns.forEach(col => {\n if (col.table !== table?.name) {\n // New table\n table = {\n name: col.table,\n columns: {},\n backfilling: [],\n minRowVersion: minRowVersions.get(col.table) ?? null,\n };\n tables.push(table);\n }\n\n // Try to read from metadata table first, fall back to SQLite column type\n let dataType: string;\n let elemPgTypeClass:\n | typeof PostgresTypeClass.Base\n | typeof PostgresTypeClass.Enum\n | null;\n\n const metadata = useColumnMetadata\n ? ColumnMetadataStore.getInstance(db)?.getColumn(col.table, col.name)\n : undefined;\n if (metadata) {\n // Read from metadata table and convert to pipe notation\n dataType = metadataToLiteTypeString(metadata);\n elemPgTypeClass = metadata.isArray\n ? metadata.isEnum\n ? PostgresTypeClass.Enum\n : PostgresTypeClass.Base\n : null;\n } else {\n // Fall back to reading from SQLite column type (pipe notation)\n dataType = col.type;\n elemPgTypeClass = isArray(col.type)\n ? isEnum(col.type)\n ? PostgresTypeClass.Enum\n : PostgresTypeClass.Base\n : null;\n }\n\n table.columns[col.name] = {\n pos: Object.keys(table.columns).length + 1,\n dataType,\n characterMaximumLength: null,\n notNull: col.notNull !== 0,\n dflt: col.dflt,\n elemPgTypeClass,\n };\n if (metadata?.isBackfilling) {\n table.backfilling.push(col.name);\n }\n if (col.keyPos) {\n table.primaryKey ??= [];\n while (table.primaryKey.length < col.keyPos) {\n table.primaryKey.push('');\n }\n table.primaryKey[col.keyPos - 1] = col.name;\n }\n });\n\n return tables;\n}\n\n/**\n * An index as it exists on the replica. SQLite retains only the DDL text of a\n * partial index, not a structured predicate, so replica indexes report a\n * `partial` flag in place of the {@link LiteIndexSpec.predicate}.\n */\nexport type ReplicaIndexSpec = Omit<LiteIndexSpec, 'predicate'> & {\n readonly partial?: boolean | undefined;\n};\n\ntype MutableReplicaIndexSpec = Omit<MutableLiteIndexSpec, 'predicate'> & {\n partial?: boolean | undefined;\n};\n\nexport function listIndexes(db: Database): ReplicaIndexSpec[] {\n const indexes = db\n .prepare(\n `SELECT \n idx.name as indexName, \n idx.tbl_name as tableName, \n info.\"unique\" as \"unique\",\n info.partial as partial,\n col.name as column,\n CASE WHEN col.desc = 0 THEN 'ASC' ELSE 'DESC' END as dir\n FROM sqlite_master as idx\n JOIN pragma_index_list(idx.tbl_name) AS info ON info.name = idx.name\n JOIN pragma_index_xinfo(idx.name) as col\n WHERE idx.type = 'index' AND \n col.key = 1 AND\n idx.tbl_name NOT LIKE '_zero.%'\n ORDER BY idx.name, col.seqno ASC`,\n )\n .all() as {\n indexName: string;\n tableName: string;\n unique: number;\n partial: number;\n column: string;\n dir: 'ASC' | 'DESC';\n }[];\n\n const ret: MutableReplicaIndexSpec[] = [];\n for (const {\n indexName: name,\n tableName,\n unique,\n partial,\n column,\n dir,\n } of indexes) {\n if (ret.at(-1)?.name === name) {\n // Aggregate multiple column names into the array.\n must(ret.at(-1)).columns[column] = dir;\n } else {\n ret.push({\n tableName,\n name,\n columns: {[column]: dir},\n unique: unique !== 0,\n ...(partial === 0 ? {} : {partial: true}),\n });\n }\n }\n\n return ret;\n}\n\nexport type ZqlSpecOptions = {\n /**\n * Controls whether to include backfilling columns in the computed\n * LiteAndZqlSpec. In general, backfilling columns should be include\n * in \"replication\" logic (copying data from upstream to the replica),\n * and excluded from \"sync\" logic (sending data from the replica to\n * clients).\n */\n includeBackfillingColumns: boolean;\n};\n\n/**\n * Computes a TableSpec \"view\" of the replicated data that is\n * suitable for processing / consumption for the client. This\n * includes:\n * * excluding tables without a PRIMARY KEY or UNIQUE INDEX\n * * excluding columns with types that are not supported by ZQL\n * * choosing columns to use as the primary key amongst those\n * in unique indexes\n *\n * @param tableSpecs an optional map to reset and populate\n * @param fullTables an optional map to receive the full table specs,\n * which may include tables and columns that are not synced to\n * the client because they lack a primary key or are of unsupported\n * data types.\n */\nexport function computeZqlSpecs(\n lc: LogContext,\n replica: Database,\n opts: ZqlSpecOptions,\n tableSpecs: Map<string, LiteAndZqlSpec> = new Map(),\n fullTables?: Map<string, LiteTableSpec>,\n): Map<string, LiteAndZqlSpec> {\n return computeZqlSpecsFromLiteSpecs(\n listTables(replica),\n listIndexes(replica),\n opts,\n tableSpecs,\n fullTables,\n lc,\n );\n}\n\nfunction isPartialIndex(idx: LiteIndexSpec | ReplicaIndexSpec): boolean {\n return (\n ('predicate' in idx && idx.predicate !== undefined) ||\n ('partial' in idx && idx.partial === true)\n );\n}\n\nexport function computeZqlSpecsFromLiteSpecs(\n tables: LiteTableSpecWithReplicationStatus[],\n indexes: readonly (LiteIndexSpec | ReplicaIndexSpec)[],\n {includeBackfillingColumns}: ZqlSpecOptions,\n tableSpecs: Map<string, LiteAndZqlSpec> = new Map(),\n fullTables?: Map<string, LiteTableSpec>,\n lc?: LogContext,\n): Map<string, LiteAndZqlSpec> {\n tableSpecs.clear();\n fullTables?.clear();\n\n const uniqueIndexColumns = new Map<string, string[][]>();\n // A partial unique index only constrains the rows matching its predicate,\n // so it cannot serve as a row key.\n for (const {tableName, columns} of indexes.filter(\n idx => idx.unique && !isPartialIndex(idx),\n )) {\n getOrInsertComputed(uniqueIndexColumns, tableName, newArray).push(\n Object.keys(columns),\n );\n }\n\n tables.forEach(fullTable => {\n fullTables?.set(fullTable.name, fullTable);\n\n const backfilling = new Set(fullTable.backfilling);\n // Only include columns that:\n // - have a defined ZQL Value\n // - aren't backfilling if `includeBackfillingColumns` is false\n const visibleColumns = Object.entries(fullTable.columns).filter(\n ([col, {dataType}]) =>\n liteTypeToZqlValueType(dataType) &&\n (includeBackfillingColumns || !backfilling.has(col)),\n );\n const notNullColumns = new Set(\n visibleColumns\n .filter(\n ([col, {dataType}]) =>\n !nullableUpstream(dataType) || fullTable.primaryKey?.includes(col),\n )\n .map(([col]) => col),\n );\n\n const uniqueKeys = uniqueIndexColumns.get(fullTable.name) ?? [];\n // Examine all column combinations that can serve as a primary key,\n // i.e. excluding indexes over nullable or unsynced columns.\n const keys = uniqueKeys.filter(\n key => difference(new Set(key), notNullColumns).size === 0,\n );\n if (keys.length === 0) {\n // Only include tables with a row key.\n //\n // Note that this will automatically exclude tables that are being\n // backfilled (when includeBackfillingColumns is `false`) since candidate\n // keys only include visible columns.\n lc?.debug?.(\n `not syncing table ${fullTable.name} because it ` +\n (backfilling.size ? 'is being backfilled' : 'has no primary key'),\n );\n return;\n }\n // Pick the \"best\" (i.e. shortest) key for default IVM operations.\n const primaryKey = keys.sort(keyCmp)[0];\n\n const tableSpec = {\n ...fullTable,\n columns: Object.fromEntries(visibleColumns),\n primaryKey: v.parse(primaryKey, primaryKeySchema),\n uniqueKeys: uniqueKeys.map(key => v.parse(key, primaryKeySchema)),\n allPotentialPrimaryKeys: keys.map(key => v.parse(key, primaryKeySchema)),\n minRowVersion: fullTable.minRowVersion ?? null,\n };\n\n tableSpecs.set(tableSpec.name, {\n tableSpec,\n zqlSpec: Object.fromEntries(\n Object.entries(tableSpec.columns).map(([name, {dataType}]) => [\n name,\n mapLiteDataTypeToZqlSchemaValue(dataType),\n ]),\n ),\n });\n });\n return tableSpecs;\n}\n\nexport function mustGetTableSpec(\n tableSpecs: Map<string, LiteAndZqlSpec>,\n tableName: string,\n): LiteAndZqlSpec {\n const tableSpec = tableSpecs.get(tableName);\n if (!tableSpec) {\n throw new Error(\n `table '${tableName}' is not one of: ${[...tableSpecs.keys()]\n .filter(t => !t.includes('.') && !t.startsWith('_litestream_'))\n .sort()}. ` +\n `Check the spelling and ensure that the table has a primary key.`,\n );\n }\n return tableSpec;\n}\n\n// Deterministic comparator for favoring shorter row keys.\nfunction keyCmp(a: string[], b: string[]) {\n if (a.length !== b.length) {\n return a.length - b.length; // Fewer columns are better.\n }\n for (let i = 0; i < a.length; i++) {\n if (a[i] < b[i]) {\n return -1;\n }\n if (a[i] > b[i]) {\n return 1;\n }\n }\n return 0;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/observability/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EAET,aAAa,EACb,eAAe,EACf,aAAa,EACd,MAAM,oBAAoB,CAAC;AAK5B,MAAM,MAAM,QAAQ,GAChB,aAAa,GACb,SAAS,GACT,MAAM,GACN,UAAU,GACV,QAAQ,CAAC;AAIb,KAAK,OAAO,GAAG,aAAa,GAAG;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,CAAC;AACrD,KAAK,gBAAgB,GAAG,OAAO,GAAG;IAAC,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAAC,CAAC;AAE/D,eAAO,MAAM,iCAAiC,wGAIpC,CAAC;AAEX,eAAO,MAAM,oCAAoC,UAEhD,CAAC;AA4BF,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,aAAa,CAAC;AACjB,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,GACZ,aAAa,CAAC;AAcjB;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;;;OAMG;IACH,QAAQ,CACN,UAAU,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,GAC9C,IAAI,CAAC;CACT,CAAC;AAoBF;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,gBAAgB,GACrB,SAAS,CAWX;AAED,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,gBAAgB,GACrB,gBAAgB,CAelB;AAKD,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,GACZ,gBAAgB,CAQlB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,gBAAgB,CAMlB;AAID,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;AACX,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC;AAgBX,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,eAAe,CAAC;AACnB,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,GACZ,eAAe,CAAC"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/observability/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EAET,aAAa,EACb,eAAe,EACf,aAAa,EACd,MAAM,oBAAoB,CAAC;AAM5B,MAAM,MAAM,QAAQ,GAChB,aAAa,GACb,SAAS,GACT,MAAM,GACN,UAAU,GACV,QAAQ,CAAC;AAIb,KAAK,OAAO,GAAG,aAAa,GAAG;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,CAAC;AACrD,KAAK,gBAAgB,GAAG,OAAO,GAAG;IAAC,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAAC,CAAC;AAE/D,eAAO,MAAM,iCAAiC,wGAIpC,CAAC;AAEX,eAAO,MAAM,oCAAoC,UAEhD,CAAC;AAoBF,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,aAAa,CAAC;AACjB,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,GACZ,aAAa,CAAC;AAcjB;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;;;OAMG;IACH,QAAQ,CACN,UAAU,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,GAC9C,IAAI,CAAC;CACT,CAAC;AAoBF;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,gBAAgB,GACrB,SAAS,CAWX;AAED,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,gBAAgB,GACrB,gBAAgB,CAelB;AAKD,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,GACZ,gBAAgB,CAQlB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,gBAAgB,CAMlB;AAID,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;AACX,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC;AAgBX,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,eAAe,CAAC;AACnB,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,GACZ,eAAe,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { getOrInsertComputed } from "../../../shared/src/map.js";
1
2
  import { metrics } from "@opentelemetry/api";
2
3
  //#region ../zero-cache/src/observability/metrics.ts
3
4
  var meter;
@@ -27,13 +28,7 @@ function getMeter() {
27
28
  }
28
29
  function cache() {
29
30
  const instruments = /* @__PURE__ */ new Map();
30
- return (name, creator) => {
31
- const existing = instruments.get(name);
32
- if (existing) return existing;
33
- const ret = creator(name);
34
- instruments.set(name, ret);
35
- return ret;
36
- };
31
+ return (name, creator) => getOrInsertComputed(instruments, name, creator);
37
32
  }
38
33
  var upDownCounters = cache();
39
34
  function getOrCreateUpDownCounter(category, name, opts) {
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","names":[],"sources":["../../../../../zero-cache/src/observability/metrics.ts"],"sourcesContent":["import type {\n Counter,\n Histogram,\n Meter,\n MetricOptions,\n ObservableGauge,\n UpDownCounter,\n} from '@opentelemetry/api';\nimport {metrics} from '@opentelemetry/api';\n\n// intentional lazy initialization so it is not started before the SDK is started.\n\nexport type Category =\n | 'replication' // postgres to replica\n | 'replica' // health of replica and litestream backup\n | 'sync' // replica to client\n | 'mutation'\n | 'server';\n\nlet meter: Meter | undefined;\n\ntype Options = MetricOptions & {description: string};\ntype HistogramOptions = Options & {bucketBoundaries: number[]};\n\nexport const NATIVE_HISTOGRAM_INSTRUMENT_NAMES = [\n 'zero.sync.view_syncer_lag',\n 'zero.sync.view_syncer_hydration',\n 'zero.sync.e2e_serving_lag',\n] as const;\n\nexport const LONG_DURATION_HISTOGRAM_BOUNDARIES_S = [\n 1, 2, 5, 10, 30, 60, 120, 300, 600, 1200, 2400, 3600, 7200,\n];\n\nfunction getMeter() {\n if (!meter) {\n meter = metrics.getMeter('zero');\n }\n return meter;\n}\n\nfunction cache<TRet>(): (\n name: string,\n creator: (name: string) => TRet,\n) => TRet {\n const instruments = new Map<string, TRet>();\n return (name: string, creator: (name: string) => TRet) => {\n const existing = instruments.get(name);\n if (existing) {\n return existing;\n }\n\n const ret = creator(name);\n instruments.set(name, ret);\n return ret;\n };\n}\n\nconst upDownCounters = cache<UpDownCounter>();\n\nexport function getOrCreateUpDownCounter(\n category: Category,\n name: string,\n description: string,\n): UpDownCounter;\nexport function getOrCreateUpDownCounter(\n category: Category,\n name: string,\n opts: Options,\n): UpDownCounter;\nexport function getOrCreateUpDownCounter(\n category: Category,\n name: string,\n opts: string | Options,\n): UpDownCounter {\n return upDownCounters(name, name =>\n getMeter().createUpDownCounter(\n `zero.${category}.${name}`,\n typeof opts === 'string' ? {description: opts} : opts,\n ),\n );\n}\n\n/**\n * A latency histogram whose {@link recordMs} method accepts raw millisecond\n * durations and converts them to seconds internally.\n *\n * Use {@link getOrCreateLatencyHistogram} to create one — the unit (`'s'`),\n * bucket boundaries, and ms→s conversion are all baked in\n */\nexport type LatencyHistogram = {\n /**\n * Record a duration. Pass the raw elapsed milliseconds — the conversion to\n * seconds (required by the `unit: 's'` OTel histogram) is handled internally.\n *\n * @param durationMs Elapsed time in **milliseconds** (do NOT pre-divide).\n * @param attributes Optional OTel attributes to attach to the observation.\n */\n recordMs(\n durationMs: number,\n attributes?: Parameters<Histogram['record']>[1],\n ): void;\n};\n\n/**\n * Bucket boundaries (in seconds) for zero's latency histograms.\n *\n * The operational range is 1 ms – 5,000 ms (including customers actively\n * tuning queries). ~2× logarithmic steps give proportionally consistent\n * `histogram_quantile` accuracy regardless of where values cluster within\n * that range. 10,000 ms and 30,000 ms are overflow catchers for truly broken\n * states.\n *\n * 1 ms, 2 ms, 5 ms, 10 ms, 20 ms, 50 ms, 100 ms, 200 ms, 500 ms,\n * 1 s, 2 s, 5 s, 10 s, 30 s\n */\nconst LATENCY_HISTOGRAM_BOUNDARIES_S = [\n 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30,\n];\n\nconst histograms = cache<Histogram>();\n\n/**\n * Creates a histogram for dimensionless values without applying the\n * millisecond-to-second conversion used by the latency helpers below.\n */\nexport function getOrCreateValueHistogram(\n category: Category,\n name: string,\n opts: HistogramOptions,\n): Histogram {\n const {bucketBoundaries, ...metricOptions} = opts;\n return histograms(name, name =>\n getMeter().createHistogram(`zero.${category}.${name}`, {\n ...metricOptions,\n advice: {\n ...metricOptions.advice,\n explicitBucketBoundaries: bucketBoundaries,\n },\n }),\n );\n}\n\nexport function getOrCreateHistogram(\n category: Category,\n name: string,\n opts: HistogramOptions,\n): LatencyHistogram {\n const {bucketBoundaries, ...metricOptions} = opts;\n const h = histograms(name, name =>\n getMeter().createHistogram(`zero.${category}.${name}`, {\n ...metricOptions,\n advice: {\n ...metricOptions.advice,\n explicitBucketBoundaries: bucketBoundaries,\n },\n }),\n );\n return {\n recordMs: (durationMs, attributes) =>\n h.record(durationMs / 1000, attributes),\n };\n}\n\n// Add instruments created through this helper to\n// NATIVE_HISTOGRAM_INSTRUMENT_NAMES so the SDK exports them as exponential\n// histograms.\nexport function getOrCreateNativeHistogram(\n category: Category,\n name: string,\n opts: Options,\n): LatencyHistogram {\n const h = histograms(name, name =>\n getMeter().createHistogram(`zero.${category}.${name}`, opts),\n );\n return {\n recordMs: (durationMs, attributes) =>\n h.record(durationMs / 1000, attributes),\n };\n}\n\n/**\n * Creates (or retrieves) a latency histogram for the given metric.\n *\n * - `unit` is always `'s'` (seconds), matching the OTel convention.\n * - Bucket boundaries are pre-set for zero's typical operation range\n * (1 ms – 5 s); see {@link LATENCY_HISTOGRAM_BOUNDARIES_S}.\n * - The returned {@link LatencyHistogram} accepts **milliseconds** via\n * `recordMs()`, so callers never need to divide by 1000.\n *\n * @example\n * ```ts\n * readonly #hydrationTime = getOrCreateLatencyHistogram(\n * 'sync', 'hydration-time', 'Time to hydrate a query.',\n * );\n * // ...\n * this.#hydrationTime.recordMs(performance.now() - start);\n * ```\n */\nexport function getOrCreateLatencyHistogram(\n category: Category,\n name: string,\n description: string,\n): LatencyHistogram {\n return getOrCreateHistogram(category, name, {\n description,\n unit: 's',\n bucketBoundaries: LATENCY_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\nconst counters = cache<Counter>();\n\nexport function getOrCreateCounter(\n category: Category,\n name: string,\n description: string,\n): Counter;\nexport function getOrCreateCounter(\n category: Category,\n name: string,\n opts: Options,\n): Counter;\nexport function getOrCreateCounter(\n category: Category,\n name: string,\n opts: string | Options,\n): Counter {\n return counters(name, name =>\n getMeter().createCounter(\n `zero.${category}.${name}`,\n typeof opts === 'string' ? {description: opts} : opts,\n ),\n );\n}\n\nconst gauges = cache<ObservableGauge>();\n\nexport function getOrCreateGauge(\n category: Category,\n name: string,\n description: string,\n): ObservableGauge;\nexport function getOrCreateGauge(\n category: Category,\n name: string,\n opts: Options,\n): ObservableGauge;\nexport function getOrCreateGauge(\n category: Category,\n name: string,\n opts: string | Options,\n): ObservableGauge {\n return gauges(name, name =>\n getMeter().createObservableGauge(\n `zero.${category}.${name}`,\n typeof opts === 'string' ? {description: opts} : opts,\n ),\n );\n}\n"],"mappings":";;AAmBA,IAAI;AAKJ,IAAa,oCAAoC;CAC/C;CACA;CACA;AACF;AAEA,IAAa,uCAAuC;CAClD;CAAG;CAAG;CAAG;CAAI;CAAI;CAAI;CAAK;CAAK;CAAK;CAAM;CAAM;CAAM;AACxD;AAEA,SAAS,WAAW;CAClB,IAAI,CAAC,OACH,QAAQ,QAAQ,SAAS,MAAM;CAEjC,OAAO;AACT;AAEA,SAAS,QAGC;CACR,MAAM,8BAAc,IAAI,IAAkB;CAC1C,QAAQ,MAAc,YAAoC;EACxD,MAAM,WAAW,YAAY,IAAI,IAAI;EACrC,IAAI,UACF,OAAO;EAGT,MAAM,MAAM,QAAQ,IAAI;EACxB,YAAY,IAAI,MAAM,GAAG;EACzB,OAAO;CACT;AACF;AAEA,IAAM,iBAAiB,MAAqB;AAY5C,SAAgB,yBACd,UACA,MACA,MACe;CACf,OAAO,eAAe,OAAM,SAC1B,SAAS,EAAE,oBACT,QAAQ,SAAS,GAAG,QACpB,OAAO,SAAS,WAAW,EAAC,aAAa,KAAI,IAAI,IACnD,CACF;AACF;;;;;;;;;;;;;AAmCA,IAAM,iCAAiC;CACrC;CAAO;CAAO;CAAO;CAAM;CAAM;CAAM;CAAK;CAAK;CAAK;CAAG;CAAG;CAAG;CAAI;AACrE;AAEA,IAAM,aAAa,MAAiB;;;;;AAMpC,SAAgB,0BACd,UACA,MACA,MACW;CACX,MAAM,EAAC,kBAAkB,GAAG,kBAAiB;CAC7C,OAAO,WAAW,OAAM,SACtB,SAAS,EAAE,gBAAgB,QAAQ,SAAS,GAAG,QAAQ;EACrD,GAAG;EACH,QAAQ;GACN,GAAG,cAAc;GACjB,0BAA0B;EAC5B;CACF,CAAC,CACH;AACF;AAEA,SAAgB,qBACd,UACA,MACA,MACkB;CAClB,MAAM,EAAC,kBAAkB,GAAG,kBAAiB;CAC7C,MAAM,IAAI,WAAW,OAAM,SACzB,SAAS,EAAE,gBAAgB,QAAQ,SAAS,GAAG,QAAQ;EACrD,GAAG;EACH,QAAQ;GACN,GAAG,cAAc;GACjB,0BAA0B;EAC5B;CACF,CAAC,CACH;CACA,OAAO,EACL,WAAW,YAAY,eACrB,EAAE,OAAO,aAAa,KAAM,UAAU,EAC1C;AACF;AAKA,SAAgB,2BACd,UACA,MACA,MACkB;CAClB,MAAM,IAAI,WAAW,OAAM,SACzB,SAAS,EAAE,gBAAgB,QAAQ,SAAS,GAAG,QAAQ,IAAI,CAC7D;CACA,OAAO,EACL,WAAW,YAAY,eACrB,EAAE,OAAO,aAAa,KAAM,UAAU,EAC1C;AACF;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,4BACd,UACA,MACA,aACkB;CAClB,OAAO,qBAAqB,UAAU,MAAM;EAC1C;EACA,MAAM;EACN,kBAAkB;CACpB,CAAC;AACH;AAEA,IAAM,WAAW,MAAe;AAYhC,SAAgB,mBACd,UACA,MACA,MACS;CACT,OAAO,SAAS,OAAM,SACpB,SAAS,EAAE,cACT,QAAQ,SAAS,GAAG,QACpB,OAAO,SAAS,WAAW,EAAC,aAAa,KAAI,IAAI,IACnD,CACF;AACF;AAEA,IAAM,SAAS,MAAuB;AAYtC,SAAgB,iBACd,UACA,MACA,MACiB;CACjB,OAAO,OAAO,OAAM,SAClB,SAAS,EAAE,sBACT,QAAQ,SAAS,GAAG,QACpB,OAAO,SAAS,WAAW,EAAC,aAAa,KAAI,IAAI,IACnD,CACF;AACF"}
1
+ {"version":3,"file":"metrics.js","names":[],"sources":["../../../../../zero-cache/src/observability/metrics.ts"],"sourcesContent":["import type {\n Counter,\n Histogram,\n Meter,\n MetricOptions,\n ObservableGauge,\n UpDownCounter,\n} from '@opentelemetry/api';\nimport {metrics} from '@opentelemetry/api';\nimport {getOrInsertComputed} from '../../../shared/src/map.ts';\n\n// intentional lazy initialization so it is not started before the SDK is started.\n\nexport type Category =\n | 'replication' // postgres to replica\n | 'replica' // health of replica and litestream backup\n | 'sync' // replica to client\n | 'mutation'\n | 'server';\n\nlet meter: Meter | undefined;\n\ntype Options = MetricOptions & {description: string};\ntype HistogramOptions = Options & {bucketBoundaries: number[]};\n\nexport const NATIVE_HISTOGRAM_INSTRUMENT_NAMES = [\n 'zero.sync.view_syncer_lag',\n 'zero.sync.view_syncer_hydration',\n 'zero.sync.e2e_serving_lag',\n] as const;\n\nexport const LONG_DURATION_HISTOGRAM_BOUNDARIES_S = [\n 1, 2, 5, 10, 30, 60, 120, 300, 600, 1200, 2400, 3600, 7200,\n];\n\nfunction getMeter() {\n if (!meter) {\n meter = metrics.getMeter('zero');\n }\n return meter;\n}\n\nfunction cache<TRet extends object>(): (\n name: string,\n creator: (name: string) => TRet,\n) => TRet {\n const instruments = new Map<string, TRet>();\n return (name: string, creator: (name: string) => TRet) =>\n getOrInsertComputed(instruments, name, creator);\n}\n\nconst upDownCounters = cache<UpDownCounter>();\n\nexport function getOrCreateUpDownCounter(\n category: Category,\n name: string,\n description: string,\n): UpDownCounter;\nexport function getOrCreateUpDownCounter(\n category: Category,\n name: string,\n opts: Options,\n): UpDownCounter;\nexport function getOrCreateUpDownCounter(\n category: Category,\n name: string,\n opts: string | Options,\n): UpDownCounter {\n return upDownCounters(name, name =>\n getMeter().createUpDownCounter(\n `zero.${category}.${name}`,\n typeof opts === 'string' ? {description: opts} : opts,\n ),\n );\n}\n\n/**\n * A latency histogram whose {@link recordMs} method accepts raw millisecond\n * durations and converts them to seconds internally.\n *\n * Use {@link getOrCreateLatencyHistogram} to create one — the unit (`'s'`),\n * bucket boundaries, and ms→s conversion are all baked in\n */\nexport type LatencyHistogram = {\n /**\n * Record a duration. Pass the raw elapsed milliseconds — the conversion to\n * seconds (required by the `unit: 's'` OTel histogram) is handled internally.\n *\n * @param durationMs Elapsed time in **milliseconds** (do NOT pre-divide).\n * @param attributes Optional OTel attributes to attach to the observation.\n */\n recordMs(\n durationMs: number,\n attributes?: Parameters<Histogram['record']>[1],\n ): void;\n};\n\n/**\n * Bucket boundaries (in seconds) for zero's latency histograms.\n *\n * The operational range is 1 ms – 5,000 ms (including customers actively\n * tuning queries). ~2× logarithmic steps give proportionally consistent\n * `histogram_quantile` accuracy regardless of where values cluster within\n * that range. 10,000 ms and 30,000 ms are overflow catchers for truly broken\n * states.\n *\n * 1 ms, 2 ms, 5 ms, 10 ms, 20 ms, 50 ms, 100 ms, 200 ms, 500 ms,\n * 1 s, 2 s, 5 s, 10 s, 30 s\n */\nconst LATENCY_HISTOGRAM_BOUNDARIES_S = [\n 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30,\n];\n\nconst histograms = cache<Histogram>();\n\n/**\n * Creates a histogram for dimensionless values without applying the\n * millisecond-to-second conversion used by the latency helpers below.\n */\nexport function getOrCreateValueHistogram(\n category: Category,\n name: string,\n opts: HistogramOptions,\n): Histogram {\n const {bucketBoundaries, ...metricOptions} = opts;\n return histograms(name, name =>\n getMeter().createHistogram(`zero.${category}.${name}`, {\n ...metricOptions,\n advice: {\n ...metricOptions.advice,\n explicitBucketBoundaries: bucketBoundaries,\n },\n }),\n );\n}\n\nexport function getOrCreateHistogram(\n category: Category,\n name: string,\n opts: HistogramOptions,\n): LatencyHistogram {\n const {bucketBoundaries, ...metricOptions} = opts;\n const h = histograms(name, name =>\n getMeter().createHistogram(`zero.${category}.${name}`, {\n ...metricOptions,\n advice: {\n ...metricOptions.advice,\n explicitBucketBoundaries: bucketBoundaries,\n },\n }),\n );\n return {\n recordMs: (durationMs, attributes) =>\n h.record(durationMs / 1000, attributes),\n };\n}\n\n// Add instruments created through this helper to\n// NATIVE_HISTOGRAM_INSTRUMENT_NAMES so the SDK exports them as exponential\n// histograms.\nexport function getOrCreateNativeHistogram(\n category: Category,\n name: string,\n opts: Options,\n): LatencyHistogram {\n const h = histograms(name, name =>\n getMeter().createHistogram(`zero.${category}.${name}`, opts),\n );\n return {\n recordMs: (durationMs, attributes) =>\n h.record(durationMs / 1000, attributes),\n };\n}\n\n/**\n * Creates (or retrieves) a latency histogram for the given metric.\n *\n * - `unit` is always `'s'` (seconds), matching the OTel convention.\n * - Bucket boundaries are pre-set for zero's typical operation range\n * (1 ms – 5 s); see {@link LATENCY_HISTOGRAM_BOUNDARIES_S}.\n * - The returned {@link LatencyHistogram} accepts **milliseconds** via\n * `recordMs()`, so callers never need to divide by 1000.\n *\n * @example\n * ```ts\n * readonly #hydrationTime = getOrCreateLatencyHistogram(\n * 'sync', 'hydration-time', 'Time to hydrate a query.',\n * );\n * // ...\n * this.#hydrationTime.recordMs(performance.now() - start);\n * ```\n */\nexport function getOrCreateLatencyHistogram(\n category: Category,\n name: string,\n description: string,\n): LatencyHistogram {\n return getOrCreateHistogram(category, name, {\n description,\n unit: 's',\n bucketBoundaries: LATENCY_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\nconst counters = cache<Counter>();\n\nexport function getOrCreateCounter(\n category: Category,\n name: string,\n description: string,\n): Counter;\nexport function getOrCreateCounter(\n category: Category,\n name: string,\n opts: Options,\n): Counter;\nexport function getOrCreateCounter(\n category: Category,\n name: string,\n opts: string | Options,\n): Counter {\n return counters(name, name =>\n getMeter().createCounter(\n `zero.${category}.${name}`,\n typeof opts === 'string' ? {description: opts} : opts,\n ),\n );\n}\n\nconst gauges = cache<ObservableGauge>();\n\nexport function getOrCreateGauge(\n category: Category,\n name: string,\n description: string,\n): ObservableGauge;\nexport function getOrCreateGauge(\n category: Category,\n name: string,\n opts: Options,\n): ObservableGauge;\nexport function getOrCreateGauge(\n category: Category,\n name: string,\n opts: string | Options,\n): ObservableGauge {\n return gauges(name, name =>\n getMeter().createObservableGauge(\n `zero.${category}.${name}`,\n typeof opts === 'string' ? {description: opts} : opts,\n ),\n );\n}\n"],"mappings":";;;AAoBA,IAAI;AAKJ,IAAa,oCAAoC;CAC/C;CACA;CACA;AACF;AAEA,IAAa,uCAAuC;CAClD;CAAG;CAAG;CAAG;CAAI;CAAI;CAAI;CAAK;CAAK;CAAK;CAAM;CAAM;CAAM;AACxD;AAEA,SAAS,WAAW;CAClB,IAAI,CAAC,OACH,QAAQ,QAAQ,SAAS,MAAM;CAEjC,OAAO;AACT;AAEA,SAAS,QAGC;CACR,MAAM,8BAAc,IAAI,IAAkB;CAC1C,QAAQ,MAAc,YACpB,oBAAoB,aAAa,MAAM,OAAO;AAClD;AAEA,IAAM,iBAAiB,MAAqB;AAY5C,SAAgB,yBACd,UACA,MACA,MACe;CACf,OAAO,eAAe,OAAM,SAC1B,SAAS,EAAE,oBACT,QAAQ,SAAS,GAAG,QACpB,OAAO,SAAS,WAAW,EAAC,aAAa,KAAI,IAAI,IACnD,CACF;AACF;;;;;;;;;;;;;AAmCA,IAAM,iCAAiC;CACrC;CAAO;CAAO;CAAO;CAAM;CAAM;CAAM;CAAK;CAAK;CAAK;CAAG;CAAG;CAAG;CAAI;AACrE;AAEA,IAAM,aAAa,MAAiB;;;;;AAMpC,SAAgB,0BACd,UACA,MACA,MACW;CACX,MAAM,EAAC,kBAAkB,GAAG,kBAAiB;CAC7C,OAAO,WAAW,OAAM,SACtB,SAAS,EAAE,gBAAgB,QAAQ,SAAS,GAAG,QAAQ;EACrD,GAAG;EACH,QAAQ;GACN,GAAG,cAAc;GACjB,0BAA0B;EAC5B;CACF,CAAC,CACH;AACF;AAEA,SAAgB,qBACd,UACA,MACA,MACkB;CAClB,MAAM,EAAC,kBAAkB,GAAG,kBAAiB;CAC7C,MAAM,IAAI,WAAW,OAAM,SACzB,SAAS,EAAE,gBAAgB,QAAQ,SAAS,GAAG,QAAQ;EACrD,GAAG;EACH,QAAQ;GACN,GAAG,cAAc;GACjB,0BAA0B;EAC5B;CACF,CAAC,CACH;CACA,OAAO,EACL,WAAW,YAAY,eACrB,EAAE,OAAO,aAAa,KAAM,UAAU,EAC1C;AACF;AAKA,SAAgB,2BACd,UACA,MACA,MACkB;CAClB,MAAM,IAAI,WAAW,OAAM,SACzB,SAAS,EAAE,gBAAgB,QAAQ,SAAS,GAAG,QAAQ,IAAI,CAC7D;CACA,OAAO,EACL,WAAW,YAAY,eACrB,EAAE,OAAO,aAAa,KAAM,UAAU,EAC1C;AACF;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,4BACd,UACA,MACA,aACkB;CAClB,OAAO,qBAAqB,UAAU,MAAM;EAC1C;EACA,MAAM;EACN,kBAAkB;CACpB,CAAC;AACH;AAEA,IAAM,WAAW,MAAe;AAYhC,SAAgB,mBACd,UACA,MACA,MACS;CACT,OAAO,SAAS,OAAM,SACpB,SAAS,EAAE,cACT,QAAQ,SAAS,GAAG,QACpB,OAAO,SAAS,WAAW,EAAC,aAAa,KAAI,IAAI,IACnD,CACF;AACF;AAEA,IAAM,SAAS,MAAuB;AAYtC,SAAgB,iBACd,UACA,MACA,MACiB;CACjB,OAAO,OAAO,OAAM,SAClB,SAAS,EAAE,sBACT,QAAQ,SAAS,GAAG,QACpB,OAAO,SAAS,WAAW,EAAC,aAAa,KAAI,IAAI,IACnD,CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"inspector-delegate.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/inspector-delegate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,6BAA6B,CAAC;AAEnE,OAAO,EAAC,OAAO,EAAC,MAAM,gCAAgC,CAAC;AACvD,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,mCAAmC,CAAC;AAC3D,OAAO,KAAK,EAAC,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,qBAAa,iBAAkB,YAAW,eAAe;;gBAO3C,sBAAsB,EAAE,sBAAsB,GAAG,SAAS;IAItE,SAAS,CAAC,CAAC,SAAS,MAAM,SAAS,EACjC,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,MAAM,EACb,GAAG,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,GACpB,IAAI;IAgBP,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI;IAYtE,cAAc;;;;IAId,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS;IAIhD,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAMlC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI;IAIzC;;;OAGG;IACH,eAAe,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO;IAMtD;;;OAGG;IACH,gBAAgB,CACd,aAAa,EAAE,aAAa,EAC5B,KAAK,GAAE,SAAoB,GAC1B,IAAI;IAIP;;;;;;OAMG;IACH,kBAAkB,CAAC,aAAa,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,SAAS;IASlE;;;;OAIG;IACG,oBAAoB,CACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,SAAS,iBAAiB,EAAE,EAClC,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,GAAG,CAAC;CAuChB"}
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,qBAAa,iBAAkB,YAAW,eAAe;;gBAO3C,sBAAsB,EAAE,sBAAsB,GAAG,SAAS;IAItE,SAAS,CAAC,CAAC,SAAS,MAAM,SAAS,EACjC,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,MAAM,EACb,GAAG,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,GACpB,IAAI;IAeP,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI;IAYtE,cAAc;;;;IAId,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS;IAIhD,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAMlC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI;IAIzC;;;OAGG;IACH,eAAe,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO;IAMtD;;;OAGG;IACH,gBAAgB,CACd,aAAa,EAAE,aAAa,EAC5B,KAAK,GAAE,SAAoB,GAC1B,IAAI;IAIP;;;;;;OAMG;IACH,kBAAkB,CAAC,aAAa,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,SAAS;IASlE;;;;OAIG;IACG,oBAAoB,CACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,SAAS,iBAAiB,EAAE,EAClC,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,GAAG,CAAC;CAuChB"}
@@ -1,4 +1,5 @@
1
1
  import { assert } from "../../../shared/src/asserts.js";
2
+ import { getOrInsertComputed } from "../../../shared/src/map.js";
2
3
  import { hashNameAndArgs } from "../../../zero-protocol/src/query-hash-visitor.js";
3
4
  import { mapValues } from "../../../shared/src/objects.js";
4
5
  import { isServerMetric } from "../../../zql/src/query/metrics-delegate.js";
@@ -27,14 +28,7 @@ var InspectorDelegate = class {
27
28
  assert(isServerMetric(metric), `Invalid server metric: ${metric}`);
28
29
  const queryID = args[0];
29
30
  if (metric === "query-materialization-server") this.#perQueryHydrateMs.set(queryID, value);
30
- else {
31
- let digest = this.#perQueryUpdateMetrics.get(queryID);
32
- if (!digest) {
33
- digest = new TDigest();
34
- this.#perQueryUpdateMetrics.set(queryID, digest);
35
- }
36
- digest.add(value);
37
- }
31
+ else getOrInsertComputed(this.#perQueryUpdateMetrics, queryID, () => new TDigest()).add(value);
38
32
  this.#globalMetrics[metric].add(value);
39
33
  }
40
34
  getMetricsJSONForQuery(queryID) {
@@ -1 +1 @@
1
- {"version":3,"file":"inspector-delegate.js","names":["#globalMetrics","#perQueryHydrateMs","#perQueryUpdateMetrics","#queryIDToAST","#customQueryTransformer"],"sources":["../../../../../zero-cache/src/server/inspector-delegate.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport type {ReadonlyJSONValue} from '../../../shared/src/json.ts';\nimport {mapValues} from '../../../shared/src/objects.ts';\nimport {TDigest} from '../../../shared/src/tdigest.ts';\nimport type {AST} from '../../../zero-protocol/src/ast.ts';\nimport type {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\nexport class InspectorDelegate implements MetricsDelegate {\n readonly #globalMetrics: ServerMetrics = newMetrics();\n readonly #perQueryHydrateMs = new Map<string, number>();\n readonly #perQueryUpdateMetrics = new Map<string, TDigest>();\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 } else {\n let digest = this.#perQueryUpdateMetrics.get(queryID);\n if (!digest) {\n digest = new TDigest();\n this.#perQueryUpdateMetrics.set(queryID, digest);\n }\n digest.add(value);\n }\n this.#globalMetrics[metric].add(value);\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 ?? 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":";;;;;;;;;AAoCA,IAAM,WAAsB,CAAC;;;;;;AAO7B,IAAM,8CAA8B,IAAI,IAA8B;AAEtE,IAAa,oBAAb,MAA0D;CACxD,iBAAyC,WAAW;CACpD,qCAA8B,IAAI,IAAoB;CACtD,yCAAkC,IAAI,IAAqB;CAC3D,gCAA2C,IAAI,IAAI;CACnD;CAEA,YAAY,wBAA4D;EACtE,KAAKI,0BAA0B;CACjC;CAEA,UACE,QACA,OACA,GAAG,MACG;EACN,OAAO,eAAe,MAAM,GAAG,0BAA0B,QAAQ;EACjE,MAAM,UAAU,KAAK;EACrB,IAAI,WAAW,gCACb,KAAKH,mBAAmB,IAAI,SAAS,KAAK;OACrC;GACL,IAAI,SAAS,KAAKC,uBAAuB,IAAI,OAAO;GACpD,IAAI,CAAC,QAAQ;IACX,SAAS,IAAI,QAAQ;IACrB,KAAKA,uBAAuB,IAAI,SAAS,MAAM;GACjD;GACA,OAAO,IAAI,KAAK;EAClB;EACA,KAAKF,eAAe,QAAQ,IAAI,KAAK;CACvC;CAEA,uBAAuB,SAAgD;EACrE,MAAM,YAAY,KAAKC,mBAAmB,IAAI,OAAO;EACrD,MAAM,gBAAgB,KAAKC,uBAAuB,IAAI,OAAO;EAC7D,IAAI,cAAc,KAAA,KAAa,kBAAkB,KAAA,GAC/C,OAAO;EAET,OAAO;GACL,6BAA6B;GAC7B,wBAAwB,iBAAiB,IAAI,QAAQ,GAAG,OAAO;EACjE;CACF;CAEA,iBAAiB;EACf,OAAO,UAAU,KAAKF,iBAAgB,MAAK,EAAE,OAAO,CAAC;CACvD;CAEA,eAAe,SAAkC;EAC/C,OAAO,KAAKG,cAAc,IAAI,OAAO;CACvC;CAEA,YAAY,SAAuB;EACjC,KAAKF,mBAAmB,OAAO,OAAO;EACtC,KAAKC,uBAAuB,OAAO,OAAO;EAC1C,KAAKC,cAAc,OAAO,OAAO;CACnC;CAEA,SAAS,SAAiB,KAAgB;EACxC,KAAKA,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,KAAKC,yBACL,mFACF;EAIA,MAAM,UAA+B,CACnC;GACE,IAHY,gBAAkB,MAAM,IAGhC;GACJ,MAAM;GACN;GACA;GACA,aAAa,CAAC;EAChB,CACF;EAEA,MAAM,UAAU,MAAM,KAAKA,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":["#globalMetrics","#perQueryHydrateMs","#perQueryUpdateMetrics","#queryIDToAST","#customQueryTransformer"],"sources":["../../../../../zero-cache/src/server/inspector-delegate.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport type {ReadonlyJSONValue} from '../../../shared/src/json.ts';\nimport {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\nexport class InspectorDelegate implements MetricsDelegate {\n readonly #globalMetrics: ServerMetrics = newMetrics();\n readonly #perQueryHydrateMs = new Map<string, number>();\n readonly #perQueryUpdateMetrics = new Map<string, TDigest>();\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 } else {\n getOrInsertComputed(\n this.#perQueryUpdateMetrics,\n queryID,\n () => new TDigest(),\n ).add(value);\n }\n this.#globalMetrics[metric].add(value);\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 ?? 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;AAEtE,IAAa,oBAAb,MAA0D;CACxD,iBAAyC,WAAW;CACpD,qCAA8B,IAAI,IAAoB;CACtD,yCAAkC,IAAI,IAAqB;CAC3D,gCAA2C,IAAI,IAAI;CACnD;CAEA,YAAY,wBAA4D;EACtE,KAAKI,0BAA0B;CACjC;CAEA,UACE,QACA,OACA,GAAG,MACG;EACN,OAAO,eAAe,MAAM,GAAG,0BAA0B,QAAQ;EACjE,MAAM,UAAU,KAAK;EACrB,IAAI,WAAW,gCACb,KAAKH,mBAAmB,IAAI,SAAS,KAAK;OAE1C,oBACE,KAAKC,wBACL,eACM,IAAI,QAAQ,CACpB,EAAE,IAAI,KAAK;EAEb,KAAKF,eAAe,QAAQ,IAAI,KAAK;CACvC;CAEA,uBAAuB,SAAgD;EACrE,MAAM,YAAY,KAAKC,mBAAmB,IAAI,OAAO;EACrD,MAAM,gBAAgB,KAAKC,uBAAuB,IAAI,OAAO;EAC7D,IAAI,cAAc,KAAA,KAAa,kBAAkB,KAAA,GAC/C,OAAO;EAET,OAAO;GACL,6BAA6B;GAC7B,wBAAwB,iBAAiB,IAAI,QAAQ,GAAG,OAAO;EACjE;CACF;CAEA,iBAAiB;EACf,OAAO,UAAU,KAAKF,iBAAgB,MAAK,EAAE,OAAO,CAAC;CACvD;CAEA,eAAe,SAAkC;EAC/C,OAAO,KAAKG,cAAc,IAAI,OAAO;CACvC;CAEA,YAAY,SAAuB;EACjC,KAAKF,mBAAmB,OAAO,OAAO;EACtC,KAAKC,uBAAuB,OAAO,OAAO;EAC1C,KAAKC,cAAc,OAAO,OAAO;CACnC;CAEA,SAAS,SAAiB,KAAgB;EACxC,KAAKA,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,KAAKC,yBACL,mFACF;EAIA,MAAM,UAA+B,CACnC;GACE,IAHY,gBAAkB,MAAM,IAGhC;GACJ,MAAM;GACN;GACA;GACA,aAAa,CAAC;EAChB,CACF;EAEA,MAAM,UAAU,MAAM,KAAKA,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":"syncer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/syncer.ts"],"names":[],"mappings":"AAiCA,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAqC/B,wBAA8B,SAAS,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAkNf"}
1
+ {"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/syncer.ts"],"names":[],"mappings":"AAkCA,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAqC/B,wBAA8B,SAAS,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAgOf"}
@@ -25,6 +25,7 @@ import { CustomQueryTransformer } from "../custom-queries/transform-query.js";
25
25
  import { MutagenService } from "../services/mutagen/mutagen.js";
26
26
  import { PusherService } from "../services/mutagen/pusher.js";
27
27
  import { ConnectionContextManagerImpl } from "../services/view-syncer/connection-context-manager.js";
28
+ import { SnapshotRowCache } from "../services/view-syncer/snapshot-row-cache.js";
28
29
  import { ViewSyncerService } from "../services/view-syncer/view-syncer.js";
29
30
  import { Syncer } from "../workers/syncer.js";
30
31
  import { InspectorDelegate } from "./inspector-delegate.js";
@@ -100,6 +101,7 @@ async function runWorker(parent, env, ...args) {
100
101
  })
101
102
  };
102
103
  };
104
+ const snapshotRowCache = config.snapshotRowCacheSize > 0 ? new SnapshotRowCache(config.snapshotRowCacheSize) : void 0;
103
105
  const viewSyncerFactory = (id, sub, drainCoordinator) => {
104
106
  const logger = lc.withContext("taskID", config.taskID).withContext("component", "view-syncer").withContext("appID", shard.appID).withContext("shardNum", shard.shardNum).withContext("clientGroupID", id).withContext("instance", randomID());
105
107
  const customQueryTransformer = customQueryConfig && new CustomQueryTransformer(logger, shard);
@@ -108,7 +110,7 @@ async function runWorker(parent, env, ...args) {
108
110
  const inspectorDelegate = new InspectorDelegate(customQueryTransformer);
109
111
  const priorityOpRunningYieldThresholdMs = Math.max(config.yieldThresholdMs / 4, 2);
110
112
  const normalYieldThresholdMs = Math.max(config.yieldThresholdMs, 2);
111
- return new ViewSyncerService(config, logger, shard, config.taskID, id, cvrDB, new PipelineDriver(logger, config.log, new Snapshotter(logger, replicaFile, shard), shard, operatorStorage.createClientGroupStorage(id), id, inspectorDelegate, () => isPriorityOpRunning() ? priorityOpRunningYieldThresholdMs : normalYieldThresholdMs, config.enableQueryPlanner, config), sub, drainCoordinator, config.log.slowHydrateThreshold, inspectorDelegate, connContextManager, customQueryTransformer, runPriorityOp);
113
+ return new ViewSyncerService(config, logger, shard, config.taskID, id, cvrDB, new PipelineDriver(logger, config.log, new Snapshotter(logger, replicaFile, shard, void 0, snapshotRowCache), shard, operatorStorage.createClientGroupStorage(id), id, inspectorDelegate, () => isPriorityOpRunning() ? priorityOpRunningYieldThresholdMs : normalYieldThresholdMs, config.enableQueryPlanner, config), sub, drainCoordinator, config.log.slowHydrateThreshold, inspectorDelegate, connContextManager, customQueryTransformer, runPriorityOp);
112
114
  };
113
115
  const syncer = new Syncer(lc, config, viewSyncerFactory, upstreamDB ? (id) => new MutagenService(lc.withContext("component", "mutagen").withContext("clientGroupID", id), shard, id, upstreamDB, config, writeAuthzStorage) : void 0, pushConfig === void 0 ? void 0 : (id, connContextManager) => new PusherService(config, lc.withContext("clientGroupID", id), id, connContextManager), parent, validateLegacyJWT);
114
116
  startAnonymousTelemetry(lc, config);
@@ -1 +1 @@
1
- {"version":3,"file":"syncer.js","names":[],"sources":["../../../../../zero-cache/src/server/syncer.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport {tmpdir} from 'node:os';\nimport path from 'node:path';\nimport {pid} from 'node:process';\nimport {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {randInt} from '../../../shared/src/rand.ts';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport {DatabaseStorage} from '../../../zqlite/src/database-storage.ts';\nimport type {ValidateLegacyJWT} from '../auth/auth.ts';\nimport {tokenConfigOptions, verifyToken} from '../auth/jwt.ts';\nimport type {NormalizedZeroConfig} from '../config/normalize.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {CustomQueryTransformer} from '../custom-queries/transform-query.ts';\nimport {registerSQLiteCorruptionDiagnosticTarget} from '../db/sqlite-corruption.ts';\nimport {warmupConnections} from '../db/warmup.ts';\nimport {initEventSink} from '../observability/events.ts';\nimport {exitAfter, runUntilKilled} from '../services/life-cycle.ts';\nimport {MutagenService} from '../services/mutagen/mutagen.ts';\nimport {PusherService} from '../services/mutagen/pusher.ts';\nimport type {ReplicaState} from '../services/replicator/replicator.ts';\nimport {\n type ConnectionContextManager,\n ConnectionContextManagerImpl,\n} from '../services/view-syncer/connection-context-manager.ts';\nimport type {DrainCoordinator} from '../services/view-syncer/drain-coordinator.ts';\nimport {PipelineDriver} from '../services/view-syncer/pipeline-driver.ts';\nimport {Snapshotter} from '../services/view-syncer/snapshotter.ts';\nimport {ViewSyncerService} from '../services/view-syncer/view-syncer.ts';\nimport {ProtocolErrorWithLevel} from '../types/error-with-level.ts';\nimport {connectPgClient} from '../types/pg.ts';\nimport {\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {installProfileHandler} from '../types/profiler.ts';\nimport {getShardID} from '../types/shards.ts';\nimport type {Subscription} from '../types/subscription.ts';\nimport {replicaFileModeSchema, replicaFileName} from '../workers/replicator.ts';\nimport {Syncer} from '../workers/syncer.ts';\nimport {startAnonymousTelemetry} from './anonymous-otel-start.ts';\nimport {InspectorDelegate} from './inspector-delegate.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\nimport {isPriorityOpRunning, runPriorityOp} from './priority-op.ts';\n\nfunction randomID() {\n return randInt(1, Number.MAX_SAFE_INTEGER).toString(36);\n}\n\nfunction getCustomQueryConfig(\n config: Pick<NormalizedZeroConfig, 'query' | 'getQueries'>,\n) {\n const queryConfig = config.query?.url ? config.query : config.getQueries;\n\n if (!queryConfig?.url) {\n return undefined;\n }\n\n return {\n url: queryConfig.url,\n apiKey: queryConfig.apiKey,\n allowedClientHeaders: queryConfig.allowedClientHeaders,\n allowedRequestHeaders: queryConfig.allowedRequestHeaders,\n forwardCookies: queryConfig.forwardCookies ?? false,\n };\n}\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 ...args: string[]\n): Promise<void> {\n assert(args.length >= 2, `expected [fileMode, workerIndex, ...flags]`);\n const fileMode = v.parse(args[0], replicaFileModeSchema);\n const workerIndex = Number(args[1]);\n installProfileHandler(parent, `syncer-${workerIndex}`, workerIndex);\n const config = getNormalizedZeroConfig({env, argv: args.slice(2)});\n\n startOtelAuto(\n createLogContext(config, 'syncer', workerIndex, false),\n 'syncer',\n workerIndex,\n );\n lc = createLogContext(config, 'syncer', workerIndex);\n initEventSink(lc, config);\n\n const {cvr, upstream, enableCrudMutations} = config;\n\n const replicaFile = replicaFileName(config.replica.file, fileMode);\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: 'syncer replica',\n dbPath: replicaFile,\n },\n config.sqliteCorruptionChecks,\n );\n lc.debug?.(`running view-syncer on ${replicaFile}`);\n\n const cvrDB = await connectPgClient(lc, cvr.db, `sync-worker-${pid}-cvr`, {\n max: must(cvr.maxConnsPerWorker, 'cvr.maxConnsPerWorker must be set'),\n });\n\n const upstreamDB =\n enableCrudMutations && upstream.type === 'pg'\n ? await connectPgClient(lc, upstream.db, `sync-worker-${pid}-upstream`, {\n max: must(\n upstream.maxConnsPerWorker,\n 'upstream.maxConnsPerWorker must be set',\n ),\n })\n : undefined;\n\n const dbWarmup = Promise.allSettled([\n warmupConnections(lc, cvrDB, 'cvr'),\n upstreamDB ? warmupConnections(lc, upstreamDB, 'upstream') : promiseVoid,\n ]);\n\n const tmpDir = config.storageDBTmpDir ?? tmpdir();\n const operatorStorage = DatabaseStorage.create(\n lc,\n path.join(tmpDir, `sync-worker-${randomUUID()}`),\n );\n const writeAuthzStorage = DatabaseStorage.create(\n lc,\n path.join(tmpDir, `mutagen-${randomUUID()}`),\n );\n\n const shard = getShardID(config);\n const customQueryConfig = getCustomQueryConfig(config);\n const pushConfig =\n config.push.url === undefined && config.mutate.url === undefined\n ? undefined\n : {\n ...config.push,\n ...config.mutate,\n url: must(\n config.push.url ?? config.mutate.url,\n 'No push or mutate URL configured',\n ),\n };\n\n /** @deprecated used in JWT validation */\n let validateLegacyJWT: ValidateLegacyJWT | undefined = undefined;\n\n const tokenOptions = tokenConfigOptions(config.auth ?? {});\n if (tokenOptions.length === 1) {\n validateLegacyJWT = async (token, {userID}) => {\n if (!userID) {\n throw new ProtocolErrorWithLevel(\n {\n kind: 'Unauthorized',\n message: 'UserID is required for JWT validation.',\n origin: 'zeroCache',\n },\n 'warn',\n );\n }\n\n const decoded = await verifyToken(config.auth, token, {\n subject: userID,\n ...(config.auth?.issuer && {issuer: config.auth.issuer}),\n ...(config.auth?.audience && {\n audience: config.auth.audience,\n }),\n });\n return {\n type: 'jwt',\n raw: token,\n decoded,\n };\n };\n }\n\n const viewSyncerFactory = (\n id: string,\n sub: Subscription<ReplicaState>,\n drainCoordinator: DrainCoordinator,\n ) => {\n const logger = lc\n .withContext('taskID', config.taskID)\n .withContext('component', 'view-syncer')\n .withContext('appID', shard.appID)\n .withContext('shardNum', shard.shardNum)\n .withContext('clientGroupID', id)\n .withContext('instance', randomID());\n\n const customQueryTransformer =\n customQueryConfig && new CustomQueryTransformer(logger, shard);\n const connContextManager = new ConnectionContextManagerImpl(\n logger,\n config.auth.revalidateIntervalSeconds,\n config.auth.retransformIntervalSeconds,\n customQueryConfig,\n pushConfig,\n validateLegacyJWT,\n );\n\n lc.debug?.(\n `creating view syncer. Query Planner Enabled: ${config.enableQueryPlanner}`,\n );\n\n const inspectorDelegate = new InspectorDelegate(customQueryTransformer);\n\n const priorityOpRunningYieldThresholdMs = Math.max(\n config.yieldThresholdMs / 4,\n 2,\n );\n const normalYieldThresholdMs = Math.max(config.yieldThresholdMs, 2);\n\n return new ViewSyncerService(\n config,\n logger,\n shard,\n config.taskID,\n id,\n cvrDB,\n new PipelineDriver(\n logger,\n config.log,\n new Snapshotter(logger, replicaFile, shard),\n shard,\n operatorStorage.createClientGroupStorage(id),\n id,\n inspectorDelegate,\n () =>\n isPriorityOpRunning()\n ? priorityOpRunningYieldThresholdMs\n : normalYieldThresholdMs,\n config.enableQueryPlanner,\n config,\n ),\n sub,\n drainCoordinator,\n config.log.slowHydrateThreshold,\n inspectorDelegate,\n connContextManager,\n customQueryTransformer,\n runPriorityOp,\n );\n };\n\n const mutagenFactory = upstreamDB\n ? (id: string) =>\n new MutagenService(\n lc\n .withContext('component', 'mutagen')\n .withContext('clientGroupID', id),\n shard,\n id,\n upstreamDB,\n config,\n writeAuthzStorage,\n )\n : undefined;\n\n const pusherFactory =\n pushConfig === undefined\n ? undefined\n : (id: string, connContextManager: ConnectionContextManager) =>\n new PusherService(\n config,\n lc.withContext('clientGroupID', id),\n id,\n connContextManager,\n );\n\n const syncer = new Syncer(\n lc,\n config,\n viewSyncerFactory,\n mutagenFactory,\n pusherFactory,\n parent,\n validateLegacyJWT,\n );\n\n startAnonymousTelemetry(lc, config);\n\n void dbWarmup.then(() => parent.send(['ready', {ready: true}]));\n\n return runUntilKilled(lc, parent, syncer);\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(\n () => lc,\n () => runWorker(must(parentWorker), process.env, ...process.argv.slice(2)),\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"syncer.js","names":[],"sources":["../../../../../zero-cache/src/server/syncer.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport {tmpdir} from 'node:os';\nimport path from 'node:path';\nimport {pid} from 'node:process';\nimport {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {randInt} from '../../../shared/src/rand.ts';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport {DatabaseStorage} from '../../../zqlite/src/database-storage.ts';\nimport type {ValidateLegacyJWT} from '../auth/auth.ts';\nimport {tokenConfigOptions, verifyToken} from '../auth/jwt.ts';\nimport type {NormalizedZeroConfig} from '../config/normalize.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {CustomQueryTransformer} from '../custom-queries/transform-query.ts';\nimport {registerSQLiteCorruptionDiagnosticTarget} from '../db/sqlite-corruption.ts';\nimport {warmupConnections} from '../db/warmup.ts';\nimport {initEventSink} from '../observability/events.ts';\nimport {exitAfter, runUntilKilled} from '../services/life-cycle.ts';\nimport {MutagenService} from '../services/mutagen/mutagen.ts';\nimport {PusherService} from '../services/mutagen/pusher.ts';\nimport type {ReplicaState} from '../services/replicator/replicator.ts';\nimport {\n type ConnectionContextManager,\n ConnectionContextManagerImpl,\n} from '../services/view-syncer/connection-context-manager.ts';\nimport type {DrainCoordinator} from '../services/view-syncer/drain-coordinator.ts';\nimport {PipelineDriver} from '../services/view-syncer/pipeline-driver.ts';\nimport {SnapshotRowCache} from '../services/view-syncer/snapshot-row-cache.ts';\nimport {Snapshotter} from '../services/view-syncer/snapshotter.ts';\nimport {ViewSyncerService} from '../services/view-syncer/view-syncer.ts';\nimport {ProtocolErrorWithLevel} from '../types/error-with-level.ts';\nimport {connectPgClient} from '../types/pg.ts';\nimport {\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {installProfileHandler} from '../types/profiler.ts';\nimport {getShardID} from '../types/shards.ts';\nimport type {Subscription} from '../types/subscription.ts';\nimport {replicaFileModeSchema, replicaFileName} from '../workers/replicator.ts';\nimport {Syncer} from '../workers/syncer.ts';\nimport {startAnonymousTelemetry} from './anonymous-otel-start.ts';\nimport {InspectorDelegate} from './inspector-delegate.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\nimport {isPriorityOpRunning, runPriorityOp} from './priority-op.ts';\n\nfunction randomID() {\n return randInt(1, Number.MAX_SAFE_INTEGER).toString(36);\n}\n\nfunction getCustomQueryConfig(\n config: Pick<NormalizedZeroConfig, 'query' | 'getQueries'>,\n) {\n const queryConfig = config.query?.url ? config.query : config.getQueries;\n\n if (!queryConfig?.url) {\n return undefined;\n }\n\n return {\n url: queryConfig.url,\n apiKey: queryConfig.apiKey,\n allowedClientHeaders: queryConfig.allowedClientHeaders,\n allowedRequestHeaders: queryConfig.allowedRequestHeaders,\n forwardCookies: queryConfig.forwardCookies ?? false,\n };\n}\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 ...args: string[]\n): Promise<void> {\n assert(args.length >= 2, `expected [fileMode, workerIndex, ...flags]`);\n const fileMode = v.parse(args[0], replicaFileModeSchema);\n const workerIndex = Number(args[1]);\n installProfileHandler(parent, `syncer-${workerIndex}`, workerIndex);\n const config = getNormalizedZeroConfig({env, argv: args.slice(2)});\n\n startOtelAuto(\n createLogContext(config, 'syncer', workerIndex, false),\n 'syncer',\n workerIndex,\n );\n lc = createLogContext(config, 'syncer', workerIndex);\n initEventSink(lc, config);\n\n const {cvr, upstream, enableCrudMutations} = config;\n\n const replicaFile = replicaFileName(config.replica.file, fileMode);\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: 'syncer replica',\n dbPath: replicaFile,\n },\n config.sqliteCorruptionChecks,\n );\n lc.debug?.(`running view-syncer on ${replicaFile}`);\n\n const cvrDB = await connectPgClient(lc, cvr.db, `sync-worker-${pid}-cvr`, {\n max: must(cvr.maxConnsPerWorker, 'cvr.maxConnsPerWorker must be set'),\n });\n\n const upstreamDB =\n enableCrudMutations && upstream.type === 'pg'\n ? await connectPgClient(lc, upstream.db, `sync-worker-${pid}-upstream`, {\n max: must(\n upstream.maxConnsPerWorker,\n 'upstream.maxConnsPerWorker must be set',\n ),\n })\n : undefined;\n\n const dbWarmup = Promise.allSettled([\n warmupConnections(lc, cvrDB, 'cvr'),\n upstreamDB ? warmupConnections(lc, upstreamDB, 'upstream') : promiseVoid,\n ]);\n\n const tmpDir = config.storageDBTmpDir ?? tmpdir();\n const operatorStorage = DatabaseStorage.create(\n lc,\n path.join(tmpDir, `sync-worker-${randomUUID()}`),\n );\n const writeAuthzStorage = DatabaseStorage.create(\n lc,\n path.join(tmpDir, `mutagen-${randomUUID()}`),\n );\n\n const shard = getShardID(config);\n const customQueryConfig = getCustomQueryConfig(config);\n const pushConfig =\n config.push.url === undefined && config.mutate.url === undefined\n ? undefined\n : {\n ...config.push,\n ...config.mutate,\n url: must(\n config.push.url ?? config.mutate.url,\n 'No push or mutate URL configured',\n ),\n };\n\n /** @deprecated used in JWT validation */\n let validateLegacyJWT: ValidateLegacyJWT | undefined = undefined;\n\n const tokenOptions = tokenConfigOptions(config.auth ?? {});\n if (tokenOptions.length === 1) {\n validateLegacyJWT = async (token, {userID}) => {\n if (!userID) {\n throw new ProtocolErrorWithLevel(\n {\n kind: 'Unauthorized',\n message: 'UserID is required for JWT validation.',\n origin: 'zeroCache',\n },\n 'warn',\n );\n }\n\n const decoded = await verifyToken(config.auth, token, {\n subject: userID,\n ...(config.auth?.issuer && {issuer: config.auth.issuer}),\n ...(config.auth?.audience && {\n audience: config.auth.audience,\n }),\n });\n return {\n type: 'jwt',\n raw: token,\n decoded,\n };\n };\n }\n\n // Shared by all of the view-syncers on this worker so that the row reads\n // performed when advancing their pipelines are done once per worker rather\n // than once per client group.\n const snapshotRowCache =\n config.snapshotRowCacheSize > 0\n ? new SnapshotRowCache(config.snapshotRowCacheSize)\n : undefined;\n\n const viewSyncerFactory = (\n id: string,\n sub: Subscription<ReplicaState>,\n drainCoordinator: DrainCoordinator,\n ) => {\n const logger = lc\n .withContext('taskID', config.taskID)\n .withContext('component', 'view-syncer')\n .withContext('appID', shard.appID)\n .withContext('shardNum', shard.shardNum)\n .withContext('clientGroupID', id)\n .withContext('instance', randomID());\n\n const customQueryTransformer =\n customQueryConfig && new CustomQueryTransformer(logger, shard);\n const connContextManager = new ConnectionContextManagerImpl(\n logger,\n config.auth.revalidateIntervalSeconds,\n config.auth.retransformIntervalSeconds,\n customQueryConfig,\n pushConfig,\n validateLegacyJWT,\n );\n\n lc.debug?.(\n `creating view syncer. Query Planner Enabled: ${config.enableQueryPlanner}`,\n );\n\n const inspectorDelegate = new InspectorDelegate(customQueryTransformer);\n\n const priorityOpRunningYieldThresholdMs = Math.max(\n config.yieldThresholdMs / 4,\n 2,\n );\n const normalYieldThresholdMs = Math.max(config.yieldThresholdMs, 2);\n\n return new ViewSyncerService(\n config,\n logger,\n shard,\n config.taskID,\n id,\n cvrDB,\n new PipelineDriver(\n logger,\n config.log,\n new Snapshotter(\n logger,\n replicaFile,\n shard,\n undefined,\n snapshotRowCache,\n ),\n shard,\n operatorStorage.createClientGroupStorage(id),\n id,\n inspectorDelegate,\n () =>\n isPriorityOpRunning()\n ? priorityOpRunningYieldThresholdMs\n : normalYieldThresholdMs,\n config.enableQueryPlanner,\n config,\n ),\n sub,\n drainCoordinator,\n config.log.slowHydrateThreshold,\n inspectorDelegate,\n connContextManager,\n customQueryTransformer,\n runPriorityOp,\n );\n };\n\n const mutagenFactory = upstreamDB\n ? (id: string) =>\n new MutagenService(\n lc\n .withContext('component', 'mutagen')\n .withContext('clientGroupID', id),\n shard,\n id,\n upstreamDB,\n config,\n writeAuthzStorage,\n )\n : undefined;\n\n const pusherFactory =\n pushConfig === undefined\n ? undefined\n : (id: string, connContextManager: ConnectionContextManager) =>\n new PusherService(\n config,\n lc.withContext('clientGroupID', id),\n id,\n connContextManager,\n );\n\n const syncer = new Syncer(\n lc,\n config,\n viewSyncerFactory,\n mutagenFactory,\n pusherFactory,\n parent,\n validateLegacyJWT,\n );\n\n startAnonymousTelemetry(lc, config);\n\n void dbWarmup.then(() => parent.send(['ready', {ready: true}]));\n\n return runUntilKilled(lc, parent, syncer);\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(\n () => lc,\n () => runWorker(must(parentWorker), process.env, ...process.argv.slice(2)),\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,oDAAoD,CAAC;AAC3F,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,mCAAmC,CAAC;AAC3D,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,6CAA6C,CAAC;AAC9E,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,kDAAkD,CAAC;AAWxF,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAQjE,wBAAsB,YAAY,CAChC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,oBAAoB,EAC5B,YAAY,EAAE,YAAY,EAC1B,GAAG,EAAE,GAAG,EACR,UAAU,UAAO,EACjB,UAAU,UAAQ,EAClB,WAAW,CAAC,EAAE,iBAAiB,EAC/B,IAAI,CAAC,EAAE,OAAO,EACd,SAAS,UAAQ,GAChB,OAAO,CAAC,kBAAkB,CAAC,CA6F7B"}
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,oDAAoD,CAAC;AAC3F,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,mCAAmC,CAAC;AAC3D,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,6CAA6C,CAAC;AAC9E,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,kDAAkD,CAAC;AAWxF,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAQjE,wBAAsB,YAAY,CAChC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,oBAAoB,EAC5B,YAAY,EAAE,YAAY,EAC1B,GAAG,EAAE,GAAG,EACR,UAAU,UAAO,EACjB,UAAU,UAAQ,EAClB,WAAW,CAAC,EAAE,iBAAiB,EAC/B,IAAI,CAAC,EAAE,OAAO,EACd,SAAS,UAAQ,GAChB,OAAO,CAAC,kBAAkB,CAAC,CA4F7B"}
@@ -1,3 +1,4 @@
1
+ import { getOrInsertComputed } from "../../../shared/src/map.js";
1
2
  import { MemoryStorage } from "../../../zql/src/ivm/memory-storage.js";
2
3
  import { AccumulatorDebugger, serializePlanDebugEvents } from "../../../zql/src/planner/planner-debug.js";
3
4
  import { Debug } from "../../../zql/src/builder/debug-delegate.js";
@@ -38,14 +39,14 @@ async function analyzeQuery(lc, config, clientSchema, ast, syncedRows = true, ve
38
39
  host: {
39
40
  debug: new Debug(vendedRows, MAX_ANALYZE_ROWS),
40
41
  enableNotExists: true,
42
+ disableCorrelatedPredicatePushdown: config.enableCorrelatedPredicatePushdown === false,
43
+ enablePlannerAwarePushdown: config.enablePlannerAwarePushdown !== false,
41
44
  getSource(tableName) {
42
- let source = tables.get(tableName);
43
- if (source) return source;
44
- const tableSpec = mustGetTableSpec(tableSpecs, tableName);
45
- const { primaryKey } = tableSpec.tableSpec;
46
- source = new TableSource(lc, config.log, db, tableName, tableSpec.zqlSpec, primaryKey, shouldYield);
47
- tables.set(tableName, source);
48
- return source;
45
+ return getOrInsertComputed(tables, tableName, (tableName) => {
46
+ const tableSpec = mustGetTableSpec(tableSpecs, tableName);
47
+ const { primaryKey } = tableSpec.tableSpec;
48
+ return new TableSource(lc, config.log, db, tableName, tableSpec.zqlSpec, primaryKey, shouldYield);
49
+ });
49
50
  },
50
51
  createStorage() {
51
52
  return new MemoryStorage();
@@ -1 +1 @@
1
- {"version":3,"file":"analyze.js","names":[],"sources":["../../../../../zero-cache/src/services/analyze.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {AnalyzeQueryResult} from '../../../zero-protocol/src/analyze-query-result.ts';\nimport type {AST} from '../../../zero-protocol/src/ast.ts';\nimport type {ClientSchema} from '../../../zero-protocol/src/client-schema.ts';\nimport type {PermissionsConfig} from '../../../zero-schema/src/compiled-permissions.ts';\nimport {Debug} from '../../../zql/src/builder/debug-delegate.ts';\nimport {MemoryStorage} from '../../../zql/src/ivm/memory-storage.ts';\nimport {\n AccumulatorDebugger,\n serializePlanDebugEvents,\n} from '../../../zql/src/planner/planner-debug.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport {explainQueries} from '../../../zqlite/src/explain-queries.ts';\nimport {createSQLiteCostModel} from '../../../zqlite/src/sqlite-cost-model.ts';\nimport {TableSource} from '../../../zqlite/src/table-source.ts';\nimport type {JWTAuth} from '../auth/auth.ts';\nimport type {NormalizedZeroConfig} from '../config/normalize.ts';\nimport {computeZqlSpecs, mustGetTableSpec} from '../db/lite-tables.ts';\nimport type {LiteAndZqlSpec, LiteTableSpec} from '../db/specs.ts';\nimport {MAX_ANALYZE_ROWS, runAst} from './run-ast.ts';\nimport {TimeSliceTimer} from './view-syncer/view-syncer.ts';\n\nconst TIME_SLICE_LAP_THRESHOLD_MS = 200;\n\nexport async function analyzeQuery(\n lc: LogContext,\n config: NormalizedZeroConfig,\n clientSchema: ClientSchema,\n ast: AST,\n syncedRows = true,\n vendedRows = false,\n permissions?: PermissionsConfig,\n auth?: JWTAuth,\n joinPlans = false,\n): Promise<AnalyzeQueryResult> {\n using db = new Database(lc, config.replica.file);\n const fullTables = new Map<string, LiteTableSpec>();\n const tableSpecs = new Map<string, LiteAndZqlSpec>();\n const tables = new Map<string, TableSource>();\n\n computeZqlSpecs(\n lc,\n db,\n {includeBackfillingColumns: false},\n tableSpecs,\n fullTables,\n );\n\n // Mirror production: the planner runs iff ZERO_ENABLE_QUERY_PLANNER is set\n // on the server, so the analysis reflects what actually executes. Diagnostic\n // event collection is orthogonal and opt-in via `joinPlans`.\n const costModel = config.enableQueryPlanner\n ? createSQLiteCostModel(db, tableSpecs)\n : undefined;\n const planDebugger = joinPlans ? new AccumulatorDebugger() : undefined;\n const timer = await new TimeSliceTimer(lc).start();\n const shouldYield = () => timer.elapsedLap() > TIME_SLICE_LAP_THRESHOLD_MS;\n const yieldProcess = () => timer.yieldProcess();\n const result = await runAst(\n lc,\n clientSchema,\n ast,\n true,\n {\n applyPermissions: permissions !== undefined,\n syncedRows,\n vendedRows,\n auth,\n db,\n tableSpecs,\n permissions,\n costModel,\n planDebugger,\n maxSyncedRowsPerTable: MAX_ANALYZE_ROWS,\n host: {\n debug: new Debug(vendedRows, MAX_ANALYZE_ROWS),\n enableNotExists: true,\n getSource(tableName: string) {\n let source = tables.get(tableName);\n if (source) {\n return source;\n }\n\n const tableSpec = mustGetTableSpec(tableSpecs, tableName);\n const {primaryKey} = tableSpec.tableSpec;\n\n source = new TableSource(\n lc,\n config.log,\n db,\n tableName,\n tableSpec.zqlSpec,\n primaryKey,\n shouldYield,\n );\n tables.set(tableName, source);\n return source;\n },\n createStorage() {\n return new MemoryStorage();\n },\n decorateSourceInput: input => input,\n decorateInput: input => input,\n addEdge() {},\n decorateFilterInput: input => input,\n },\n },\n yieldProcess,\n );\n\n // Fill in plans for any queries SQLite did not actually execute (and thus\n // did not populate scanStatus EXPLAIN for) using the substituted-binding\n // fallback. Plans captured at execution time use the real bindings and win.\n const fallback = explainQueries(result.readRowCountsByQuery ?? {}, db);\n const captured = result.sqlitePlans ?? {};\n for (const [query, plan] of Object.entries(fallback)) {\n if (!captured[query]) {\n captured[query] = plan;\n }\n }\n result.sqlitePlans = captured;\n\n if (planDebugger) {\n result.joinPlans = serializePlanDebugEvents(planDebugger.events);\n }\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;;AAsBA,IAAM,8BAA8B;AAEpC,eAAsB,aACpB,IACA,QACA,cACA,KACA,aAAa,MACb,aAAa,OACb,aACA,MACA,YAAY,OACiB;;;EAC7B,MAAM,KAAA,YAAA,EAAK,IAAI,SAAS,IAAI,OAAO,QAAQ,IAAI,CAAA;EAC/C,MAAM,6BAAa,IAAI,IAA2B;EAClD,MAAM,6BAAa,IAAI,IAA4B;EACnD,MAAM,yBAAS,IAAI,IAAyB;EAE5C,gBACE,IACA,IACA,EAAC,2BAA2B,MAAK,GACjC,YACA,UACF;EAKA,MAAM,YAAY,OAAO,qBACrB,sBAAsB,IAAI,UAAU,IACpC,KAAA;EACJ,MAAM,eAAe,YAAY,IAAI,oBAAoB,IAAI,KAAA;EAC7D,MAAM,QAAQ,MAAM,IAAI,eAAe,EAAE,EAAE,MAAM;EACjD,MAAM,oBAAoB,MAAM,WAAW,IAAI;EAC/C,MAAM,qBAAqB,MAAM,aAAa;EAC9C,MAAM,SAAS,MAAM,OACnB,IACA,cACA,KACA,MACA;GACE,kBAAkB,gBAAgB,KAAA;GAClC;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,uBAAuB;GACvB,MAAM;IACJ,OAAO,IAAI,MAAM,YAAY,gBAAgB;IAC7C,iBAAiB;IACjB,UAAU,WAAmB;KAC3B,IAAI,SAAS,OAAO,IAAI,SAAS;KACjC,IAAI,QACF,OAAO;KAGT,MAAM,YAAY,iBAAiB,YAAY,SAAS;KACxD,MAAM,EAAC,eAAc,UAAU;KAE/B,SAAS,IAAI,YACX,IACA,OAAO,KACP,IACA,WACA,UAAU,SACV,YACA,WACF;KACA,OAAO,IAAI,WAAW,MAAM;KAC5B,OAAO;IACT;IACA,gBAAgB;KACd,OAAO,IAAI,cAAc;IAC3B;IACA,sBAAqB,UAAS;IAC9B,gBAAe,UAAS;IACxB,UAAU,CAAC;IACX,sBAAqB,UAAS;GAChC;EACF,GACA,YACF;EAKA,MAAM,WAAW,eAAe,OAAO,wBAAwB,CAAC,GAAG,EAAE;EACrE,MAAM,WAAW,OAAO,eAAe,CAAC;EACxC,KAAK,MAAM,CAAC,OAAO,SAAS,OAAO,QAAQ,QAAQ,GACjD,IAAI,CAAC,SAAS,QACZ,SAAS,SAAS;EAGtB,OAAO,cAAc;EAErB,IAAI,cACF,OAAO,YAAY,yBAAyB,aAAa,MAAM;EAGjE,OAAO;;;;;;AACT"}
1
+ {"version":3,"file":"analyze.js","names":[],"sources":["../../../../../zero-cache/src/services/analyze.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {getOrInsertComputed} from '../../../shared/src/map.ts';\nimport type {AnalyzeQueryResult} from '../../../zero-protocol/src/analyze-query-result.ts';\nimport type {AST} from '../../../zero-protocol/src/ast.ts';\nimport type {ClientSchema} from '../../../zero-protocol/src/client-schema.ts';\nimport type {PermissionsConfig} from '../../../zero-schema/src/compiled-permissions.ts';\nimport {Debug} from '../../../zql/src/builder/debug-delegate.ts';\nimport {MemoryStorage} from '../../../zql/src/ivm/memory-storage.ts';\nimport {\n AccumulatorDebugger,\n serializePlanDebugEvents,\n} from '../../../zql/src/planner/planner-debug.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport {explainQueries} from '../../../zqlite/src/explain-queries.ts';\nimport {createSQLiteCostModel} from '../../../zqlite/src/sqlite-cost-model.ts';\nimport {TableSource} from '../../../zqlite/src/table-source.ts';\nimport type {JWTAuth} from '../auth/auth.ts';\nimport type {NormalizedZeroConfig} from '../config/normalize.ts';\nimport {computeZqlSpecs, mustGetTableSpec} from '../db/lite-tables.ts';\nimport type {LiteAndZqlSpec, LiteTableSpec} from '../db/specs.ts';\nimport {MAX_ANALYZE_ROWS, runAst} from './run-ast.ts';\nimport {TimeSliceTimer} from './view-syncer/view-syncer.ts';\n\nconst TIME_SLICE_LAP_THRESHOLD_MS = 200;\n\nexport async function analyzeQuery(\n lc: LogContext,\n config: NormalizedZeroConfig,\n clientSchema: ClientSchema,\n ast: AST,\n syncedRows = true,\n vendedRows = false,\n permissions?: PermissionsConfig,\n auth?: JWTAuth,\n joinPlans = false,\n): Promise<AnalyzeQueryResult> {\n using db = new Database(lc, config.replica.file);\n const fullTables = new Map<string, LiteTableSpec>();\n const tableSpecs = new Map<string, LiteAndZqlSpec>();\n const tables = new Map<string, TableSource>();\n\n computeZqlSpecs(\n lc,\n db,\n {includeBackfillingColumns: false},\n tableSpecs,\n fullTables,\n );\n\n // Mirror production: the planner runs iff ZERO_ENABLE_QUERY_PLANNER is set\n // on the server, so the analysis reflects what actually executes. Diagnostic\n // event collection is orthogonal and opt-in via `joinPlans`.\n const costModel = config.enableQueryPlanner\n ? createSQLiteCostModel(db, tableSpecs)\n : undefined;\n const planDebugger = joinPlans ? new AccumulatorDebugger() : undefined;\n const timer = await new TimeSliceTimer(lc).start();\n const shouldYield = () => timer.elapsedLap() > TIME_SLICE_LAP_THRESHOLD_MS;\n const yieldProcess = () => timer.yieldProcess();\n const result = await runAst(\n lc,\n clientSchema,\n ast,\n true,\n {\n applyPermissions: permissions !== undefined,\n syncedRows,\n vendedRows,\n auth,\n db,\n tableSpecs,\n permissions,\n costModel,\n planDebugger,\n maxSyncedRowsPerTable: MAX_ANALYZE_ROWS,\n host: {\n debug: new Debug(vendedRows, MAX_ANALYZE_ROWS),\n enableNotExists: true,\n // Mirror production, as with the planner above.\n disableCorrelatedPredicatePushdown:\n config.enableCorrelatedPredicatePushdown === false,\n enablePlannerAwarePushdown: config.enablePlannerAwarePushdown !== false,\n getSource(tableName: string) {\n return getOrInsertComputed(tables, tableName, tableName => {\n const tableSpec = mustGetTableSpec(tableSpecs, tableName);\n const {primaryKey} = tableSpec.tableSpec;\n\n return new TableSource(\n lc,\n config.log,\n db,\n tableName,\n tableSpec.zqlSpec,\n primaryKey,\n shouldYield,\n );\n });\n },\n createStorage() {\n return new MemoryStorage();\n },\n decorateSourceInput: input => input,\n decorateInput: input => input,\n addEdge() {},\n decorateFilterInput: input => input,\n },\n },\n yieldProcess,\n );\n\n // Fill in plans for any queries SQLite did not actually execute (and thus\n // did not populate scanStatus EXPLAIN for) using the substituted-binding\n // fallback. Plans captured at execution time use the real bindings and win.\n const fallback = explainQueries(result.readRowCountsByQuery ?? {}, db);\n const captured = result.sqlitePlans ?? {};\n for (const [query, plan] of Object.entries(fallback)) {\n if (!captured[query]) {\n captured[query] = plan;\n }\n }\n result.sqlitePlans = captured;\n\n if (planDebugger) {\n result.joinPlans = serializePlanDebugEvents(planDebugger.events);\n }\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;AAuBA,IAAM,8BAA8B;AAEpC,eAAsB,aACpB,IACA,QACA,cACA,KACA,aAAa,MACb,aAAa,OACb,aACA,MACA,YAAY,OACiB;;;EAC7B,MAAM,KAAA,YAAA,EAAK,IAAI,SAAS,IAAI,OAAO,QAAQ,IAAI,CAAA;EAC/C,MAAM,6BAAa,IAAI,IAA2B;EAClD,MAAM,6BAAa,IAAI,IAA4B;EACnD,MAAM,yBAAS,IAAI,IAAyB;EAE5C,gBACE,IACA,IACA,EAAC,2BAA2B,MAAK,GACjC,YACA,UACF;EAKA,MAAM,YAAY,OAAO,qBACrB,sBAAsB,IAAI,UAAU,IACpC,KAAA;EACJ,MAAM,eAAe,YAAY,IAAI,oBAAoB,IAAI,KAAA;EAC7D,MAAM,QAAQ,MAAM,IAAI,eAAe,EAAE,EAAE,MAAM;EACjD,MAAM,oBAAoB,MAAM,WAAW,IAAI;EAC/C,MAAM,qBAAqB,MAAM,aAAa;EAC9C,MAAM,SAAS,MAAM,OACnB,IACA,cACA,KACA,MACA;GACE,kBAAkB,gBAAgB,KAAA;GAClC;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,uBAAuB;GACvB,MAAM;IACJ,OAAO,IAAI,MAAM,YAAY,gBAAgB;IAC7C,iBAAiB;IAEjB,oCACE,OAAO,sCAAsC;IAC/C,4BAA4B,OAAO,+BAA+B;IAClE,UAAU,WAAmB;KAC3B,OAAO,oBAAoB,QAAQ,YAAW,cAAa;MACzD,MAAM,YAAY,iBAAiB,YAAY,SAAS;MACxD,MAAM,EAAC,eAAc,UAAU;MAE/B,OAAO,IAAI,YACT,IACA,OAAO,KACP,IACA,WACA,UAAU,SACV,YACA,WACF;KACF,CAAC;IACH;IACA,gBAAgB;KACd,OAAO,IAAI,cAAc;IAC3B;IACA,sBAAqB,UAAS;IAC9B,gBAAe,UAAS;IACxB,UAAU,CAAC;IACX,sBAAqB,UAAS;GAChC;EACF,GACA,YACF;EAKA,MAAM,WAAW,eAAe,OAAO,wBAAwB,CAAC,GAAG,EAAE;EACrE,MAAM,WAAW,OAAO,eAAe,CAAC;EACxC,KAAK,MAAM,CAAC,OAAO,SAAS,OAAO,QAAQ,QAAQ,GACjD,IAAI,CAAC,SAAS,QACZ,SAAS,SAAS;EAGtB,OAAO,cAAc;EAErB,IAAI,cACF,OAAO,YAAY,yBAAyB,aAAa,MAAM;EAGjE,OAAO;;;;;;AACT"}
@@ -38,7 +38,19 @@ export declare class PusherService implements Service, Pusher {
38
38
  }] | ["connected", {
39
39
  timestamp?: number | undefined;
40
40
  wsid: string;
41
- }] | import("../../../../zero-protocol/src/error.ts").ErrorMessage | ["pushResponse", {
41
+ }] | import("../../../../zero-protocol/src/error.ts").ErrorMessage | ["transformError", ({
42
+ message?: string | undefined;
43
+ details?: import("../../../../shared/src/json.ts").ReadonlyJSONValue | undefined;
44
+ error: "app";
45
+ id: string;
46
+ name: string;
47
+ } | {
48
+ details?: import("../../../../shared/src/json.ts").ReadonlyJSONValue | undefined;
49
+ error: "parse";
50
+ id: string;
51
+ name: string;
52
+ message: string;
53
+ })[]] | ["pushResponse", {
42
54
  mutationIDs?: {
43
55
  id: number;
44
56
  clientID: string;
@@ -82,19 +94,7 @@ export declare class PusherService implements Service, Pusher {
82
94
  data?: import("../../../../shared/src/json.ts").ReadonlyJSONValue | undefined;
83
95
  };
84
96
  }[];
85
- }] | ["transformError", ({
86
- message?: string | undefined;
87
- details?: import("../../../../shared/src/json.ts").ReadonlyJSONValue | undefined;
88
- error: "app";
89
- id: string;
90
- name: string;
91
- } | {
92
- details?: import("../../../../shared/src/json.ts").ReadonlyJSONValue | undefined;
93
- error: "parse";
94
- id: string;
95
- name: string;
96
- message: string;
97
- })[]] | ["inspect", {
97
+ }] | ["inspect", {
98
98
  id: string;
99
99
  op: "queries";
100
100
  value: {
@@ -216,7 +216,7 @@ export declare class PusherService implements Service, Pusher {
216
216
  ordering?: readonly (readonly [string, "desc" | "asc"])[] | undefined;
217
217
  joinType?: "semi" | "flipped" | "unflippable" | undefined;
218
218
  type: "node-cost";
219
- nodeType: "join" | "connection" | "fan-out" | "fan-in" | "terminus";
219
+ nodeType: "join" | "filter" | "connection" | "fan-out" | "fan-in" | "terminus";
220
220
  node: string;
221
221
  branchPattern: number[];
222
222
  downstreamChildSelectivity: number;
@@ -232,7 +232,7 @@ export declare class PusherService implements Service, Pusher {
232
232
  attemptNumber?: number | undefined;
233
233
  constraint?: Record<string, unknown> | null | undefined;
234
234
  type: "node-constraint";
235
- nodeType: "join" | "connection" | "fan-out" | "fan-in" | "terminus";
235
+ nodeType: "join" | "filter" | "connection" | "fan-out" | "fan-in" | "terminus";
236
236
  node: string;
237
237
  branchPattern: number[];
238
238
  from: string;
@@ -337,7 +337,19 @@ export declare class PusherService implements Service, Pusher {
337
337
  }] | ["connected", {
338
338
  timestamp?: number | undefined;
339
339
  wsid: string;
340
- }] | import("../../../../zero-protocol/src/error.ts").ErrorMessage | ["pushResponse", {
340
+ }] | import("../../../../zero-protocol/src/error.ts").ErrorMessage | ["transformError", ({
341
+ message?: string | undefined;
342
+ details?: import("../../../../shared/src/json.ts").ReadonlyJSONValue | undefined;
343
+ error: "app";
344
+ id: string;
345
+ name: string;
346
+ } | {
347
+ details?: import("../../../../shared/src/json.ts").ReadonlyJSONValue | undefined;
348
+ error: "parse";
349
+ id: string;
350
+ name: string;
351
+ message: string;
352
+ })[]] | ["pushResponse", {
341
353
  mutationIDs?: {
342
354
  id: number;
343
355
  clientID: string;
@@ -381,19 +393,7 @@ export declare class PusherService implements Service, Pusher {
381
393
  data?: import("../../../../shared/src/json.ts").ReadonlyJSONValue | undefined;
382
394
  };
383
395
  }[];
384
- }] | ["transformError", ({
385
- message?: string | undefined;
386
- details?: import("../../../../shared/src/json.ts").ReadonlyJSONValue | undefined;
387
- error: "app";
388
- id: string;
389
- name: string;
390
- } | {
391
- details?: import("../../../../shared/src/json.ts").ReadonlyJSONValue | undefined;
392
- error: "parse";
393
- id: string;
394
- name: string;
395
- message: string;
396
- })[]] | ["inspect", {
396
+ }] | ["inspect", {
397
397
  id: string;
398
398
  op: "queries";
399
399
  value: {
@@ -515,7 +515,7 @@ export declare class PusherService implements Service, Pusher {
515
515
  ordering?: readonly (readonly [string, "desc" | "asc"])[] | undefined;
516
516
  joinType?: "semi" | "flipped" | "unflippable" | undefined;
517
517
  type: "node-cost";
518
- nodeType: "join" | "connection" | "fan-out" | "fan-in" | "terminus";
518
+ nodeType: "join" | "filter" | "connection" | "fan-out" | "fan-in" | "terminus";
519
519
  node: string;
520
520
  branchPattern: number[];
521
521
  downstreamChildSelectivity: number;
@@ -531,7 +531,7 @@ export declare class PusherService implements Service, Pusher {
531
531
  attemptNumber?: number | undefined;
532
532
  constraint?: Record<string, unknown> | null | undefined;
533
533
  type: "node-constraint";
534
- nodeType: "join" | "connection" | "fan-out" | "fan-in" | "terminus";
534
+ nodeType: "join" | "filter" | "connection" | "fan-out" | "fan-in" | "terminus";
535
535
  node: string;
536
536
  branchPattern: number[];
537
537
  from: string;
@@ -1 +1 @@
1
- {"version":3,"file":"pusher.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pusher.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAOjD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,uCAAuC,CAAC;AAYtE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,8CAA8C,CAAC;AAG7E,OAAO,EAAC,KAAK,QAAQ,EAAC,MAAM,uCAAuC,CAAC;AAEpE,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,6BAA6B,CAAC;AAK5D,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAC,YAAY,EAAC,MAAM,6BAA6B,CAAC;AACzD,OAAO,KAAK,EAAC,aAAa,EAAE,YAAY,EAAC,MAAM,6BAA6B,CAAC;AAC7E,OAAO,KAAK,EAAC,iBAAiB,EAAE,OAAO,EAAC,MAAM,eAAe,CAAC;AAC9D,OAAO,KAAK,EACV,iBAAiB,EACjB,wBAAwB,EACxB,kBAAkB,EACnB,MAAM,8CAA8C,CAAC;AAEtD,MAAM,WAAW,MAAO,SAAQ,iBAAiB;IAC/C,cAAc,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IACjE,WAAW,CAAC,QAAQ,EAAE,kBAAkB,EAAE,IAAI,EAAE,QAAQ,GAAG,aAAa,CAAC;IACzE,oBAAoB,CAClB,SAAS,EAAE,kBAAkB,EAC7B,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,qBAAqB,CACnB,SAAS,EAAE,kBAAkB,EAC7B,SAAS,EAAE,MAAM,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,KAAK,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,GAAG,OAAO,CAAC,CAAC;AAEhD;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAc,YAAW,OAAO,EAAE,MAAM;;IACnD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;gBAWlB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,UAAU,EACd,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,wBAAwB;IAe9C,cAAc,CAAC,QAAQ,EAAE,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAI3C,WAAW,CACT,QAAQ,EAAE,kBAAkB,EAC5B,IAAI,EAAE,QAAQ,GACb,OAAO,CAAC,aAAa,EAAE,YAAY,CAAC;IAWjC,oBAAoB,CACxB,SAAS,EAAE,kBAAkB,EAC7B,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,IAAI,CAAC;IAgDhB;;;;;OAKG;IACG,qBAAqB,CACzB,SAAS,EAAE,kBAAkB,EAC7B,SAAS,EAAE,MAAM,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC;IAmDhB,GAAG;IAKH,KAAK;IAQL,OAAO,IAAI,OAAO;IAIlB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAKpB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAQtB;AAED,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,iBAAiB,CAAC;CAC5B,CAAC;AACF,KAAK,iBAAiB,GAAG,WAAW,GAAG,MAAM,CAAC;AAqW9C;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,CAAC,iBAAiB,GAAG,SAAS,CAAC,EAAE,GAClD,CAAC,WAAW,EAAE,EAAE,OAAO,CAAC,CAgC1B"}
@@ -1,4 +1,6 @@
1
1
  import { assert } from "../../../../shared/src/asserts.js";
2
+ import { getOrInsertComputed } from "../../../../shared/src/map.js";
3
+ import { groupBy, newArray } from "../../../../shared/src/arrays.js";
2
4
  import { must } from "../../../../shared/src/must.js";
3
5
  import { getErrorMessage } from "../../../../shared/src/error.js";
4
6
  import { PushFailed } from "../../../../zero-protocol/src/error-kind-enum.js";
@@ -8,7 +10,6 @@ import { isProtocolError } from "../../../../zero-protocol/src/error.js";
8
10
  import { Custom } from "../../../../zero-protocol/src/mutation-type-enum.js";
9
11
  import { CLEANUP_RESULTS_MUTATION_NAME } from "../../../../zero-protocol/src/mutation.js";
10
12
  import "../../../../zero-protocol/src/push.js";
11
- import { groupBy } from "../../../../shared/src/arrays.js";
12
13
  import "../../config/zero-config.js";
13
14
  import { mutateResponseSchema } from "../../../../zero-protocol/src/mutate-server.js";
14
15
  import { getOrCreateCounter } from "../../observability/metrics.js";
@@ -380,10 +381,7 @@ function combinePushes(entries) {
380
381
  }
381
382
  for (const entry of entries) {
382
383
  if (entry === "stop" || entry === void 0) return [collect(), true];
383
- const key = `${entry.connCtx.clientID}:${entry.connCtx.wsID}:${entry.connCtx.revision}`;
384
- const existing = pushesByConnection.get(key);
385
- if (existing) existing.push(entry);
386
- else pushesByConnection.set(key, [entry]);
384
+ getOrInsertComputed(pushesByConnection, `${entry.connCtx.clientID}:${entry.connCtx.wsID}:${entry.connCtx.revision}`, newArray).push(entry);
387
385
  }
388
386
  return [collect(), false];
389
387
  }