@rocicorp/zero 1.12.0-canary.0 → 1.12.0-canary.1

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 (28) hide show
  1. package/out/analyze-query/src/analyze-cli.js +2 -2
  2. package/out/analyze-query/src/analyze-cli.js.map +1 -1
  3. package/out/zero/package.js +1 -1
  4. package/out/zero/package.js.map +1 -1
  5. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  6. package/out/zero-cache/src/config/zero-config.js +9 -6
  7. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  8. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  9. package/out/zero-cache/src/server/change-streamer.js +14 -5
  10. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  11. package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
  12. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  13. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  14. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +7 -5
  15. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  16. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +11 -7
  17. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
  18. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +37 -30
  19. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  20. package/out/zero-cache/src/services/statz.d.ts.map +1 -1
  21. package/out/zero-cache/src/services/statz.js +1 -1
  22. package/out/zero-cache/src/services/statz.js.map +1 -1
  23. package/out/zero-cache/src/workers/connection.js +3 -3
  24. package/out/zero-cache/src/workers/connection.js.map +1 -1
  25. package/out/zero-cache/src/workers/syncer.js +2 -2
  26. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  27. package/out/zero-client/src/client/version.js +1 -1
  28. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"statz.js","names":[],"sources":["../../../../../zero-cache/src/services/statz.ts"],"sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport type {LogContext} from '@rocicorp/logger';\nimport auth from 'basic-auth';\nimport type {FastifyReply, FastifyRequest} from 'fastify';\nimport {BigIntJSON} from '../../../shared/src/bigint-json.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport type {NormalizedZeroConfig as ZeroConfig} from '../config/normalize.ts';\nimport {getOperatorAccess} from '../config/zero-config.ts';\nimport {StatementRunner} from '../db/statements.ts';\nimport {pgClient} from '../types/pg.ts';\nimport {getShardID, upstreamSchema} from '../types/shards.ts';\nimport {fromStateVersionString} from './change-source/pg/lsn.ts';\nimport {getReplicationState} from './replicator/schema/replication-state.ts';\n\nasync function upstreamStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config));\n const sql = pgClient(lc, config.upstream.db, 'statz-upstream');\n try {\n return await getPgStats([\n [\n 'numReplicas',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"replicas\"`,\n ],\n [\n 'numClientsWithMutations',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"clients\"`,\n ],\n [\n 'numMutationsProcessed',\n sql`SELECT SUM(\"lastMutationID\") as \"c\" FROM ${sql(schema)}.\"clients\"`,\n ],\n ]);\n } finally {\n await sql.end();\n }\n}\n\nasync function cvrStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config)) + '/cvr';\n const sql = pgClient(lc, config.cvr.db, 'statz-cvr');\n\n function numQueriesPerClientGroup(\n active: boolean,\n ): ReturnType<ReturnType<typeof pgClient>> {\n const filter = active\n ? sql`WHERE \"inactivatedAt\" IS NULL AND deleted = false`\n : sql`WHERE \"inactivatedAt\" IS NOT NULL AND (\"inactivatedAt\" + \"ttl\") > NOW()`;\n return sql`WITH\n group_counts AS (\n SELECT\n \"clientGroupID\",\n COUNT(*) AS num_queries\n FROM ${sql(schema)}.\"desires\"\n ${filter}\n GROUP BY \"clientGroupID\"\n ),\n -- Count distinct clientIDs per clientGroupID\n client_per_group_counts AS (\n SELECT\n \"clientGroupID\",\n COUNT(DISTINCT \"clientID\") AS num_clients\n FROM ${sql(schema)}.\"desires\"\n ${filter}\n GROUP BY \"clientGroupID\"\n )\n -- Combine all the information\n SELECT\n g.\"clientGroupID\",\n cpg.num_clients,\n g.num_queries\n FROM group_counts g\n JOIN client_per_group_counts cpg ON g.\"clientGroupID\" = cpg.\"clientGroupID\"\n ORDER BY g.num_queries DESC;`;\n }\n\n try {\n return await getPgStats([\n [\n 'totalNumQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\"`,\n ],\n [\n 'numUniqueQueryHashes',\n sql`SELECT COUNT(DISTINCT \"queryHash\") as \"c\" FROM ${sql(\n schema,\n )}.\"desires\"`,\n ],\n [\n 'numActiveQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"inactivatedAt\" IS NULL AND \"deleted\" = false`,\n ],\n [\n 'numInactiveQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"inactivatedAt\" IS NOT NULL AND (\"inactivatedAt\" + \"ttl\") > NOW()`,\n ],\n [\n 'numDeletedQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"deleted\" = true`,\n ],\n [\n 'freshQueriesPercentiles',\n sql`WITH client_group_counts AS (\n -- Count inactive desires per clientGroupID\n SELECT\n \"clientGroupID\",\n COUNT(*) AS fresh_count\n FROM ${sql(schema)}.\"desires\"\n WHERE\n (\"inactivatedAt\" IS NOT NULL\n AND (\"inactivatedAt\" + \"ttl\") > NOW()) OR (\"inactivatedAt\" IS NULL\n AND deleted = false)\n GROUP BY \"clientGroupID\"\n )\n\n SELECT\n percentile_cont(0.50) WITHIN GROUP (ORDER BY fresh_count) AS \"p50\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY fresh_count) AS \"p75\",\n percentile_cont(0.90) WITHIN GROUP (ORDER BY fresh_count) AS \"p90\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY fresh_count) AS \"p95\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY fresh_count) AS \"p99\",\n MIN(fresh_count) AS \"min\",\n MAX(fresh_count) AS \"max\",\n AVG(fresh_count) AS \"avg\"\n FROM client_group_counts;`,\n ],\n [\n 'rowsPerClientGroupPercentiles',\n sql`WITH client_group_counts AS (\n -- Count inactive desires per clientGroupID\n SELECT\n \"clientGroupID\",\n COUNT(*) AS row_count\n FROM ${sql(schema)}.\"rows\"\n GROUP BY \"clientGroupID\"\n )\n SELECT\n percentile_cont(0.50) WITHIN GROUP (ORDER BY row_count) AS \"p50\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY row_count) AS \"p75\",\n percentile_cont(0.90) WITHIN GROUP (ORDER BY row_count) AS \"p90\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY row_count) AS \"p95\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY row_count) AS \"p99\",\n MIN(row_count) AS \"min\",\n MAX(row_count) AS \"max\",\n AVG(row_count) AS \"avg\"\n FROM client_group_counts;`,\n ],\n [\n // check for AST blowup due to DNF conversion.\n 'astSizes',\n sql`SELECT\n percentile_cont(0.25) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"25th_percentile\",\n percentile_cont(0.5) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"50th_percentile\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"75th_percentile\",\n percentile_cont(0.9) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"90th_percentile\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"95th_percentile\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"99th_percentile\",\n MIN(length(\"clientAST\"::text)) AS \"minimum_length\",\n MAX(length(\"clientAST\"::text)) AS \"maximum_length\",\n AVG(length(\"clientAST\"::text))::integer AS \"average_length\",\n COUNT(*) AS \"total_records\"\n FROM ${sql(schema)}.\"queries\";`,\n ],\n [\n // output the hash of the largest AST\n 'biggestAstHash',\n sql`SELECT \"queryHash\", length(\"clientAST\"::text) AS \"ast_length\"\n FROM ${sql(schema)}.\"queries\"\n ORDER BY length(\"clientAST\"::text) DESC\n LIMIT 1;`,\n ],\n [\n 'totalActiveQueriesPerClientAndClientGroup',\n numQueriesPerClientGroup(true),\n ],\n [\n 'totalInactiveQueriesPerClientAndClientGroup',\n numQueriesPerClientGroup(false),\n ],\n [\n 'totalRowsPerClientGroup',\n sql`SELECT \"clientGroupID\", COUNT(*) as \"c\" FROM ${sql(\n schema,\n )}.\"rows\" GROUP BY \"clientGroupID\" ORDER BY \"c\" DESC`,\n ],\n [\n 'numRowsPerQuery',\n sql`SELECT\n k.key AS \"queryHash\",\n COUNT(*) AS row_count\n FROM ${sql(schema)}.\"rows\" r,\n LATERAL jsonb_each(r.\"refCounts\") k\n GROUP BY k.key\n ORDER BY row_count DESC;`,\n ],\n ] satisfies [\n name: string,\n query: ReturnType<ReturnType<typeof pgClient>>,\n ][]);\n } finally {\n await sql.end();\n }\n}\n\nasync function changeLogStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config)) + '/cdc';\n const sql = pgClient(lc, config.change.db, 'statz-change');\n\n try {\n return await getPgStats([\n [\n 'changeLogSize',\n sql`SELECT COUNT(*) as \"change_log_size\" FROM ${sql(schema)}.\"changeLog\"`,\n ],\n ]);\n } finally {\n await sql.end();\n }\n}\n\n// The replica is opened read-only: /statz is available to the operator\n// password, and must not change the replica. Among other things, a writable\n// connection would run `PRAGMA optimize` when closed.\nfunction replicaStats(lc: LogContext, config: ZeroConfig) {\n const db = new Database(lc, config.replica.file, {readonly: true});\n try {\n return Object.fromEntries([\n ['wal file sizes', walFileSizes(config.replica.file)],\n ['page count', pick(first(db.pragma('PAGE_COUNT')))],\n ['page size', pick(first(db.pragma('PAGE_SIZE')))],\n ['journal mode', pick(first(db.pragma('JOURNAL_MODE')))],\n ['synchronous', pick(first(db.pragma('SYNCHRONOUS')))],\n ['cache size', pick(first(db.pragma('CACHE_SIZE')))],\n ['auto vacuum', pick(first(db.pragma('AUTO_VACUUM')))],\n ['freelist count', pick(first(db.pragma('FREELIST_COUNT')))],\n ['wal autocheckpoint', pick(first(db.pragma('WAL_AUTOCHECKPOINT')))],\n ['db file stats', fs.statSync(config.replica.file)],\n ] as const);\n } finally {\n db.close();\n }\n}\n\nfunction replicationStats(lc: LogContext, config: ZeroConfig) {\n const db = new Database(lc, config.replica.file, {readonly: true});\n try {\n return getReplicationStats(db);\n } finally {\n db.close();\n }\n}\n\n/**\n * The sizes of the replica's WAL files, by suffix, read from the filesystem.\n * `PRAGMA wal_checkpoint` would report the WAL's frame counts, but it runs a\n * checkpoint to do so, and fails on a read-only connection in WAL mode.\n */\nfunction walFileSizes(file: string): Record<string, number> {\n const sizes: Record<string, number> = {};\n // `-wal` is used in WAL mode, and both files in WAL2 mode.\n for (const suffix of ['-wal', '-wal2']) {\n const stats = fs.statSync(file + suffix, {throwIfNoEntry: false});\n if (stats) {\n sizes[suffix] = stats.size;\n }\n }\n return sizes;\n}\n\nfunction getReplicationStats(db: Database) {\n const {stateVersion} = getReplicationState(new StatementRunner(db));\n const lsn = fromStateVersionString(stateVersion);\n return {lsn};\n}\n\nfunction osStats() {\n return Object.fromEntries([\n ['load avg', os.loadavg()],\n ['uptime', os.uptime()],\n ['total mem', os.totalmem()],\n ['free mem', os.freemem()],\n ['cpus', os.cpus().length],\n ['available parallelism', os.availableParallelism()],\n ['platform', os.platform()],\n ['arch', os.arch()],\n ['release', os.release()],\n ] as const);\n}\n\nasync function getPgStats(\n pendingQueries: [\n name: string,\n query: ReturnType<ReturnType<typeof pgClient>>,\n ][],\n) {\n const results = await Promise.all(\n pendingQueries.map(async ([name, query]) => [name, await query] as const),\n );\n return Object.fromEntries(results);\n}\n\ntype StatsObject = Record<string, unknown>;\n\nfunction printStats(group: string, statsObject: StatsObject): string {\n const lines: string[] = ['\\n' + header(group)];\n for (const [name, result] of Object.entries(statsObject)) {\n lines.push('\\n' + name + ': ' + BigIntJSON.stringify(result, null, 2));\n }\n lines.push('\\n');\n return lines.join('');\n}\n\n/**\n * Serves counts and sizes. This is available to the operator password as well\n * as the admin password (see `--operator-password`), so it must never return\n * application data: no row values, query ASTs or query arguments.\n *\n * HTTP query parameters:\n * * `group`: restricts the groups for which stats are computed\n * * `format=json`: returns the stats as a JSON object\n * * `pretty`: formats the JSON object with indentation\n */\nexport async function handleStatzRequest(\n lc: LogContext,\n config: ZeroConfig,\n req: FastifyRequest,\n res: FastifyReply,\n) {\n const credentials = auth(req);\n if (getOperatorAccess(lc, config, credentials?.pass) === undefined) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Statz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n const statsFns: Record<string, () => Promise<StatsObject> | StatsObject> = {\n upstream: () => upstreamStats(lc, config),\n cvr: () => cvrStats(lc, config),\n changeLog: () => changeLogStats(lc, config),\n replica: () => replicaStats(lc, config),\n replication: () => replicationStats(lc, config),\n os: () => osStats(),\n };\n\n async function computeStats(group: string): Promise<[string, StatsObject]> {\n try {\n return [group, await statsFns[group]()];\n } catch (e) {\n lc.error?.(`error computing ${group} stats`, e);\n return [group, {error: String(e)}];\n }\n }\n\n const query = req.query as Record<string, unknown>;\n const groups =\n typeof query.group === 'string'\n ? query.group.split(',')\n : Array.isArray(query.group)\n ? query.group\n : undefined;\n\n const stats = await Promise.all(\n groups\n ? groups.filter(g => g in statsFns).map(computeStats)\n : Object.keys(statsFns).map(computeStats),\n );\n\n if (query.format === 'json') {\n const indent = query.pretty !== undefined ? 2 : undefined;\n await res\n .header('Content-Type', 'application/json')\n .send(BigIntJSON.stringify(Object.fromEntries(stats), null, indent));\n return;\n } else {\n const body = stats\n .map(([group, statsObject]) => printStats(group, statsObject))\n .join('');\n await res.header('Content-Type', 'text/plain; charset=utf-8').send(body);\n }\n}\n\nfunction first(x: object[]): object {\n return x[0];\n}\n\nfunction pick(x: object): unknown {\n return Object.values(x)[0];\n}\n\nfunction header(name: string): string {\n return `=== ${name} ===\\n`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"statz.js","names":[],"sources":["../../../../../zero-cache/src/services/statz.ts"],"sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport type {LogContext} from '@rocicorp/logger';\nimport auth from 'basic-auth';\nimport type {FastifyReply, FastifyRequest} from 'fastify';\nimport {BigIntJSON} from '../../../shared/src/bigint-json.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport type {NormalizedZeroConfig as ZeroConfig} from '../config/normalize.ts';\nimport {getOperatorAccess} from '../config/zero-config.ts';\nimport {StatementRunner} from '../db/statements.ts';\nimport {pgClient} from '../types/pg.ts';\nimport {getShardID, upstreamSchema} from '../types/shards.ts';\nimport {fromStateVersionString} from './change-source/pg/lsn.ts';\nimport {getReplicationState} from './replicator/schema/replication-state.ts';\n\nasync function upstreamStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config));\n const sql = pgClient(lc, config.upstream.db, 'statz-upstream');\n try {\n return await getPgStats([\n [\n 'numReplicas',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"replicas\"`,\n ],\n [\n 'numClientsWithMutations',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"clients\"`,\n ],\n [\n 'numMutationsProcessed',\n sql`SELECT SUM(\"lastMutationID\") as \"c\" FROM ${sql(schema)}.\"clients\"`,\n ],\n ]);\n } finally {\n await sql.end();\n }\n}\n\nasync function cvrStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config)) + '/cvr';\n const sql = pgClient(lc, config.cvr.db, 'statz-cvr');\n\n function numQueriesPerClientGroup(\n active: boolean,\n ): ReturnType<ReturnType<typeof pgClient>> {\n const filter = active\n ? sql`WHERE \"inactivatedAt\" IS NULL AND deleted = false`\n : sql`WHERE \"inactivatedAt\" IS NOT NULL AND (\"inactivatedAt\" + \"ttl\") > NOW()`;\n return sql`WITH\n group_counts AS (\n SELECT\n \"clientGroupID\",\n COUNT(*) AS num_queries\n FROM ${sql(schema)}.\"desires\"\n ${filter}\n GROUP BY \"clientGroupID\"\n ),\n -- Count distinct clientIDs per clientGroupID\n client_per_group_counts AS (\n SELECT\n \"clientGroupID\",\n COUNT(DISTINCT \"clientID\") AS num_clients\n FROM ${sql(schema)}.\"desires\"\n ${filter}\n GROUP BY \"clientGroupID\"\n )\n -- Combine all the information\n SELECT\n g.\"clientGroupID\",\n cpg.num_clients,\n g.num_queries\n FROM group_counts g\n JOIN client_per_group_counts cpg ON g.\"clientGroupID\" = cpg.\"clientGroupID\"\n ORDER BY g.num_queries DESC;`;\n }\n\n try {\n return await getPgStats([\n [\n 'totalNumQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\"`,\n ],\n [\n 'numUniqueQueryHashes',\n sql`SELECT COUNT(DISTINCT \"queryHash\") as \"c\" FROM ${sql(\n schema,\n )}.\"desires\"`,\n ],\n [\n 'numActiveQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"inactivatedAt\" IS NULL AND \"deleted\" = false`,\n ],\n [\n 'numInactiveQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"inactivatedAt\" IS NOT NULL AND (\"inactivatedAt\" + \"ttl\") > NOW()`,\n ],\n [\n 'numDeletedQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"deleted\" = true`,\n ],\n [\n 'freshQueriesPercentiles',\n sql`WITH client_group_counts AS (\n -- Count inactive desires per clientGroupID\n SELECT\n \"clientGroupID\",\n COUNT(*) AS fresh_count\n FROM ${sql(schema)}.\"desires\"\n WHERE\n (\"inactivatedAt\" IS NOT NULL\n AND (\"inactivatedAt\" + \"ttl\") > NOW()) OR (\"inactivatedAt\" IS NULL\n AND deleted = false)\n GROUP BY \"clientGroupID\"\n )\n\n SELECT\n percentile_cont(0.50) WITHIN GROUP (ORDER BY fresh_count) AS \"p50\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY fresh_count) AS \"p75\",\n percentile_cont(0.90) WITHIN GROUP (ORDER BY fresh_count) AS \"p90\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY fresh_count) AS \"p95\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY fresh_count) AS \"p99\",\n MIN(fresh_count) AS \"min\",\n MAX(fresh_count) AS \"max\",\n AVG(fresh_count) AS \"avg\"\n FROM client_group_counts;`,\n ],\n [\n 'rowsPerClientGroupPercentiles',\n sql`WITH client_group_counts AS (\n -- Count inactive desires per clientGroupID\n SELECT\n \"clientGroupID\",\n COUNT(*) AS row_count\n FROM ${sql(schema)}.\"rows\"\n GROUP BY \"clientGroupID\"\n )\n SELECT\n percentile_cont(0.50) WITHIN GROUP (ORDER BY row_count) AS \"p50\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY row_count) AS \"p75\",\n percentile_cont(0.90) WITHIN GROUP (ORDER BY row_count) AS \"p90\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY row_count) AS \"p95\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY row_count) AS \"p99\",\n MIN(row_count) AS \"min\",\n MAX(row_count) AS \"max\",\n AVG(row_count) AS \"avg\"\n FROM client_group_counts;`,\n ],\n [\n // check for AST blowup due to DNF conversion.\n 'astSizes',\n sql`SELECT\n percentile_cont(0.25) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"25th_percentile\",\n percentile_cont(0.5) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"50th_percentile\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"75th_percentile\",\n percentile_cont(0.9) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"90th_percentile\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"95th_percentile\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"99th_percentile\",\n MIN(length(\"clientAST\"::text)) AS \"minimum_length\",\n MAX(length(\"clientAST\"::text)) AS \"maximum_length\",\n AVG(length(\"clientAST\"::text))::integer AS \"average_length\",\n COUNT(*) AS \"total_records\"\n FROM ${sql(schema)}.\"queries\";`,\n ],\n [\n // output the hash of the largest AST\n 'biggestAstHash',\n sql`SELECT \"queryHash\", length(\"clientAST\"::text) AS \"ast_length\"\n FROM ${sql(schema)}.\"queries\"\n ORDER BY length(\"clientAST\"::text) DESC\n LIMIT 1;`,\n ],\n [\n 'totalActiveQueriesPerClientAndClientGroup',\n numQueriesPerClientGroup(true),\n ],\n [\n 'totalInactiveQueriesPerClientAndClientGroup',\n numQueriesPerClientGroup(false),\n ],\n [\n 'totalRowsPerClientGroup',\n sql`SELECT \"clientGroupID\", COUNT(*) as \"c\" FROM ${sql(\n schema,\n )}.\"rows\" GROUP BY \"clientGroupID\" ORDER BY \"c\" DESC`,\n ],\n [\n 'numRowsPerQuery',\n sql`SELECT\n k.key AS \"queryHash\",\n COUNT(*) AS row_count\n FROM ${sql(schema)}.\"rows\" r,\n LATERAL jsonb_each(r.\"refCounts\") k\n GROUP BY k.key\n ORDER BY row_count DESC;`,\n ],\n ] satisfies [\n name: string,\n query: ReturnType<ReturnType<typeof pgClient>>,\n ][]);\n } finally {\n await sql.end();\n }\n}\n\nasync function changeLogStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config)) + '/cdc';\n const sql = pgClient(lc, config.change.db, 'statz-change');\n\n try {\n return await getPgStats([\n [\n 'changeLogSize',\n sql`SELECT COUNT(*) as \"change_log_size\" FROM ${sql(schema)}.\"changeLog\"`,\n ],\n ]);\n } finally {\n await sql.end();\n }\n}\n\n// The replica is opened read-only: /statz is available to the operator\n// password, and must not change the replica. Among other things, a writable\n// connection would run `PRAGMA optimize` when closed.\nfunction replicaStats(lc: LogContext, config: ZeroConfig) {\n const db = new Database(lc, config.replica.file, {readonly: true});\n try {\n return Object.fromEntries([\n ['wal file sizes', walFileSizes(config.replica.file)],\n ['page count', pick(first(db.pragma('PAGE_COUNT')))],\n ['page size', pick(first(db.pragma('PAGE_SIZE')))],\n ['journal mode', pick(first(db.pragma('JOURNAL_MODE')))],\n ['synchronous', pick(first(db.pragma('SYNCHRONOUS')))],\n ['cache size', pick(first(db.pragma('CACHE_SIZE')))],\n ['auto vacuum', pick(first(db.pragma('AUTO_VACUUM')))],\n ['freelist count', pick(first(db.pragma('FREELIST_COUNT')))],\n ['wal autocheckpoint', pick(first(db.pragma('WAL_AUTOCHECKPOINT')))],\n ['db file stats', fs.statSync(config.replica.file)],\n ] as const);\n } finally {\n db.close();\n }\n}\n\nfunction replicationStats(lc: LogContext, config: ZeroConfig) {\n const db = new Database(lc, config.replica.file, {readonly: true});\n try {\n return getReplicationStats(db);\n } finally {\n db.close();\n }\n}\n\n/**\n * The sizes of the replica's WAL files, by suffix, read from the filesystem.\n * `PRAGMA wal_checkpoint` would report the WAL's frame counts, but it runs a\n * checkpoint to do so, and fails on a read-only connection in WAL mode.\n */\nfunction walFileSizes(file: string): Record<string, number> {\n const sizes: Record<string, number> = {};\n // `-wal` is used in WAL mode, and both files in WAL2 mode.\n for (const suffix of ['-wal', '-wal2']) {\n const stats = fs.statSync(file + suffix, {throwIfNoEntry: false});\n if (stats) {\n sizes[suffix] = stats.size;\n }\n }\n return sizes;\n}\n\nfunction getReplicationStats(db: Database) {\n const {stateVersion} = getReplicationState(new StatementRunner(db));\n const lsn = fromStateVersionString(stateVersion);\n return {lsn};\n}\n\nfunction osStats() {\n return Object.fromEntries([\n ['load avg', os.loadavg()],\n ['uptime', os.uptime()],\n ['total mem', os.totalmem()],\n ['free mem', os.freemem()],\n ['cpus', os.cpus().length],\n ['available parallelism', os.availableParallelism()],\n ['platform', os.platform()],\n ['arch', os.arch()],\n ['release', os.release()],\n ] as const);\n}\n\nasync function getPgStats(\n pendingQueries: [\n name: string,\n query: ReturnType<ReturnType<typeof pgClient>>,\n ][],\n) {\n const results = await Promise.all(\n pendingQueries.map(async ([name, query]) => [name, await query] as const),\n );\n return Object.fromEntries(results);\n}\n\ntype StatsObject = Record<string, unknown>;\n\nfunction printStats(group: string, statsObject: StatsObject): string {\n const lines: string[] = ['\\n' + header(group)];\n for (const [name, result] of Object.entries(statsObject)) {\n lines.push('\\n' + name + ': ' + BigIntJSON.stringify(result, null, 2));\n }\n lines.push('\\n');\n return lines.join('');\n}\n\n/**\n * Serves counts and sizes. This is available to the operator password as well\n * as the admin password (see `--operator-password`), so it must never return\n * application data: no row values, query ASTs or query arguments.\n *\n * HTTP query parameters:\n * * `group`: restricts the groups for which stats are computed\n * * `format=json`: returns the stats as a JSON object\n * * `pretty`: formats the JSON object with indentation\n */\nexport async function handleStatzRequest(\n lc: LogContext,\n config: ZeroConfig,\n req: FastifyRequest,\n res: FastifyReply,\n) {\n const credentials = auth(req);\n if (getOperatorAccess(lc, config, credentials?.pass) === undefined) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Statz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n const statsFns: Record<string, () => Promise<StatsObject> | StatsObject> = {\n upstream: () => upstreamStats(lc, config),\n cvr: () => cvrStats(lc, config),\n // The PG change log (and thus the change DB) is not used when disabled.\n ...(config.changeStreamer.pgChangeLogEnabled\n ? {changeLog: () => changeLogStats(lc, config)}\n : {}),\n replica: () => replicaStats(lc, config),\n replication: () => replicationStats(lc, config),\n os: () => osStats(),\n };\n\n async function computeStats(group: string): Promise<[string, StatsObject]> {\n try {\n return [group, await statsFns[group]()];\n } catch (e) {\n lc.error?.(`error computing ${group} stats`, e);\n return [group, {error: String(e)}];\n }\n }\n\n const query = req.query as Record<string, unknown>;\n const groups =\n typeof query.group === 'string'\n ? query.group.split(',')\n : Array.isArray(query.group)\n ? query.group\n : undefined;\n\n const stats = await Promise.all(\n groups\n ? groups.filter(g => g in statsFns).map(computeStats)\n : Object.keys(statsFns).map(computeStats),\n );\n\n if (query.format === 'json') {\n const indent = query.pretty !== undefined ? 2 : undefined;\n await res\n .header('Content-Type', 'application/json')\n .send(BigIntJSON.stringify(Object.fromEntries(stats), null, indent));\n return;\n } else {\n const body = stats\n .map(([group, statsObject]) => printStats(group, statsObject))\n .join('');\n await res.header('Content-Type', 'text/plain; charset=utf-8').send(body);\n }\n}\n\nfunction first(x: object[]): object {\n return x[0];\n}\n\nfunction pick(x: object): unknown {\n return Object.values(x)[0];\n}\n\nfunction header(name: string): string {\n return `=== ${name} ===\\n`;\n}\n"],"mappings":"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"}
@@ -9,7 +9,7 @@ import { getOrCreateCounter } from "../observability/metrics.js";
9
9
  import { ProtocolErrorWithLevel, getLogLevel, wrapWithProtocolError } from "../types/error-with-level.js";
10
10
  import { upstreamSchemaWithUnparsedAnalyzeQuery } from "../../../zero-protocol/src/up.js";
11
11
  import { PokeChunkEncoder } from "../types/poke-chunk.js";
12
- import WebSocket$1, { createWebSocketStream } from "ws";
12
+ import WebSocket, { createWebSocketStream } from "ws";
13
13
  import { Readable, Writable, pipeline } from "node:stream";
14
14
  //#region ../zero-cache/src/workers/connection.ts
15
15
  function hasOwn(value, property) {
@@ -224,7 +224,7 @@ var Connection = class {
224
224
  }
225
225
  };
226
226
  function send(lc, ws, data, callback, serialized) {
227
- if (ws.readyState === WebSocket$1.OPEN) {
227
+ if (ws.readyState === WebSocket.OPEN) {
228
228
  serialized ??= JSON.stringify(data);
229
229
  if (callback === "ignore-backpressure") {
230
230
  ws.send(serialized);
@@ -310,7 +310,7 @@ function sendAsync(lc, ws, data) {
310
310
  }
311
311
  function sendBinary(lc, ws, chunk) {
312
312
  return new Promise((resolve, reject) => {
313
- if (ws.readyState !== WebSocket$1.OPEN) {
313
+ if (ws.readyState !== WebSocket.OPEN) {
314
314
  const error = webSocketClosedError();
315
315
  lc.debug?.(`Dropping outbound binary message on ws (state: ${ws.readyState})`);
316
316
  reject(error);
@@ -1 +1 @@
1
- {"version":3,"file":"connection.js","names":["#ws","#wsID","#protocolVersion","#lc","#onClose","#messageHandler","#allowLegacyQueries","#downstreamSender","#downstreamMsgTimer","#webSocketErrors","#handleClose","#handleError","#proxyInbound","#maybeSendPong","#closeWithError","#closed","#viewSyncerOutboundStream","#pusherOutboundStream","#handleMessage","#loggedUnknownMessage","#handleMessageResult","#closeWithThrown","#proxyOutbound","#recordWebSocketError","#lastDownstreamMsgTime","#pokeChunkEncoder","#sendPokePart","#sendPokeEnd"],"sources":["../../../../../zero-cache/src/workers/connection.ts"],"sourcesContent":["import {pipeline, Readable, Writable} from 'node:stream';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport type {CloseEvent, Data, ErrorEvent} from 'ws';\nimport WebSocket, {createWebSocketStream} from 'ws';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport * as valita from '../../../shared/src/valita.ts';\nimport type {ConnectedMessage} from '../../../zero-protocol/src/connect.ts';\nimport type {Downstream} from '../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport type {ErrorBody} from '../../../zero-protocol/src/error.ts';\nimport {\n isProtocolError,\n type ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {\n POKE_CHUNK_PROTOCOL_VERSION,\n type PokeChunk,\n type PokeEndMessage,\n type PokePartMessage,\n} from '../../../zero-protocol/src/poke.ts';\nimport {\n MIN_SERVER_SUPPORTED_SYNC_PROTOCOL,\n PROTOCOL_VERSION,\n} from '../../../zero-protocol/src/protocol-version.ts';\nimport {\n upstreamSchemaWithUnparsedAnalyzeQuery,\n type UpstreamWithUnparsedAnalyzeQuery,\n} from '../../../zero-protocol/src/up.ts';\nimport {getOrCreateCounter} from '../observability/metrics.ts';\nimport type {ViewSyncerDownstream} from '../types/downstream.ts';\nimport {\n ProtocolErrorWithLevel,\n getLogLevel,\n wrapWithProtocolError,\n} from '../types/error-with-level.ts';\nimport {PokeChunkEncoder} from '../types/poke-chunk.ts';\nimport type {Source} from '../types/streams.ts';\nimport type {ConnectParams} from './connect-params.ts';\n\nexport type HandlerResult =\n | {\n type: 'ok';\n }\n | {\n type: 'fatal';\n error: ErrorBody;\n }\n | {\n type: 'transient';\n errors: ErrorBody[];\n }\n | StreamResult;\n\nexport type StreamResult =\n | {\n type: 'stream';\n source: 'viewSyncer';\n stream: Source<ViewSyncerDownstream>;\n }\n | {\n type: 'stream';\n source: 'pusher';\n stream: Source<Downstream>;\n };\n\nexport interface MessageHandler {\n handleMessage(\n msg: UpstreamWithUnparsedAnalyzeQuery,\n ): Promise<HandlerResult[]>;\n}\n\nfunction hasOwn(value: unknown, property: string): boolean {\n return (\n typeof value === 'object' &&\n value !== null &&\n Object.hasOwn(value, property)\n );\n}\n\nfunction containsLegacyQuery(message: unknown): boolean {\n if (!Array.isArray(message)) {\n return false;\n }\n\n const body = message[1];\n if (typeof body !== 'object' || body === null) {\n return false;\n }\n\n if (\n message[0] !== 'initConnection' &&\n message[0] !== 'changeDesiredQueries'\n ) {\n return false;\n }\n\n const patch = body['desiredQueriesPatch'];\n return (\n Array.isArray(patch) && patch.some(operation => hasOwn(operation, 'ast'))\n );\n}\n\nconst LEGACY_QUERIES_DISABLED_MESSAGE =\n 'Legacy queries are disabled by this Zero server. This client must use custom queries instead of sending query ASTs directly. To temporarily restore compatibility, set ZERO_ALLOW_LEGACY_QUERIES=true on zero-cache.';\n\n// Exported for testing purposes.\n//\n// Client messages are parsed leniently, so a newer client can add message\n// kinds and fields:\n// - A message of a kind this server doesn't know parses as an\n// UnrecognizedTuple, for the connection to drop. Malformed messages still\n// throw.\n// - Messages are parsed in strip mode, so fields this server doesn't know are\n// dropped. Strip rather than passthrough: nothing this server doesn't know\n// reaches the CVR or the API server, and valita's passthrough drops unknown\n// fields anyway on any object with a transformed field below it (such as\n// jsonSchema args). ASTs stay strict in the schema, because a server that\n// ignored an AST field would run a different query.\nexport function parseUpstreamMessage(\n value: unknown,\n allowLegacyQueries: boolean,\n): UpstreamWithUnparsedAnalyzeQuery | valita.UnrecognizedTuple {\n if (!allowLegacyQueries && containsLegacyQuery(value)) {\n throw new Error(LEGACY_QUERIES_DISABLED_MESSAGE);\n }\n return valita.parse(value, upstreamSchemaWithUnparsedAnalyzeQuery, 'strip');\n}\n\n// Ensures that a downstream message is sent at least every interval, sending a\n// 'pong' if necessary. This is set to be slightly longer than the client-side\n// PING_INTERVAL of 5 seconds, so that in the common case, 'pong's are sent in\n// response to client-initiated 'ping's. However, if the inbound stream is\n// backed up because a command is taking a long time to process, the pings\n// will be stuck in the queue (i.e. back-pressured), in which case pongs will\n// be manually sent to notify the client of server liveness.\n//\n// This is equivalent to what is done for Postgres keepalives on the\n// replication stream (which can similarly be back-pressured):\n// https://github.com/rocicorp/mono/blob/f98cb369a2dbb15650328859c732db358f187ef0/packages/zero-cache/src/services/change-source/pg/logical-replication/stream.ts#L21\nconst DOWNSTREAM_MSG_INTERVAL_MS = 6_000;\nexport const WEBSOCKET_SEND_TIMEOUT_MS = 10_000;\nconst PROTOCOL_VERSION_ATTRIBUTE = 'protocol.version';\nconst EVENT_TYPE_ATTRIBUTE = 'event.type';\n\n/**\n * Represents a connection between the client and server.\n *\n * Handles incoming messages on the connection and dispatches\n * them to the correct service.\n *\n * Listens to the ViewSyncer and sends messages to the client.\n */\nexport class Connection {\n readonly #ws: WebSocket;\n readonly #wsID: string;\n readonly #protocolVersion: number;\n readonly #lc: LogContext;\n readonly #onClose: () => void;\n readonly #messageHandler: MessageHandler;\n readonly #allowLegacyQueries: boolean;\n readonly #downstreamSender: DownstreamSender;\n readonly #downstreamMsgTimer: NodeJS.Timeout | undefined;\n readonly #webSocketErrors = getOrCreateCounter(\n 'sync',\n 'websocket.errors',\n 'Client WebSocket error events.',\n );\n\n #viewSyncerOutboundStream: Source<ViewSyncerDownstream> | undefined;\n #pusherOutboundStream: Source<Downstream> | undefined;\n #closed = false;\n #loggedUnknownMessage = false;\n\n constructor(\n lc: LogContext,\n connectParams: ConnectParams,\n ws: WebSocket,\n allowLegacyQueries: boolean,\n messageHandler: MessageHandler,\n onClose: () => void,\n ) {\n const {clientGroupID, clientID, wsID, protocolVersion} = connectParams;\n this.#messageHandler = messageHandler;\n this.#allowLegacyQueries = allowLegacyQueries;\n\n this.#ws = ws;\n this.#wsID = wsID;\n this.#protocolVersion = protocolVersion;\n\n this.#lc = lc\n .withContext('connection')\n .withContext('clientID', clientID)\n .withContext('clientGroupID', clientGroupID)\n .withContext('wsID', wsID);\n this.#downstreamSender = new DownstreamSender(\n this.#lc,\n ws,\n protocolVersion,\n );\n this.#lc.debug?.('new connection');\n this.#onClose = onClose;\n\n this.#ws.addEventListener('close', this.#handleClose);\n this.#ws.addEventListener('error', this.#handleError);\n\n this.#proxyInbound();\n this.#downstreamMsgTimer = setInterval(\n this.#maybeSendPong,\n DOWNSTREAM_MSG_INTERVAL_MS / 2,\n );\n }\n\n /**\n * Checks the protocol version and errors for unsupported protocols,\n * sending the initial `connected` response on success.\n *\n * This is early in the connection lifecycle because {@link #handleMessage}\n * will only parse messages with schema(s) of supported protocol versions.\n */\n init(): boolean {\n if (\n this.#protocolVersion > PROTOCOL_VERSION ||\n this.#protocolVersion < MIN_SERVER_SUPPORTED_SYNC_PROTOCOL\n ) {\n this.#closeWithError({\n kind: ErrorKind.VersionNotSupported,\n message: `server is at sync protocol v${PROTOCOL_VERSION} and does not support v${\n this.#protocolVersion\n }. The ${\n this.#protocolVersion > PROTOCOL_VERSION ? 'server' : 'client'\n } must be updated to a newer release.`,\n origin: ErrorOrigin.ZeroCache,\n });\n } else {\n const connectedMessage: ConnectedMessage = [\n 'connected',\n {wsid: this.#wsID, timestamp: Date.now()},\n ];\n this.send(connectedMessage, 'ignore-backpressure');\n return true;\n }\n return false;\n }\n\n close(reason: string, ...args: unknown[]) {\n if (this.#closed) {\n return;\n }\n this.#closed = true;\n this.#lc.info?.(`closing connection: ${reason}`, ...args);\n this.#ws.removeEventListener('close', this.#handleClose);\n this.#ws.removeEventListener('error', this.#handleError);\n this.#viewSyncerOutboundStream?.cancel();\n this.#viewSyncerOutboundStream = undefined;\n this.#pusherOutboundStream?.cancel();\n this.#pusherOutboundStream = undefined;\n this.#onClose();\n if (this.#ws.readyState !== this.#ws.CLOSED) {\n this.#ws.close();\n }\n clearTimeout(this.#downstreamMsgTimer);\n\n // spin down services if we have\n // no more client connections for the client group?\n }\n\n handleInitConnection(initConnectionMsg: string) {\n return this.#handleMessage({data: initConnectionMsg});\n }\n\n #handleMessage = async (event: {data: Data}) => {\n const data = event.data.toString();\n if (this.#closed) {\n this.#lc.debug?.('Ignoring message received after closed', data);\n return;\n }\n\n let msg;\n try {\n const value = JSON.parse(data);\n msg = parseUpstreamMessage(value, this.#allowLegacyQueries);\n } catch (e) {\n const errorBody = {\n kind: ErrorKind.InvalidMessage,\n message: String(e),\n origin: ErrorOrigin.ZeroCache,\n } as const;\n this.#closeWithError(\n errorBody,\n new ProtocolErrorWithLevel(errorBody, 'warn'),\n );\n return;\n }\n\n if (!upstreamSchemaWithUnparsedAnalyzeQuery.isKnown(msg)) {\n // A message kind from a newer client.\n if (!this.#loggedUnknownMessage) {\n this.#loggedUnknownMessage = true;\n this.#lc.info?.(\n `Ignoring messages of unknown type, starting with ${JSON.stringify(msg[0])}`,\n );\n }\n return;\n }\n\n try {\n const msgType = msg[0];\n if (msgType === 'ping') {\n this.send(['pong', {}], 'ignore-backpressure');\n return;\n }\n\n const result = await this.#messageHandler.handleMessage(msg);\n for (const r of result) {\n this.#handleMessageResult(r);\n }\n } catch (e) {\n this.#closeWithThrown(e);\n }\n };\n\n #handleMessageResult(result: HandlerResult): void {\n switch (result.type) {\n case 'fatal':\n this.#closeWithError(result.error);\n break;\n case 'ok':\n break;\n case 'stream': {\n switch (result.source) {\n case 'viewSyncer':\n assert(\n this.#viewSyncerOutboundStream === undefined,\n 'Outbound stream already set for this connection!',\n );\n this.#viewSyncerOutboundStream = result.stream;\n break;\n case 'pusher':\n assert(\n this.#pusherOutboundStream === undefined,\n 'Outbound stream already set for this connection!',\n );\n this.#pusherOutboundStream = result.stream;\n break;\n }\n if (result.source === 'viewSyncer') {\n this.#proxyOutbound(result.stream, (downstream, callback) =>\n this.send(downstream.message, callback, downstream.serialized),\n );\n } else {\n this.#proxyOutbound(result.stream, (downstream, callback) =>\n this.send(downstream, callback),\n );\n }\n break;\n }\n case 'transient': {\n for (const error of result.errors) {\n this.sendError(error);\n }\n }\n }\n }\n\n #handleClose = (e: CloseEvent) => {\n const {code, reason, wasClean} = e;\n if (!wasClean) {\n this.#recordWebSocketError('unclean_close');\n }\n this.close('WebSocket close event', {code, reason, wasClean});\n };\n\n #handleError = (e: ErrorEvent) => {\n this.#recordWebSocketError('error_event');\n this.#lc.warn?.('WebSocket error event', e.message, e.error);\n };\n\n #recordWebSocketError(eventType: string) {\n this.#webSocketErrors.add(1, {\n [PROTOCOL_VERSION_ATTRIBUTE]: this.#protocolVersion,\n [EVENT_TYPE_ATTRIBUTE]: eventType,\n });\n }\n\n #proxyInbound() {\n pipeline(\n createWebSocketStream(this.#ws),\n new Writable({\n write: (data, _encoding, callback) => {\n this.#handleMessage({data}).then(() => callback(), callback);\n },\n }),\n // The done callback is not used, as #handleClose and #handleError,\n // configured on the underlying WebSocket, provide more complete\n // information.\n () => {},\n );\n }\n\n #proxyOutbound<T>(\n outboundStream: Source<T>,\n sendMessage: (\n downstream: T,\n callback: (err?: Error | null) => void,\n ) => void,\n ) {\n // Note: createWebSocketStream() is avoided here in order to control\n // exception handling with #closeWithThrown(). If the Writable\n // from createWebSocketStream() were instead used, exceptions\n // from the outboundStream result in the Writable closing the\n // the websocket before the error message can be sent.\n pipeline(\n Readable.from(outboundStream),\n new Writable({\n objectMode: true,\n write: (downstream: T, _encoding, callback) =>\n sendMessage(downstream, callback),\n }),\n e =>\n e\n ? this.#closeWithThrown(e)\n : this.close(`downstream closed by ViewSyncer`),\n );\n }\n\n #closeWithThrown(e: unknown) {\n const errorBody =\n findProtocolError(e)?.errorBody ?? wrapWithProtocolError(e).errorBody;\n\n this.#closeWithError(errorBody, e);\n }\n\n #closeWithError(errorBody: ErrorBody, thrown?: unknown) {\n this.sendError(errorBody, thrown);\n this.close(\n `${errorBody.kind} (${errorBody.origin}): ${errorBody.message}`,\n errorBody,\n );\n }\n\n #lastDownstreamMsgTime = Date.now();\n\n #maybeSendPong = () => {\n if (Date.now() - this.#lastDownstreamMsgTime > DOWNSTREAM_MSG_INTERVAL_MS) {\n this.#lc.debug?.('manually sending pong');\n this.send(['pong', {}], 'ignore-backpressure');\n }\n };\n\n send(\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n ) {\n this.#lastDownstreamMsgTime = Date.now();\n this.#downstreamSender.send(data, callback, serialized);\n }\n\n sendError(errorBody: ErrorBody, thrown?: unknown) {\n sendError(this.#lc, this.#ws, errorBody, thrown);\n }\n}\n\nexport type WebSocketLike = Pick<WebSocket, 'readyState'> & {\n send(data: string | PokeChunk, cb?: (err?: Error) => void): void;\n};\n\n// Exported for testing purposes.\nexport function send(\n lc: LogContext,\n ws: WebSocketLike,\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n) {\n if (ws.readyState === WebSocket.OPEN) {\n serialized ??= JSON.stringify(data);\n if (callback === 'ignore-backpressure') {\n ws.send(serialized);\n return;\n }\n\n let completed = false;\n const timer = setTimeout(() => {\n complete(webSocketSendTimeoutError());\n }, WEBSOCKET_SEND_TIMEOUT_MS);\n timer.unref();\n\n const complete = (error?: Error | null) => {\n if (completed) {\n return;\n }\n completed = true;\n clearTimeout(timer);\n callback(error);\n };\n\n ws.send(serialized, complete);\n } else {\n lc.debug?.(`Dropping outbound message on ws (state: ${ws.readyState})`, {\n dropped: data,\n });\n if (callback !== 'ignore-backpressure') {\n callback(\n new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: 'WebSocket closed',\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n ),\n );\n }\n }\n}\n\n/**\n * Sends downstream messages in the format supported by a connection's sync\n * protocol version. Keep version forks contained here so the view-syncer can\n * continue producing one canonical poke representation.\n *\n * Exported for compatibility testing.\n */\nexport class DownstreamSender {\n readonly #lc: LogContext;\n readonly #ws: WebSocketLike;\n readonly #protocolVersion: number;\n #pokeChunkEncoder: PokeChunkEncoder | undefined;\n\n constructor(lc: LogContext, ws: WebSocketLike, protocolVersion: number) {\n this.#lc = lc;\n this.#ws = ws;\n this.#protocolVersion = protocolVersion;\n }\n\n send(\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n ): void {\n if (this.#protocolVersion >= POKE_CHUNK_PROTOCOL_VERSION) {\n switch (data[0]) {\n case 'pokeStart':\n assert(\n this.#pokeChunkEncoder === undefined,\n 'cannot start a poke while another poke is in progress',\n );\n this.#pokeChunkEncoder = new PokeChunkEncoder();\n break;\n case 'pokePart':\n this.#sendPokePart(data, callback, serialized);\n return;\n case 'pokeEnd':\n this.#sendPokeEnd(data, callback);\n return;\n }\n }\n send(this.#lc, this.#ws, data, callback, serialized);\n }\n\n #sendPokePart(\n pokePart: PokePartMessage,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n ): void {\n const encoder = this.#pokeChunkEncoder;\n assert(encoder, 'pokePart received without a pokeStart');\n\n serialized ??= JSON.stringify(pokePart);\n assert(\n serialized.startsWith(POKE_PART_PREFIX) && serialized.endsWith(']'),\n 'invalid serialized pokePart',\n );\n void encoder\n .addPatch(serialized.slice(POKE_PART_PREFIX.length, -1), chunk =>\n sendBinary(this.#lc, this.#ws, chunk),\n )\n .then(\n () => invokeCallback(callback),\n error => invokeCallback(callback, toError(error)),\n );\n }\n\n #sendPokeEnd(\n pokeEnd: PokeEndMessage,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n ): void {\n const encoder = this.#pokeChunkEncoder;\n assert(encoder, 'pokeEnd received without a pokeStart');\n this.#pokeChunkEncoder = undefined;\n\n if (pokeEnd[1].cancel) {\n encoder.cancel();\n send(this.#lc, this.#ws, pokeEnd, callback);\n return;\n }\n\n void encoder\n .finish(chunk => sendBinary(this.#lc, this.#ws, chunk))\n .then(() => sendAsync(this.#lc, this.#ws, pokeEnd))\n .then(\n () => invokeCallback(callback),\n error => invokeCallback(callback, toError(error)),\n );\n }\n}\n\nconst POKE_PART_PREFIX = '[\"pokePart\",';\n\nfunction sendAsync(\n lc: LogContext,\n ws: WebSocketLike,\n data: Downstream,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n send(lc, ws, data, error => (error ? reject(error) : resolve()));\n });\n}\n\nfunction sendBinary(\n lc: LogContext,\n ws: WebSocketLike,\n chunk: PokeChunk,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n if (ws.readyState !== WebSocket.OPEN) {\n const error = webSocketClosedError();\n lc.debug?.(\n `Dropping outbound binary message on ws (state: ${ws.readyState})`,\n );\n reject(error);\n return;\n }\n let completed = false;\n const timer = setTimeout(\n () => complete(webSocketSendTimeoutError()),\n WEBSOCKET_SEND_TIMEOUT_MS,\n );\n timer.unref();\n\n const complete = (error?: Error | null) => {\n if (completed) {\n return;\n }\n completed = true;\n clearTimeout(timer);\n error ? reject(error) : resolve();\n };\n\n ws.send(chunk, complete);\n });\n}\n\nfunction invokeCallback(\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n error?: Error,\n): void {\n if (callback !== 'ignore-backpressure') {\n callback(error);\n }\n}\n\nfunction toError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n\nfunction webSocketClosedError(): ProtocolErrorWithLevel {\n return new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: 'WebSocket closed',\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n );\n}\n\nfunction webSocketSendTimeoutError(): ProtocolErrorWithLevel {\n return new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: `WebSocket send timed out after ${WEBSOCKET_SEND_TIMEOUT_MS} ms`,\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n );\n}\n\nexport function sendError(\n lc: LogContext,\n ws: WebSocket,\n errorBody: ErrorBody,\n thrown?: unknown,\n) {\n lc = lc.withContext('errorKind', errorBody.kind);\n\n let logLevel: LogLevel;\n\n // If the thrown error is a ProtocolErrorWithLevel, its explicit logLevel takes precedence\n if (thrown instanceof ProtocolErrorWithLevel) {\n logLevel = thrown.logLevel;\n }\n // Errors with errno or transient socket codes are low-level, transient I/O issues\n // (e.g., EPIPE, ECONNRESET) and should be warnings, not errors\n else if (\n hasErrno(thrown) ||\n hasTransientSocketCode(thrown) ||\n isTransientSocketMessage(errorBody.message)\n ) {\n logLevel = 'warn';\n }\n // Fallback: check errorBody.kind for errors that weren't thrown as ProtocolErrorWithLevel\n else if (\n errorBody.kind === ErrorKind.ClientNotFound ||\n errorBody.kind === ErrorKind.TransformFailed\n ) {\n logLevel = 'warn';\n } else {\n logLevel = thrown ? getLogLevel(thrown) : 'info';\n }\n\n lc[logLevel]?.('Sending error on WebSocket', errorBody, thrown ?? '');\n send(lc, ws, ['error', errorBody], 'ignore-backpressure');\n}\n\nexport function findProtocolError(error: unknown): ProtocolError | undefined {\n if (isProtocolError(error)) {\n return error;\n }\n if (error instanceof Error && error.cause) {\n return findProtocolError(error.cause);\n }\n return undefined;\n}\n\nfunction hasErrno(error: unknown): boolean {\n return Boolean(\n error &&\n typeof error === 'object' &&\n 'errno' in error &&\n typeof (error as {errno: unknown}).errno !== 'undefined',\n );\n}\n\n// System error codes that indicate transient socket conditions.\n// These are checked via the `code` property on errors.\nconst TRANSIENT_SOCKET_ERROR_CODES = new Set([\n 'EPIPE',\n 'ECONNRESET',\n 'ECANCELED',\n]);\n\n// Error messages that indicate transient socket conditions but don't have\n// standard error codes (e.g., WebSocket library errors).\nconst TRANSIENT_SOCKET_MESSAGE_PATTERNS = [\n 'socket was closed while data was being compressed',\n];\n\nfunction hasTransientSocketCode(error: unknown): boolean {\n if (!error || typeof error !== 'object') {\n return false;\n }\n const maybeCode =\n 'code' in error ? String((error as {code?: unknown}).code) : undefined;\n return Boolean(\n maybeCode && TRANSIENT_SOCKET_ERROR_CODES.has(maybeCode.toUpperCase()),\n );\n}\n\nfunction isTransientSocketMessage(message: string | undefined): boolean {\n if (!message) {\n return false;\n }\n const lower = message.toLowerCase();\n return TRANSIENT_SOCKET_MESSAGE_PATTERNS.some(pattern =>\n lower.includes(pattern),\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"connection.js","names":["#ws","#wsID","#protocolVersion","#lc","#onClose","#messageHandler","#allowLegacyQueries","#downstreamSender","#downstreamMsgTimer","#webSocketErrors","#handleClose","#handleError","#proxyInbound","#maybeSendPong","#closeWithError","#closed","#viewSyncerOutboundStream","#pusherOutboundStream","#handleMessage","#loggedUnknownMessage","#handleMessageResult","#closeWithThrown","#proxyOutbound","#recordWebSocketError","#lastDownstreamMsgTime","#pokeChunkEncoder","#sendPokePart","#sendPokeEnd"],"sources":["../../../../../zero-cache/src/workers/connection.ts"],"sourcesContent":["import {pipeline, Readable, Writable} from 'node:stream';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport type {CloseEvent, Data, ErrorEvent} from 'ws';\nimport WebSocket, {createWebSocketStream} from 'ws';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport * as valita from '../../../shared/src/valita.ts';\nimport type {ConnectedMessage} from '../../../zero-protocol/src/connect.ts';\nimport type {Downstream} from '../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport type {ErrorBody} from '../../../zero-protocol/src/error.ts';\nimport {\n isProtocolError,\n type ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {\n POKE_CHUNK_PROTOCOL_VERSION,\n type PokeChunk,\n type PokeEndMessage,\n type PokePartMessage,\n} from '../../../zero-protocol/src/poke.ts';\nimport {\n MIN_SERVER_SUPPORTED_SYNC_PROTOCOL,\n PROTOCOL_VERSION,\n} from '../../../zero-protocol/src/protocol-version.ts';\nimport {\n upstreamSchemaWithUnparsedAnalyzeQuery,\n type UpstreamWithUnparsedAnalyzeQuery,\n} from '../../../zero-protocol/src/up.ts';\nimport {getOrCreateCounter} from '../observability/metrics.ts';\nimport type {ViewSyncerDownstream} from '../types/downstream.ts';\nimport {\n ProtocolErrorWithLevel,\n getLogLevel,\n wrapWithProtocolError,\n} from '../types/error-with-level.ts';\nimport {PokeChunkEncoder} from '../types/poke-chunk.ts';\nimport type {Source} from '../types/streams.ts';\nimport type {ConnectParams} from './connect-params.ts';\n\nexport type HandlerResult =\n | {\n type: 'ok';\n }\n | {\n type: 'fatal';\n error: ErrorBody;\n }\n | {\n type: 'transient';\n errors: ErrorBody[];\n }\n | StreamResult;\n\nexport type StreamResult =\n | {\n type: 'stream';\n source: 'viewSyncer';\n stream: Source<ViewSyncerDownstream>;\n }\n | {\n type: 'stream';\n source: 'pusher';\n stream: Source<Downstream>;\n };\n\nexport interface MessageHandler {\n handleMessage(\n msg: UpstreamWithUnparsedAnalyzeQuery,\n ): Promise<HandlerResult[]>;\n}\n\nfunction hasOwn(value: unknown, property: string): boolean {\n return (\n typeof value === 'object' &&\n value !== null &&\n Object.hasOwn(value, property)\n );\n}\n\nfunction containsLegacyQuery(message: unknown): boolean {\n if (!Array.isArray(message)) {\n return false;\n }\n\n const body = message[1];\n if (typeof body !== 'object' || body === null) {\n return false;\n }\n\n if (\n message[0] !== 'initConnection' &&\n message[0] !== 'changeDesiredQueries'\n ) {\n return false;\n }\n\n const patch = body['desiredQueriesPatch'];\n return (\n Array.isArray(patch) && patch.some(operation => hasOwn(operation, 'ast'))\n );\n}\n\nconst LEGACY_QUERIES_DISABLED_MESSAGE =\n 'Legacy queries are disabled by this Zero server. This client must use custom queries instead of sending query ASTs directly. To temporarily restore compatibility, set ZERO_ALLOW_LEGACY_QUERIES=true on zero-cache.';\n\n// Exported for testing purposes.\n//\n// Client messages are parsed leniently, so a newer client can add message\n// kinds and fields:\n// - A message of a kind this server doesn't know parses as an\n// UnrecognizedTuple, for the connection to drop. Malformed messages still\n// throw.\n// - Messages are parsed in strip mode, so fields this server doesn't know are\n// dropped. Strip rather than passthrough: nothing this server doesn't know\n// reaches the CVR or the API server, and valita's passthrough drops unknown\n// fields anyway on any object with a transformed field below it (such as\n// jsonSchema args). ASTs stay strict in the schema, because a server that\n// ignored an AST field would run a different query.\nexport function parseUpstreamMessage(\n value: unknown,\n allowLegacyQueries: boolean,\n): UpstreamWithUnparsedAnalyzeQuery | valita.UnrecognizedTuple {\n if (!allowLegacyQueries && containsLegacyQuery(value)) {\n throw new Error(LEGACY_QUERIES_DISABLED_MESSAGE);\n }\n return valita.parse(value, upstreamSchemaWithUnparsedAnalyzeQuery, 'strip');\n}\n\n// Ensures that a downstream message is sent at least every interval, sending a\n// 'pong' if necessary. This is set to be slightly longer than the client-side\n// PING_INTERVAL of 5 seconds, so that in the common case, 'pong's are sent in\n// response to client-initiated 'ping's. However, if the inbound stream is\n// backed up because a command is taking a long time to process, the pings\n// will be stuck in the queue (i.e. back-pressured), in which case pongs will\n// be manually sent to notify the client of server liveness.\n//\n// This is equivalent to what is done for Postgres keepalives on the\n// replication stream (which can similarly be back-pressured):\n// https://github.com/rocicorp/mono/blob/f98cb369a2dbb15650328859c732db358f187ef0/packages/zero-cache/src/services/change-source/pg/logical-replication/stream.ts#L21\nconst DOWNSTREAM_MSG_INTERVAL_MS = 6_000;\nexport const WEBSOCKET_SEND_TIMEOUT_MS = 10_000;\nconst PROTOCOL_VERSION_ATTRIBUTE = 'protocol.version';\nconst EVENT_TYPE_ATTRIBUTE = 'event.type';\n\n/**\n * Represents a connection between the client and server.\n *\n * Handles incoming messages on the connection and dispatches\n * them to the correct service.\n *\n * Listens to the ViewSyncer and sends messages to the client.\n */\nexport class Connection {\n readonly #ws: WebSocket;\n readonly #wsID: string;\n readonly #protocolVersion: number;\n readonly #lc: LogContext;\n readonly #onClose: () => void;\n readonly #messageHandler: MessageHandler;\n readonly #allowLegacyQueries: boolean;\n readonly #downstreamSender: DownstreamSender;\n readonly #downstreamMsgTimer: NodeJS.Timeout | undefined;\n readonly #webSocketErrors = getOrCreateCounter(\n 'sync',\n 'websocket.errors',\n 'Client WebSocket error events.',\n );\n\n #viewSyncerOutboundStream: Source<ViewSyncerDownstream> | undefined;\n #pusherOutboundStream: Source<Downstream> | undefined;\n #closed = false;\n #loggedUnknownMessage = false;\n\n constructor(\n lc: LogContext,\n connectParams: ConnectParams,\n ws: WebSocket,\n allowLegacyQueries: boolean,\n messageHandler: MessageHandler,\n onClose: () => void,\n ) {\n const {clientGroupID, clientID, wsID, protocolVersion} = connectParams;\n this.#messageHandler = messageHandler;\n this.#allowLegacyQueries = allowLegacyQueries;\n\n this.#ws = ws;\n this.#wsID = wsID;\n this.#protocolVersion = protocolVersion;\n\n this.#lc = lc\n .withContext('connection')\n .withContext('clientID', clientID)\n .withContext('clientGroupID', clientGroupID)\n .withContext('wsID', wsID);\n this.#downstreamSender = new DownstreamSender(\n this.#lc,\n ws,\n protocolVersion,\n );\n this.#lc.debug?.('new connection');\n this.#onClose = onClose;\n\n this.#ws.addEventListener('close', this.#handleClose);\n this.#ws.addEventListener('error', this.#handleError);\n\n this.#proxyInbound();\n this.#downstreamMsgTimer = setInterval(\n this.#maybeSendPong,\n DOWNSTREAM_MSG_INTERVAL_MS / 2,\n );\n }\n\n /**\n * Checks the protocol version and errors for unsupported protocols,\n * sending the initial `connected` response on success.\n *\n * This is early in the connection lifecycle because {@link #handleMessage}\n * will only parse messages with schema(s) of supported protocol versions.\n */\n init(): boolean {\n if (\n this.#protocolVersion > PROTOCOL_VERSION ||\n this.#protocolVersion < MIN_SERVER_SUPPORTED_SYNC_PROTOCOL\n ) {\n this.#closeWithError({\n kind: ErrorKind.VersionNotSupported,\n message: `server is at sync protocol v${PROTOCOL_VERSION} and does not support v${\n this.#protocolVersion\n }. The ${\n this.#protocolVersion > PROTOCOL_VERSION ? 'server' : 'client'\n } must be updated to a newer release.`,\n origin: ErrorOrigin.ZeroCache,\n });\n } else {\n const connectedMessage: ConnectedMessage = [\n 'connected',\n {wsid: this.#wsID, timestamp: Date.now()},\n ];\n this.send(connectedMessage, 'ignore-backpressure');\n return true;\n }\n return false;\n }\n\n close(reason: string, ...args: unknown[]) {\n if (this.#closed) {\n return;\n }\n this.#closed = true;\n this.#lc.info?.(`closing connection: ${reason}`, ...args);\n this.#ws.removeEventListener('close', this.#handleClose);\n this.#ws.removeEventListener('error', this.#handleError);\n this.#viewSyncerOutboundStream?.cancel();\n this.#viewSyncerOutboundStream = undefined;\n this.#pusherOutboundStream?.cancel();\n this.#pusherOutboundStream = undefined;\n this.#onClose();\n if (this.#ws.readyState !== this.#ws.CLOSED) {\n this.#ws.close();\n }\n clearTimeout(this.#downstreamMsgTimer);\n\n // spin down services if we have\n // no more client connections for the client group?\n }\n\n handleInitConnection(initConnectionMsg: string) {\n return this.#handleMessage({data: initConnectionMsg});\n }\n\n #handleMessage = async (event: {data: Data}) => {\n const data = event.data.toString();\n if (this.#closed) {\n this.#lc.debug?.('Ignoring message received after closed', data);\n return;\n }\n\n let msg;\n try {\n const value = JSON.parse(data);\n msg = parseUpstreamMessage(value, this.#allowLegacyQueries);\n } catch (e) {\n const errorBody = {\n kind: ErrorKind.InvalidMessage,\n message: String(e),\n origin: ErrorOrigin.ZeroCache,\n } as const;\n this.#closeWithError(\n errorBody,\n new ProtocolErrorWithLevel(errorBody, 'warn'),\n );\n return;\n }\n\n if (!upstreamSchemaWithUnparsedAnalyzeQuery.isKnown(msg)) {\n // A message kind from a newer client.\n if (!this.#loggedUnknownMessage) {\n this.#loggedUnknownMessage = true;\n this.#lc.info?.(\n `Ignoring messages of unknown type, starting with ${JSON.stringify(msg[0])}`,\n );\n }\n return;\n }\n\n try {\n const msgType = msg[0];\n if (msgType === 'ping') {\n this.send(['pong', {}], 'ignore-backpressure');\n return;\n }\n\n const result = await this.#messageHandler.handleMessage(msg);\n for (const r of result) {\n this.#handleMessageResult(r);\n }\n } catch (e) {\n this.#closeWithThrown(e);\n }\n };\n\n #handleMessageResult(result: HandlerResult): void {\n switch (result.type) {\n case 'fatal':\n this.#closeWithError(result.error);\n break;\n case 'ok':\n break;\n case 'stream': {\n switch (result.source) {\n case 'viewSyncer':\n assert(\n this.#viewSyncerOutboundStream === undefined,\n 'Outbound stream already set for this connection!',\n );\n this.#viewSyncerOutboundStream = result.stream;\n break;\n case 'pusher':\n assert(\n this.#pusherOutboundStream === undefined,\n 'Outbound stream already set for this connection!',\n );\n this.#pusherOutboundStream = result.stream;\n break;\n }\n if (result.source === 'viewSyncer') {\n this.#proxyOutbound(result.stream, (downstream, callback) =>\n this.send(downstream.message, callback, downstream.serialized),\n );\n } else {\n this.#proxyOutbound(result.stream, (downstream, callback) =>\n this.send(downstream, callback),\n );\n }\n break;\n }\n case 'transient': {\n for (const error of result.errors) {\n this.sendError(error);\n }\n }\n }\n }\n\n #handleClose = (e: CloseEvent) => {\n const {code, reason, wasClean} = e;\n if (!wasClean) {\n this.#recordWebSocketError('unclean_close');\n }\n this.close('WebSocket close event', {code, reason, wasClean});\n };\n\n #handleError = (e: ErrorEvent) => {\n this.#recordWebSocketError('error_event');\n this.#lc.warn?.('WebSocket error event', e.message, e.error);\n };\n\n #recordWebSocketError(eventType: string) {\n this.#webSocketErrors.add(1, {\n [PROTOCOL_VERSION_ATTRIBUTE]: this.#protocolVersion,\n [EVENT_TYPE_ATTRIBUTE]: eventType,\n });\n }\n\n #proxyInbound() {\n pipeline(\n createWebSocketStream(this.#ws),\n new Writable({\n write: (data, _encoding, callback) => {\n this.#handleMessage({data}).then(() => callback(), callback);\n },\n }),\n // The done callback is not used, as #handleClose and #handleError,\n // configured on the underlying WebSocket, provide more complete\n // information.\n () => {},\n );\n }\n\n #proxyOutbound<T>(\n outboundStream: Source<T>,\n sendMessage: (\n downstream: T,\n callback: (err?: Error | null) => void,\n ) => void,\n ) {\n // Note: createWebSocketStream() is avoided here in order to control\n // exception handling with #closeWithThrown(). If the Writable\n // from createWebSocketStream() were instead used, exceptions\n // from the outboundStream result in the Writable closing the\n // the websocket before the error message can be sent.\n pipeline(\n Readable.from(outboundStream),\n new Writable({\n objectMode: true,\n write: (downstream: T, _encoding, callback) =>\n sendMessage(downstream, callback),\n }),\n e =>\n e\n ? this.#closeWithThrown(e)\n : this.close(`downstream closed by ViewSyncer`),\n );\n }\n\n #closeWithThrown(e: unknown) {\n const errorBody =\n findProtocolError(e)?.errorBody ?? wrapWithProtocolError(e).errorBody;\n\n this.#closeWithError(errorBody, e);\n }\n\n #closeWithError(errorBody: ErrorBody, thrown?: unknown) {\n this.sendError(errorBody, thrown);\n this.close(\n `${errorBody.kind} (${errorBody.origin}): ${errorBody.message}`,\n errorBody,\n );\n }\n\n #lastDownstreamMsgTime = Date.now();\n\n #maybeSendPong = () => {\n if (Date.now() - this.#lastDownstreamMsgTime > DOWNSTREAM_MSG_INTERVAL_MS) {\n this.#lc.debug?.('manually sending pong');\n this.send(['pong', {}], 'ignore-backpressure');\n }\n };\n\n send(\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n ) {\n this.#lastDownstreamMsgTime = Date.now();\n this.#downstreamSender.send(data, callback, serialized);\n }\n\n sendError(errorBody: ErrorBody, thrown?: unknown) {\n sendError(this.#lc, this.#ws, errorBody, thrown);\n }\n}\n\nexport type WebSocketLike = Pick<WebSocket, 'readyState'> & {\n send(data: string | PokeChunk, cb?: (err?: Error) => void): void;\n};\n\n// Exported for testing purposes.\nexport function send(\n lc: LogContext,\n ws: WebSocketLike,\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n) {\n if (ws.readyState === WebSocket.OPEN) {\n serialized ??= JSON.stringify(data);\n if (callback === 'ignore-backpressure') {\n ws.send(serialized);\n return;\n }\n\n let completed = false;\n const timer = setTimeout(() => {\n complete(webSocketSendTimeoutError());\n }, WEBSOCKET_SEND_TIMEOUT_MS);\n timer.unref();\n\n const complete = (error?: Error | null) => {\n if (completed) {\n return;\n }\n completed = true;\n clearTimeout(timer);\n callback(error);\n };\n\n ws.send(serialized, complete);\n } else {\n lc.debug?.(`Dropping outbound message on ws (state: ${ws.readyState})`, {\n dropped: data,\n });\n if (callback !== 'ignore-backpressure') {\n callback(\n new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: 'WebSocket closed',\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n ),\n );\n }\n }\n}\n\n/**\n * Sends downstream messages in the format supported by a connection's sync\n * protocol version. Keep version forks contained here so the view-syncer can\n * continue producing one canonical poke representation.\n *\n * Exported for compatibility testing.\n */\nexport class DownstreamSender {\n readonly #lc: LogContext;\n readonly #ws: WebSocketLike;\n readonly #protocolVersion: number;\n #pokeChunkEncoder: PokeChunkEncoder | undefined;\n\n constructor(lc: LogContext, ws: WebSocketLike, protocolVersion: number) {\n this.#lc = lc;\n this.#ws = ws;\n this.#protocolVersion = protocolVersion;\n }\n\n send(\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n ): void {\n if (this.#protocolVersion >= POKE_CHUNK_PROTOCOL_VERSION) {\n switch (data[0]) {\n case 'pokeStart':\n assert(\n this.#pokeChunkEncoder === undefined,\n 'cannot start a poke while another poke is in progress',\n );\n this.#pokeChunkEncoder = new PokeChunkEncoder();\n break;\n case 'pokePart':\n this.#sendPokePart(data, callback, serialized);\n return;\n case 'pokeEnd':\n this.#sendPokeEnd(data, callback);\n return;\n }\n }\n send(this.#lc, this.#ws, data, callback, serialized);\n }\n\n #sendPokePart(\n pokePart: PokePartMessage,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n ): void {\n const encoder = this.#pokeChunkEncoder;\n assert(encoder, 'pokePart received without a pokeStart');\n\n serialized ??= JSON.stringify(pokePart);\n assert(\n serialized.startsWith(POKE_PART_PREFIX) && serialized.endsWith(']'),\n 'invalid serialized pokePart',\n );\n void encoder\n .addPatch(serialized.slice(POKE_PART_PREFIX.length, -1), chunk =>\n sendBinary(this.#lc, this.#ws, chunk),\n )\n .then(\n () => invokeCallback(callback),\n error => invokeCallback(callback, toError(error)),\n );\n }\n\n #sendPokeEnd(\n pokeEnd: PokeEndMessage,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n ): void {\n const encoder = this.#pokeChunkEncoder;\n assert(encoder, 'pokeEnd received without a pokeStart');\n this.#pokeChunkEncoder = undefined;\n\n if (pokeEnd[1].cancel) {\n encoder.cancel();\n send(this.#lc, this.#ws, pokeEnd, callback);\n return;\n }\n\n void encoder\n .finish(chunk => sendBinary(this.#lc, this.#ws, chunk))\n .then(() => sendAsync(this.#lc, this.#ws, pokeEnd))\n .then(\n () => invokeCallback(callback),\n error => invokeCallback(callback, toError(error)),\n );\n }\n}\n\nconst POKE_PART_PREFIX = '[\"pokePart\",';\n\nfunction sendAsync(\n lc: LogContext,\n ws: WebSocketLike,\n data: Downstream,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n send(lc, ws, data, error => (error ? reject(error) : resolve()));\n });\n}\n\nfunction sendBinary(\n lc: LogContext,\n ws: WebSocketLike,\n chunk: PokeChunk,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n if (ws.readyState !== WebSocket.OPEN) {\n const error = webSocketClosedError();\n lc.debug?.(\n `Dropping outbound binary message on ws (state: ${ws.readyState})`,\n );\n reject(error);\n return;\n }\n let completed = false;\n const timer = setTimeout(\n () => complete(webSocketSendTimeoutError()),\n WEBSOCKET_SEND_TIMEOUT_MS,\n );\n timer.unref();\n\n const complete = (error?: Error | null) => {\n if (completed) {\n return;\n }\n completed = true;\n clearTimeout(timer);\n error ? reject(error) : resolve();\n };\n\n ws.send(chunk, complete);\n });\n}\n\nfunction invokeCallback(\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n error?: Error,\n): void {\n if (callback !== 'ignore-backpressure') {\n callback(error);\n }\n}\n\nfunction toError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n\nfunction webSocketClosedError(): ProtocolErrorWithLevel {\n return new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: 'WebSocket closed',\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n );\n}\n\nfunction webSocketSendTimeoutError(): ProtocolErrorWithLevel {\n return new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: `WebSocket send timed out after ${WEBSOCKET_SEND_TIMEOUT_MS} ms`,\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n );\n}\n\nexport function sendError(\n lc: LogContext,\n ws: WebSocket,\n errorBody: ErrorBody,\n thrown?: unknown,\n) {\n lc = lc.withContext('errorKind', errorBody.kind);\n\n let logLevel: LogLevel;\n\n // If the thrown error is a ProtocolErrorWithLevel, its explicit logLevel takes precedence\n if (thrown instanceof ProtocolErrorWithLevel) {\n logLevel = thrown.logLevel;\n }\n // Errors with errno or transient socket codes are low-level, transient I/O issues\n // (e.g., EPIPE, ECONNRESET) and should be warnings, not errors\n else if (\n hasErrno(thrown) ||\n hasTransientSocketCode(thrown) ||\n isTransientSocketMessage(errorBody.message)\n ) {\n logLevel = 'warn';\n }\n // Fallback: check errorBody.kind for errors that weren't thrown as ProtocolErrorWithLevel\n else if (\n errorBody.kind === ErrorKind.ClientNotFound ||\n errorBody.kind === ErrorKind.TransformFailed\n ) {\n logLevel = 'warn';\n } else {\n logLevel = thrown ? getLogLevel(thrown) : 'info';\n }\n\n lc[logLevel]?.('Sending error on WebSocket', errorBody, thrown ?? '');\n send(lc, ws, ['error', errorBody], 'ignore-backpressure');\n}\n\nexport function findProtocolError(error: unknown): ProtocolError | undefined {\n if (isProtocolError(error)) {\n return error;\n }\n if (error instanceof Error && error.cause) {\n return findProtocolError(error.cause);\n }\n return undefined;\n}\n\nfunction hasErrno(error: unknown): boolean {\n return Boolean(\n error &&\n typeof error === 'object' &&\n 'errno' in error &&\n typeof (error as {errno: unknown}).errno !== 'undefined',\n );\n}\n\n// System error codes that indicate transient socket conditions.\n// These are checked via the `code` property on errors.\nconst TRANSIENT_SOCKET_ERROR_CODES = new Set([\n 'EPIPE',\n 'ECONNRESET',\n 'ECANCELED',\n]);\n\n// Error messages that indicate transient socket conditions but don't have\n// standard error codes (e.g., WebSocket library errors).\nconst TRANSIENT_SOCKET_MESSAGE_PATTERNS = [\n 'socket was closed while data was being compressed',\n];\n\nfunction hasTransientSocketCode(error: unknown): boolean {\n if (!error || typeof error !== 'object') {\n return false;\n }\n const maybeCode =\n 'code' in error ? String((error as {code?: unknown}).code) : undefined;\n return Boolean(\n maybeCode && TRANSIENT_SOCKET_ERROR_CODES.has(maybeCode.toUpperCase()),\n );\n}\n\nfunction isTransientSocketMessage(message: string | undefined): boolean {\n if (!message) {\n return false;\n }\n const lower = message.toLowerCase();\n return TRANSIENT_SOCKET_MESSAGE_PATTERNS.some(pattern =>\n lower.includes(pattern),\n );\n}\n"],"mappings":"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"}
@@ -14,7 +14,7 @@ import { DrainCoordinator } from "../services/view-syncer/drain-coordinator.js";
14
14
  import { Connection, sendError } from "./connection.js";
15
15
  import { SyncerWsMessageHandler } from "./syncer-ws-message-handler.js";
16
16
  import { resolver } from "@rocicorp/resolver";
17
- import { WebSocket, WebSocketServer } from "ws";
17
+ import { WebSocket as WebSocket$1, WebSocketServer } from "ws";
18
18
  import { pid } from "node:process";
19
19
  //#region ../zero-cache/src/workers/syncer.ts
20
20
  function computePipelineDedupStats(viewSyncers) {
@@ -361,7 +361,7 @@ var Syncer = class {
361
361
  recordConnectionFailure("internal");
362
362
  throw e;
363
363
  }
364
- if (ws.readyState !== WebSocket.OPEN) {
364
+ if (ws.readyState !== WebSocket$1.OPEN) {
365
365
  this.#lc.debug?.("websocket closed while resolving auth", {
366
366
  clientGroupID,
367
367
  clientID,
@@ -1 +1 @@
1
- {"version":3,"file":"syncer.js","names":["#lc","#viewSyncers","#mutagens","#pushers","#connections","#pendingConnections","#clientGroupGenerations","#drainCoordinator","#parent","#wss","#stopped","#config","#validateLegacyJWT","#replicaReadyStates","#webSocketOpenConnections","#webSocketConnectionAttempts","#webSocketConnectionSuccesses","#webSocketConnectionFailures","#viewSyncerLag","#recordReplicaReadyState","#receiveConnection","#releasePending","#computeServingLagDistribution","#computePipelineDedupStats","#viewSyncerLagSampleInterval","#recordViewSyncerLagSamples","#eligibilityLogInterval","#logSharedAdvanceEligibility","#servingLagDistributionCache","#servingLagDistributionCacheClearQueued","#createConnection"],"sources":["../../../../../zero-cache/src/workers/syncer.ts"],"sourcesContent":["import {pid} from 'node:process';\nimport type {MessagePort} from 'node:worker_threads';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {WebSocket, WebSocketServer, type ServerOptions} from 'ws';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport {\n isProtocolError,\n ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {resolveAuth, type Auth, type ValidateLegacyJWT} from '../auth/auth.ts';\nimport {tokenConfigOptions} from '../auth/jwt.ts';\nimport {type ZeroConfig} from '../config/zero-config.ts';\nimport {\n getOrCreateCounter,\n getOrCreateGauge,\n getOrCreateNativeHistogram,\n getOrCreateUpDownCounter,\n} from '../observability/metrics.ts';\nimport {\n recordConnectionAttempted,\n recordConnectionSuccess,\n setActiveClientGroupsGetter,\n} from '../server/anonymous-otel-start.ts';\nimport type {Mutagen} from '../services/mutagen/mutagen.ts';\nimport type {Pusher} from '../services/mutagen/pusher.ts';\nimport type {ReplicaState} from '../services/replicator/replicator.ts';\nimport {ServiceRunner} from '../services/runner.ts';\nimport type {\n ActivityBasedService,\n Service,\n SingletonService,\n} from '../services/service.ts';\nimport type {ConnectionContextManager} from '../services/view-syncer/connection-context-manager.ts';\nimport {DrainCoordinator} from '../services/view-syncer/drain-coordinator.ts';\nimport type {ViewSyncer} from '../services/view-syncer/view-syncer.ts';\nimport type {ClientGroupStatusMessage, Worker} from '../types/processes.ts';\nimport type {Subscription} from '../types/subscription.ts';\nimport {installWebSocketReceiver} from '../types/websocket-handoff.ts';\nimport type {ConnectParams} from './connect-params.ts';\nimport {Connection, sendError} from './connection.ts';\nimport {createNotifierFrom, subscribeTo} from './replicator.ts';\nimport {SyncerWsMessageHandler} from './syncer-ws-message-handler.ts';\n\nexport type SyncerWorkerData = {\n replicatorPort: MessagePort;\n};\n\nexport type ReplicaReadyState = {\n readonly watermark: string;\n readonly replicaReadyTimeMs: number;\n};\n\nexport type ServingLagViewSyncer = Pick<\n ViewSyncer,\n 'createdAtMs' | 'servedVersion' | 'servingLagEligible'\n>;\n\nexport type ServingLagStats = {\n readonly activeClientGroups: number;\n readonly laggingClientGroups: number;\n readonly minMs: number;\n readonly p50Ms: number;\n readonly p75Ms: number;\n readonly p99Ms: number;\n readonly maxMs: number;\n};\n\ntype ServingLagDistribution = ServingLagStats & {\n readonly lagsMs: readonly number[];\n};\n\nexport type PipelineDedupViewSyncer = Pick<\n ViewSyncer,\n 'pipelineHashes' | 'clientSchemaKey'\n>;\n\nexport type PipelineDedupStats = {\n readonly clientTotal: number;\n readonly internalTotal: number;\n // (clientSchemaKey, transformationHash) -> occurrence count across client\n // groups, for client/custom queries.\n readonly clientHashes: ReadonlyMap<\n string,\n {count: number; queryName: string | undefined}\n >;\n readonly internalUnique: number;\n readonly clientSchemas: number;\n};\n\nexport function computePipelineDedupStats(\n viewSyncers: Iterable<PipelineDedupViewSyncer>,\n): PipelineDedupStats {\n let clientTotal = 0;\n let internalTotal = 0;\n const clientHashes = new Map<\n string,\n {count: number; queryName: string | undefined}\n >();\n const internalHashes = new Set<string>();\n const schemaKeys = new Set<string>();\n let vsIndex = 0;\n for (const vs of viewSyncers) {\n const schemaKey = vs.clientSchemaKey;\n if (schemaKey !== undefined) {\n schemaKeys.add(schemaKey);\n }\n // Without a client schema the pipeline's dedup identity is unknown, so key\n // it uniquely per view-syncer; otherwise schema-less pipelines from\n // unrelated client groups would collapse under a shared `undefined/<hash>`\n // key and overstate deduplication.\n const dedupSchemaKey = schemaKey ?? `#vs${vsIndex}`;\n vsIndex++;\n for (const {\n transformationHash,\n internal,\n queryName,\n } of vs.pipelineHashes()) {\n // Pipelines only share when both the client schema (which determines\n // row keys) and the transformed AST are identical.\n const key = `${dedupSchemaKey}/${transformationHash}`;\n if (internal) {\n internalTotal++;\n internalHashes.add(key);\n } else {\n clientTotal++;\n const entry = clientHashes.get(key);\n if (entry) {\n entry.count++;\n } else {\n clientHashes.set(key, {count: 1, queryName});\n }\n }\n }\n }\n return {\n clientTotal,\n internalTotal,\n clientHashes,\n internalUnique: internalHashes.size,\n clientSchemas: schemaKeys.size,\n };\n}\n\nexport const MAX_REPLICA_READY_STATES = 10_000;\nconst VIEW_SYNCER_LAG_SAMPLE_INTERVAL_MS = 60_000;\nconst SHARED_ADVANCE_ELIGIBILITY_LOG_INTERVAL_MS = 5 * 60_000;\n\nconst PROTOCOL_VERSION_ATTRIBUTE = 'protocol.version';\nconst FAILURE_REASON_ATTRIBUTE = 'reason';\n\nfunction boundReplicaReadyStates(\n replicaReadyStates: ReplicaReadyState[],\n): void {\n if (replicaReadyStates.length > MAX_REPLICA_READY_STATES) {\n replicaReadyStates.splice(\n 0,\n replicaReadyStates.length - MAX_REPLICA_READY_STATES,\n );\n }\n}\n\nfunction pruneReplicaReadyStates(\n replicaReadyStates: ReplicaReadyState[],\n firstNeededIndex: number,\n): void {\n if (firstNeededIndex > 0) {\n replicaReadyStates.splice(0, firstNeededIndex);\n }\n\n boundReplicaReadyStates(replicaReadyStates);\n}\n\nfunction lowerBoundReplicaReadyTimeMs(\n replicaReadyStates: readonly ReplicaReadyState[],\n replicaReadyTimeMs: number,\n): number {\n let low = 0;\n let high = replicaReadyStates.length;\n while (low < high) {\n const mid = low + Math.floor((high - low) / 2);\n if (replicaReadyStates[mid].replicaReadyTimeMs < replicaReadyTimeMs) {\n low = mid + 1;\n } else {\n high = mid;\n }\n }\n return low;\n}\n\nfunction upperBoundWatermark(\n replicaReadyStates: readonly ReplicaReadyState[],\n watermark: string,\n): number {\n let low = 0;\n let high = replicaReadyStates.length;\n while (low < high) {\n const mid = low + Math.floor((high - low) / 2);\n if (replicaReadyStates[mid].watermark <= watermark) {\n low = mid + 1;\n } else {\n high = mid;\n }\n }\n return low;\n}\n\nfunction findFirstUnservedIndex(\n replicaReadyStates: readonly ReplicaReadyState[],\n viewSyncer: ServingLagViewSyncer,\n): number {\n const firstReadyAfterCreation = lowerBoundReplicaReadyTimeMs(\n replicaReadyStates,\n viewSyncer.createdAtMs,\n );\n const firstAfterServedVersion =\n viewSyncer.servedVersion === null\n ? 0\n : upperBoundWatermark(replicaReadyStates, viewSyncer.servedVersion);\n\n const firstUnservedIndex = Math.max(\n firstReadyAfterCreation,\n firstAfterServedVersion,\n );\n return firstUnservedIndex < replicaReadyStates.length\n ? firstUnservedIndex\n : -1;\n}\n\nfunction percentileNearestRank(\n sortedValues: readonly number[],\n percentile: number,\n): number {\n if (sortedValues.length === 0) {\n return 0;\n }\n const index = Math.min(\n sortedValues.length - 1,\n Math.max(0, Math.ceil((percentile / 100) * sortedValues.length) - 1),\n );\n return sortedValues[index];\n}\n\nfunction computeServingLagDistributionMs(\n now: number,\n replicaReadyStates: ReplicaReadyState[],\n viewSyncers: Iterable<ServingLagViewSyncer>,\n): ServingLagDistribution {\n const lags: number[] = [];\n let laggingClientGroups = 0;\n let firstNeededIndex = replicaReadyStates.length;\n\n for (const viewSyncer of viewSyncers) {\n if (!viewSyncer.servingLagEligible) {\n continue;\n }\n\n const firstUnservedIndex = findFirstUnservedIndex(\n replicaReadyStates,\n viewSyncer,\n );\n if (firstUnservedIndex === -1) {\n lags.push(0);\n continue;\n }\n\n firstNeededIndex = Math.min(firstNeededIndex, firstUnservedIndex);\n const lagMs = Math.max(\n 0,\n now - replicaReadyStates[firstUnservedIndex].replicaReadyTimeMs,\n );\n lags.push(lagMs);\n if (lagMs > 0) {\n laggingClientGroups++;\n }\n }\n\n pruneReplicaReadyStates(replicaReadyStates, firstNeededIndex);\n\n lags.sort((a, b) => a - b);\n return {\n activeClientGroups: lags.length,\n laggingClientGroups,\n // The percentile fields are for the legacy serving_lag_stats gauge.\n // The native view_syncer_lag histogram records lagsMs and computes\n // percentiles in Prometheus/AMP.\n minMs: lags[0] ?? 0,\n p50Ms: percentileNearestRank(lags, 50),\n p75Ms: percentileNearestRank(lags, 75),\n p99Ms: percentileNearestRank(lags, 99),\n maxMs: lags.at(-1) ?? 0,\n lagsMs: lags,\n };\n}\n\nexport function computeServingLagStatsMs(\n now: number,\n replicaReadyStates: ReplicaReadyState[],\n viewSyncers: Iterable<ServingLagViewSyncer>,\n): ServingLagStats {\n const distribution = computeServingLagDistributionMs(\n now,\n replicaReadyStates,\n viewSyncers,\n );\n return {\n activeClientGroups: distribution.activeClientGroups,\n laggingClientGroups: distribution.laggingClientGroups,\n minMs: distribution.minMs,\n p50Ms: distribution.p50Ms,\n p75Ms: distribution.p75Ms,\n p99Ms: distribution.p99Ms,\n maxMs: distribution.maxMs,\n };\n}\n\nexport function computeMaxServingLagMs(\n now: number,\n replicaReadyStates: ReplicaReadyState[],\n viewSyncers: Iterable<ServingLagViewSyncer>,\n): number {\n return computeServingLagStatsMs(now, replicaReadyStates, viewSyncers).maxMs;\n}\n\nfunction getWebSocketServerOptions(config: ZeroConfig): ServerOptions {\n const options: ServerOptions = {\n noServer: true,\n maxPayload: config.websocketMaxPayloadBytes,\n };\n\n if (config.websocketCompression) {\n options.perMessageDeflate = true;\n\n if (config.websocketCompressionOptions) {\n try {\n const compressionOptions = JSON.parse(\n config.websocketCompressionOptions,\n );\n options.perMessageDeflate = compressionOptions;\n } catch (e) {\n throw new Error(\n `Failed to parse ZERO_WEBSOCKET_COMPRESSION_OPTIONS: ${String(e)}. Expected valid JSON.`,\n );\n }\n }\n }\n\n return options;\n}\n\n/**\n * The Syncer worker receives websocket handoffs for \"/sync\" connections\n * from the Dispatcher in the main thread, and creates websocket\n * {@link Connection}s with a corresponding {@link ViewSyncer}, {@link Mutagen},\n * and {@link Subscription} to version notifications from the Replicator\n * worker.\n */\nexport class Syncer implements SingletonService {\n readonly id = `syncer-${pid}`;\n readonly #lc: LogContext;\n readonly #viewSyncers: ServiceRunner<ViewSyncer & ActivityBasedService>;\n readonly #mutagens: ServiceRunner<Mutagen & Service> | undefined;\n readonly #pushers: ServiceRunner<Pusher & Service> | undefined;\n readonly #connections = new Map<string, Connection>();\n readonly #pendingConnections = new Map<string, number>();\n readonly #clientGroupGenerations = new Map<string, number>();\n readonly #drainCoordinator = new DrainCoordinator();\n readonly #parent: Worker;\n readonly #wss: WebSocketServer;\n readonly #stopped = resolver();\n readonly #config: ZeroConfig;\n readonly #validateLegacyJWT: ValidateLegacyJWT | undefined;\n readonly #replicaReadyStates: ReplicaReadyState[] = [];\n readonly #webSocketOpenConnections = getOrCreateUpDownCounter(\n 'sync',\n 'websocket.open_connections',\n 'Open client WebSocket connections.',\n );\n readonly #webSocketConnectionAttempts = getOrCreateCounter(\n 'sync',\n 'websocket.connection_attempts',\n 'Client WebSocket connection attempts.',\n );\n readonly #webSocketConnectionSuccesses = getOrCreateCounter(\n 'sync',\n 'websocket.connection_successes',\n 'Client WebSocket connections successfully initialized.',\n );\n readonly #webSocketConnectionFailures = getOrCreateCounter(\n 'sync',\n 'websocket.connection_failures',\n 'Client WebSocket connection attempts that failed before initialization.',\n );\n readonly #viewSyncerLag = getOrCreateNativeHistogram(\n 'sync',\n 'view_syncer_lag',\n {\n description:\n 'Lag from replica-ready change to ViewSyncer output for active ' +\n 'client groups. A change is output after IVM advancement, CVR flush, ' +\n 'and pokeEnd.',\n unit: 's',\n },\n );\n #servingLagDistributionCache: ServingLagDistribution | undefined;\n #servingLagDistributionCacheClearQueued = false;\n #viewSyncerLagSampleInterval: ReturnType<typeof setInterval> | undefined;\n #eligibilityLogInterval: ReturnType<typeof setInterval> | undefined;\n\n constructor(\n lc: LogContext,\n config: ZeroConfig,\n viewSyncerFactory: (\n id: string,\n sub: Subscription<ReplicaState>,\n drainCoordinator: DrainCoordinator,\n ) => ViewSyncer & ActivityBasedService,\n mutagenFactory: ((id: string) => Mutagen & Service) | undefined,\n pusherFactory:\n | ((\n id: string,\n connContextManager: ConnectionContextManager,\n ) => Pusher & Service)\n | undefined,\n parent: Worker,\n validateLegacyJWT: ValidateLegacyJWT | undefined,\n ) {\n this.#config = config;\n this.#validateLegacyJWT = validateLegacyJWT;\n // Relays notifications from the parent thread subscription\n // to ViewSyncers within this thread.\n const notifier = createNotifierFrom(lc, parent, state =>\n this.#recordReplicaReadyState(state),\n );\n subscribeTo(lc, parent);\n\n this.#lc = lc;\n this.#parent = parent;\n this.#viewSyncers = new ServiceRunner(\n lc,\n id => viewSyncerFactory(id, notifier.subscribe(), this.#drainCoordinator),\n v => v.keepalive(),\n id => {\n if ((this.#pendingConnections.get(id) ?? 0) === 0) {\n const generation = this.#clientGroupGenerations.get(id);\n this.#parent.send<ClientGroupStatusMessage>([\n 'clientGroupStatus',\n {\n clientGroupID: id,\n active: false,\n ...(generation !== undefined ? {generation} : {}),\n },\n ]);\n this.#clientGroupGenerations.delete(id);\n }\n },\n );\n if (mutagenFactory) {\n this.#mutagens = new ServiceRunner(lc, mutagenFactory, m => m.hasRefs());\n }\n if (pusherFactory) {\n this.#pushers = new ServiceRunner(\n lc,\n id =>\n pusherFactory(\n id,\n this.#viewSyncers.getService(id).connContextManager,\n ),\n p => p.hasRefs(),\n );\n }\n this.#wss = new WebSocketServer(getWebSocketServerOptions(config));\n\n installWebSocketReceiver(\n lc,\n this.#wss,\n this.#receiveConnection,\n this.#parent,\n params => {\n this.#releasePending(params.clientGroupID, params.generation);\n },\n params => {\n this.#pendingConnections.set(\n params.clientGroupID,\n (this.#pendingConnections.get(params.clientGroupID) ?? 0) + 1,\n );\n if (params.generation !== undefined) {\n const current = this.#clientGroupGenerations.get(\n params.clientGroupID,\n );\n if (current === undefined || params.generation > current) {\n this.#clientGroupGenerations.set(\n params.clientGroupID,\n params.generation,\n );\n }\n }\n },\n );\n\n setActiveClientGroupsGetter(() => this.#viewSyncers.size);\n\n getOrCreateGauge(\n 'sync',\n 'active-client-groups',\n 'Number of active client groups',\n ).addCallback(result => result.observe(this.#viewSyncers.size));\n\n getOrCreateGauge(\n 'sync',\n 'queries',\n 'Active queries (pipelines) across all client groups',\n ).addCallback(result => {\n let total = 0;\n for (const vs of this.#viewSyncers.getServices()) {\n total += vs.queryCount;\n }\n result.observe(total);\n });\n\n getOrCreateGauge(\n 'sync',\n 'rows',\n 'Tracked rows across all client groups',\n ).addCallback(result => {\n let total = 0;\n for (const vs of this.#viewSyncers.getServices()) {\n total += vs.rowCount;\n }\n result.observe(total);\n });\n\n getOrCreateGauge('sync', 'serving_lag', {\n description:\n 'Maximum time active ViewSyncer client groups have had unserved ' +\n 'replica changes. A change is served after IVM advancement, CVR flush, ' +\n 'and pokeEnd.',\n unit: 'millisecond',\n }).addCallback(result => {\n result.observe(this.#computeServingLagDistribution().maxMs);\n });\n\n getOrCreateGauge('sync', 'serving_lag_stats', {\n description:\n 'Distribution of time active ViewSyncer client groups have had ' +\n 'unserved replica changes. A change is served after IVM advancement, ' +\n 'CVR flush, and pokeEnd.',\n unit: 'millisecond',\n }).addCallback(result => {\n const stats = this.#computeServingLagDistribution();\n result.observe(stats.minMs, {stat: 'min'});\n result.observe(stats.p50Ms, {stat: 'p50'});\n result.observe(stats.p75Ms, {stat: 'p75'});\n result.observe(stats.p99Ms, {stat: 'p99'});\n result.observe(stats.maxMs, {stat: 'max'});\n });\n\n getOrCreateGauge(\n 'sync',\n 'serving_lagging_client_groups',\n 'Number of active client groups with unserved replica changes',\n ).addCallback(result => {\n result.observe(this.#computeServingLagDistribution().laggingClientGroups);\n });\n\n getOrCreateGauge(\n 'sync',\n 'pipelines_total',\n 'Query pipelines across all client groups on this worker, by query ' +\n 'type. The ratio to sync.pipelines_unique is the dedup factor ' +\n 'available to shared pipeline advancement.',\n ).addCallback(result => {\n const stats = this.#computePipelineDedupStats();\n result.observe(stats.clientTotal, {type: 'client'});\n result.observe(stats.internalTotal, {type: 'internal'});\n });\n\n getOrCreateGauge(\n 'sync',\n 'pipelines_unique',\n 'Distinct (clientSchema, transformationHash) pipelines across all ' +\n 'client groups on this worker, by query type',\n ).addCallback(result => {\n const stats = this.#computePipelineDedupStats();\n result.observe(stats.clientHashes.size, {type: 'client'});\n result.observe(stats.internalUnique, {type: 'internal'});\n });\n\n getOrCreateGauge(\n 'sync',\n 'client_schemas',\n 'Distinct client schemas across all client groups on this worker',\n ).addCallback(result => {\n result.observe(this.#computePipelineDedupStats().clientSchemas);\n });\n\n this.#viewSyncerLagSampleInterval = setInterval(\n () => this.#recordViewSyncerLagSamples(),\n VIEW_SYNCER_LAG_SAMPLE_INTERVAL_MS,\n );\n this.#viewSyncerLagSampleInterval.unref?.();\n\n this.#eligibilityLogInterval = setInterval(\n () => this.#logSharedAdvanceEligibility(),\n SHARED_ADVANCE_ELIGIBILITY_LOG_INTERVAL_MS,\n );\n this.#eligibilityLogInterval.unref?.();\n }\n\n #computeServingLagDistribution(): ServingLagDistribution {\n if (this.#servingLagDistributionCache) {\n return this.#servingLagDistributionCache;\n }\n\n const distribution = computeServingLagDistributionMs(\n Date.now(),\n this.#replicaReadyStates,\n this.#viewSyncers.getServices(),\n );\n this.#servingLagDistributionCache = distribution;\n if (!this.#servingLagDistributionCacheClearQueued) {\n this.#servingLagDistributionCacheClearQueued = true;\n queueMicrotask(() => {\n this.#servingLagDistributionCache = undefined;\n this.#servingLagDistributionCacheClearQueued = false;\n });\n }\n return distribution;\n }\n\n #recordViewSyncerLagSamples(): void {\n const {lagsMs} = this.#computeServingLagDistribution();\n for (const lagMs of lagsMs) {\n this.#viewSyncerLag.recordMs(lagMs);\n }\n }\n\n #computePipelineDedupStats(): PipelineDedupStats {\n return computePipelineDedupStats(this.#viewSyncers.getServices());\n }\n\n #logSharedAdvanceEligibility(): void {\n const stats = this.#computePipelineDedupStats();\n if (stats.clientTotal === 0) {\n return;\n }\n const unique = stats.clientHashes.size;\n const topDuplicated = [...stats.clientHashes.entries()]\n .filter(([, {count}]) => count > 1)\n .sort((a, b) => b[1].count - a[1].count)\n .slice(0, 10)\n .map(([key, {count, queryName}]) => ({key, count, queryName}));\n this.#lc.info?.('shared-advance-eligibility', {\n clientPipelines: stats.clientTotal,\n uniqueClientPipelines: unique,\n dedupFactor: Number((stats.clientTotal / Math.max(unique, 1)).toFixed(2)),\n internalPipelines: stats.internalTotal,\n clientSchemas: stats.clientSchemas,\n topDuplicated,\n });\n }\n\n #recordReplicaReadyState(state: ReplicaState): void {\n if (\n state.watermark === undefined ||\n state.replicaReadyTimeMs === undefined\n ) {\n return;\n }\n const last = this.#replicaReadyStates.at(-1);\n if (last && last.watermark >= state.watermark) {\n return;\n }\n this.#replicaReadyStates.push({\n watermark: state.watermark,\n replicaReadyTimeMs: state.replicaReadyTimeMs,\n });\n if (this.#viewSyncers.size === 0) {\n this.#replicaReadyStates.length = 0;\n return;\n }\n boundReplicaReadyStates(this.#replicaReadyStates);\n }\n\n readonly #createConnection = async (ws: WebSocket, params: ConnectParams) => {\n const metricAttributes = {\n [PROTOCOL_VERSION_ATTRIBUTE]: params.protocolVersion,\n };\n let connectionResultRecorded = false;\n const recordConnectionFailure = (reason: string) => {\n if (connectionResultRecorded) {\n return;\n }\n connectionResultRecorded = true;\n this.#webSocketConnectionFailures.add(1, {\n ...metricAttributes,\n [FAILURE_REASON_ATTRIBUTE]: reason,\n });\n };\n const recordConnectionSuccessMetric = () => {\n if (connectionResultRecorded) {\n return;\n }\n connectionResultRecorded = true;\n this.#webSocketConnectionSuccesses.add(1, metricAttributes);\n };\n\n this.#webSocketConnectionAttempts.add(1, metricAttributes);\n this.#webSocketOpenConnections.add(1, metricAttributes);\n ws.once('close', () => {\n this.#webSocketOpenConnections.add(-1, metricAttributes);\n });\n\n this.#lc.debug?.(\n 'creating connection',\n params.clientGroupID,\n params.clientID,\n );\n recordConnectionAttempted();\n const {clientID, clientGroupID, auth, userID} = params;\n const hasProvidedAuth = auth !== undefined && auth !== '';\n const incomingUserID = userID ?? null;\n\n if (hasProvidedAuth) {\n const tokenOptions = tokenConfigOptions(this.#config.auth ?? {});\n\n const hasPushOrMutate =\n this.#config?.push?.url !== undefined ||\n this.#config?.mutate?.url !== undefined;\n const hasQueries =\n this.#config?.query?.url !== undefined ||\n this.#config?.getQueries?.url !== undefined;\n\n // must either have one of the token options set or have custom mutations & queries enabled\n const hasExactlyOneTokenOption = tokenOptions.length === 1;\n const hasCustomEndpoints = hasPushOrMutate && hasQueries;\n if (!hasExactlyOneTokenOption && !hasCustomEndpoints) {\n recordConnectionFailure('configuration');\n throw new Error(\n 'Exactly one of jwk, secret, or jwksUrl must be set in order to verify tokens but actually the following were set: ' +\n JSON.stringify(tokenOptions) +\n '. You may also set both ZERO_MUTATE_URL and ZERO_QUERY_URL to enable custom mutations and queries without passing token verification options.',\n );\n }\n }\n\n let initialAuth: Auth | undefined;\n\n // Verify JWT BEFORE touching existing connections - prevents unauthenticated\n // attackers from force-disconnecting legitimate users via DoS.\n try {\n initialAuth = await resolveAuth(\n this.#lc\n .withContext('clientGroupID', clientGroupID)\n .withContext('clientID', clientID),\n // no previous auth, since this is a new connection, and resolveAuth is\n // connection scoped, not client group scoped\n undefined,\n incomingUserID,\n auth,\n this.#validateLegacyJWT,\n );\n } catch (e) {\n if (isProtocolError(e)) {\n this.#lc.warn?.(\n 'Rejecting sync connection during initial auth resolution',\n {\n clientGroupID,\n clientID,\n incomingUserID,\n hasProvidedAuth,\n errorKind: e.message,\n },\n );\n recordConnectionFailure('auth');\n sendError(this.#lc, ws, e.errorBody);\n ws.close(3000, e.errorBody.message);\n return;\n }\n recordConnectionFailure('internal');\n throw e;\n }\n\n // Resolving auth can take a while (e.g. a remote JWKS fetch), during\n // which the client may have disconnected. A Connection created for a\n // closed socket never receives the 'close' event and is thus never\n // cleaned up, leaking its timer, its registrations in the connection\n // context manager and its refs on the mutagen and pusher services.\n if (ws.readyState !== WebSocket.OPEN) {\n this.#lc.debug?.('websocket closed while resolving auth', {\n clientGroupID,\n clientID,\n readyState: ws.readyState,\n });\n recordConnectionFailure('closed_during_auth');\n return;\n }\n\n const viewSyncer = this.#viewSyncers.getService(clientGroupID);\n const connContextManager = viewSyncer.connContextManager;\n const group = connContextManager.getGroupState();\n\n // TODO(0xcadams): we only check for user ID mismatch here if the group is\n // already validated. This prevents wrong-user reconnects from evicting a\n // healthy connection, but it does not protect against same-user reconnects\n // with an invalid opaque token. The long-term fix is to keep the replacement\n // connection pending until its auth is fully validated, and only then replace\n // the existing socket.\n if (\n group.pinnedUser !== undefined &&\n group.pinnedUser.id !== incomingUserID\n ) {\n const error = new ProtocolError({\n kind: ErrorKind.Unauthorized,\n message:\n 'Client groups are pinned to a single userID. Connection userID does not match existing client group userID.',\n origin: ErrorOrigin.ZeroCache,\n });\n recordConnectionFailure('user_mismatch');\n sendError(this.#lc, ws, error.errorBody);\n ws.close(3000, error.message);\n return;\n }\n\n // Check for and close existing connections AFTER auth is validated\n const existing = this.#connections.get(clientID);\n if (existing) {\n this.#lc.debug?.(\n `client ${clientID} already connected, closing existing connection`,\n );\n existing.close(`replaced by ${params.wsID}`);\n }\n\n connContextManager.registerConnection(\n {clientID, wsID: params.wsID},\n params,\n initialAuth,\n );\n\n const mutagen = this.#mutagens?.getService(clientGroupID);\n const pusher = this.#pushers?.getService(clientGroupID);\n // a new connection is using the mutagen and pusher. Bump their ref counts.\n mutagen?.ref();\n pusher?.ref();\n\n let connection: Connection;\n try {\n connection = new Connection(\n this.#lc,\n params,\n ws,\n this.#config.allowLegacyQueries,\n new SyncerWsMessageHandler(\n this.#lc,\n params,\n connContextManager,\n viewSyncer,\n mutagen,\n pusher,\n ),\n () => {\n connContextManager.closeConnection({\n clientID,\n wsID: params.wsID,\n });\n if (this.#connections.get(clientID) === connection) {\n this.#connections.delete(clientID);\n }\n // Connection is closed. We can unref the mutagen and pusher.\n // If their ref counts are zero, they will stop themselves and set themselves invalid.\n mutagen?.unref();\n pusher?.unref();\n },\n );\n } catch (e) {\n recordConnectionFailure('internal');\n connContextManager.closeConnection({clientID, wsID: params.wsID});\n mutagen?.unref();\n pusher?.unref();\n throw e;\n }\n\n this.#connections.set(clientID, connection);\n\n if (connection.init()) {\n recordConnectionSuccessMetric();\n recordConnectionSuccess();\n } else {\n recordConnectionFailure('protocol_version');\n }\n\n if (params.initConnectionMsg) {\n this.#lc.debug?.(\n 'handling init connection message from sec header',\n params.clientGroupID,\n params.clientID,\n );\n await connection.handleInitConnection(\n JSON.stringify(params.initConnectionMsg),\n );\n }\n };\n\n readonly #receiveConnection = async (\n ws: WebSocket,\n params: ConnectParams,\n ) => {\n try {\n await this.#createConnection(ws, params);\n } finally {\n this.#releasePending(params.clientGroupID, params.generation);\n }\n };\n\n #releasePending(clientGroupID: string, generation: number | undefined): void {\n const remaining = (this.#pendingConnections.get(clientGroupID) ?? 1) - 1;\n if (remaining <= 0) {\n this.#pendingConnections.delete(clientGroupID);\n if (!this.#viewSyncers.hasService(clientGroupID)) {\n const gen =\n this.#clientGroupGenerations.get(clientGroupID) ?? generation;\n this.#parent.send<ClientGroupStatusMessage>([\n 'clientGroupStatus',\n {\n clientGroupID,\n active: false,\n ...(gen !== undefined ? {generation: gen} : {}),\n },\n ]);\n this.#clientGroupGenerations.delete(clientGroupID);\n }\n } else {\n this.#pendingConnections.set(clientGroupID, remaining);\n }\n }\n\n run() {\n return this.#stopped.promise;\n }\n\n /**\n * Graceful shutdown involves shutting down view syncers one at a time, pausing\n * for the duration of view syncer's hydration between each one. This paces the\n * disconnects to avoid creating a backlog of hydrations in the receiving server\n * when the clients reconnect.\n */\n async drain() {\n const start = Date.now();\n this.#lc.info?.(`draining ${this.#viewSyncers.size} view-syncers`);\n\n this.#drainCoordinator.drainNextIn(0);\n\n while (this.#viewSyncers.size) {\n await this.#drainCoordinator.forceDrainTimeout;\n\n // Pick an arbitrary view syncer to force drain.\n for (const vs of this.#viewSyncers.getServices()) {\n this.#lc.debug?.(`draining view-syncer ${vs.id} (forced)`);\n // When this drain or an elective drain completes, the forceDrainTimeout will\n // resolve after the next drain interval.\n void vs.stop();\n break;\n }\n }\n this.#lc.info?.(`finished draining (${Date.now() - start} ms)`);\n }\n\n stop() {\n clearInterval(this.#viewSyncerLagSampleInterval);\n clearInterval(this.#eligibilityLogInterval);\n this.#wss.close();\n this.#stopped.resolve();\n return promiseVoid;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"syncer.js","names":["#lc","#viewSyncers","#mutagens","#pushers","#connections","#pendingConnections","#clientGroupGenerations","#drainCoordinator","#parent","#wss","#stopped","#config","#validateLegacyJWT","#replicaReadyStates","#webSocketOpenConnections","#webSocketConnectionAttempts","#webSocketConnectionSuccesses","#webSocketConnectionFailures","#viewSyncerLag","#recordReplicaReadyState","#receiveConnection","#releasePending","#computeServingLagDistribution","#computePipelineDedupStats","#viewSyncerLagSampleInterval","#recordViewSyncerLagSamples","#eligibilityLogInterval","#logSharedAdvanceEligibility","#servingLagDistributionCache","#servingLagDistributionCacheClearQueued","#createConnection"],"sources":["../../../../../zero-cache/src/workers/syncer.ts"],"sourcesContent":["import {pid} from 'node:process';\nimport type {MessagePort} from 'node:worker_threads';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {WebSocket, WebSocketServer, type ServerOptions} from 'ws';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport {\n isProtocolError,\n ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {resolveAuth, type Auth, type ValidateLegacyJWT} from '../auth/auth.ts';\nimport {tokenConfigOptions} from '../auth/jwt.ts';\nimport {type ZeroConfig} from '../config/zero-config.ts';\nimport {\n getOrCreateCounter,\n getOrCreateGauge,\n getOrCreateNativeHistogram,\n getOrCreateUpDownCounter,\n} from '../observability/metrics.ts';\nimport {\n recordConnectionAttempted,\n recordConnectionSuccess,\n setActiveClientGroupsGetter,\n} from '../server/anonymous-otel-start.ts';\nimport type {Mutagen} from '../services/mutagen/mutagen.ts';\nimport type {Pusher} from '../services/mutagen/pusher.ts';\nimport type {ReplicaState} from '../services/replicator/replicator.ts';\nimport {ServiceRunner} from '../services/runner.ts';\nimport type {\n ActivityBasedService,\n Service,\n SingletonService,\n} from '../services/service.ts';\nimport type {ConnectionContextManager} from '../services/view-syncer/connection-context-manager.ts';\nimport {DrainCoordinator} from '../services/view-syncer/drain-coordinator.ts';\nimport type {ViewSyncer} from '../services/view-syncer/view-syncer.ts';\nimport type {ClientGroupStatusMessage, Worker} from '../types/processes.ts';\nimport type {Subscription} from '../types/subscription.ts';\nimport {installWebSocketReceiver} from '../types/websocket-handoff.ts';\nimport type {ConnectParams} from './connect-params.ts';\nimport {Connection, sendError} from './connection.ts';\nimport {createNotifierFrom, subscribeTo} from './replicator.ts';\nimport {SyncerWsMessageHandler} from './syncer-ws-message-handler.ts';\n\nexport type SyncerWorkerData = {\n replicatorPort: MessagePort;\n};\n\nexport type ReplicaReadyState = {\n readonly watermark: string;\n readonly replicaReadyTimeMs: number;\n};\n\nexport type ServingLagViewSyncer = Pick<\n ViewSyncer,\n 'createdAtMs' | 'servedVersion' | 'servingLagEligible'\n>;\n\nexport type ServingLagStats = {\n readonly activeClientGroups: number;\n readonly laggingClientGroups: number;\n readonly minMs: number;\n readonly p50Ms: number;\n readonly p75Ms: number;\n readonly p99Ms: number;\n readonly maxMs: number;\n};\n\ntype ServingLagDistribution = ServingLagStats & {\n readonly lagsMs: readonly number[];\n};\n\nexport type PipelineDedupViewSyncer = Pick<\n ViewSyncer,\n 'pipelineHashes' | 'clientSchemaKey'\n>;\n\nexport type PipelineDedupStats = {\n readonly clientTotal: number;\n readonly internalTotal: number;\n // (clientSchemaKey, transformationHash) -> occurrence count across client\n // groups, for client/custom queries.\n readonly clientHashes: ReadonlyMap<\n string,\n {count: number; queryName: string | undefined}\n >;\n readonly internalUnique: number;\n readonly clientSchemas: number;\n};\n\nexport function computePipelineDedupStats(\n viewSyncers: Iterable<PipelineDedupViewSyncer>,\n): PipelineDedupStats {\n let clientTotal = 0;\n let internalTotal = 0;\n const clientHashes = new Map<\n string,\n {count: number; queryName: string | undefined}\n >();\n const internalHashes = new Set<string>();\n const schemaKeys = new Set<string>();\n let vsIndex = 0;\n for (const vs of viewSyncers) {\n const schemaKey = vs.clientSchemaKey;\n if (schemaKey !== undefined) {\n schemaKeys.add(schemaKey);\n }\n // Without a client schema the pipeline's dedup identity is unknown, so key\n // it uniquely per view-syncer; otherwise schema-less pipelines from\n // unrelated client groups would collapse under a shared `undefined/<hash>`\n // key and overstate deduplication.\n const dedupSchemaKey = schemaKey ?? `#vs${vsIndex}`;\n vsIndex++;\n for (const {\n transformationHash,\n internal,\n queryName,\n } of vs.pipelineHashes()) {\n // Pipelines only share when both the client schema (which determines\n // row keys) and the transformed AST are identical.\n const key = `${dedupSchemaKey}/${transformationHash}`;\n if (internal) {\n internalTotal++;\n internalHashes.add(key);\n } else {\n clientTotal++;\n const entry = clientHashes.get(key);\n if (entry) {\n entry.count++;\n } else {\n clientHashes.set(key, {count: 1, queryName});\n }\n }\n }\n }\n return {\n clientTotal,\n internalTotal,\n clientHashes,\n internalUnique: internalHashes.size,\n clientSchemas: schemaKeys.size,\n };\n}\n\nexport const MAX_REPLICA_READY_STATES = 10_000;\nconst VIEW_SYNCER_LAG_SAMPLE_INTERVAL_MS = 60_000;\nconst SHARED_ADVANCE_ELIGIBILITY_LOG_INTERVAL_MS = 5 * 60_000;\n\nconst PROTOCOL_VERSION_ATTRIBUTE = 'protocol.version';\nconst FAILURE_REASON_ATTRIBUTE = 'reason';\n\nfunction boundReplicaReadyStates(\n replicaReadyStates: ReplicaReadyState[],\n): void {\n if (replicaReadyStates.length > MAX_REPLICA_READY_STATES) {\n replicaReadyStates.splice(\n 0,\n replicaReadyStates.length - MAX_REPLICA_READY_STATES,\n );\n }\n}\n\nfunction pruneReplicaReadyStates(\n replicaReadyStates: ReplicaReadyState[],\n firstNeededIndex: number,\n): void {\n if (firstNeededIndex > 0) {\n replicaReadyStates.splice(0, firstNeededIndex);\n }\n\n boundReplicaReadyStates(replicaReadyStates);\n}\n\nfunction lowerBoundReplicaReadyTimeMs(\n replicaReadyStates: readonly ReplicaReadyState[],\n replicaReadyTimeMs: number,\n): number {\n let low = 0;\n let high = replicaReadyStates.length;\n while (low < high) {\n const mid = low + Math.floor((high - low) / 2);\n if (replicaReadyStates[mid].replicaReadyTimeMs < replicaReadyTimeMs) {\n low = mid + 1;\n } else {\n high = mid;\n }\n }\n return low;\n}\n\nfunction upperBoundWatermark(\n replicaReadyStates: readonly ReplicaReadyState[],\n watermark: string,\n): number {\n let low = 0;\n let high = replicaReadyStates.length;\n while (low < high) {\n const mid = low + Math.floor((high - low) / 2);\n if (replicaReadyStates[mid].watermark <= watermark) {\n low = mid + 1;\n } else {\n high = mid;\n }\n }\n return low;\n}\n\nfunction findFirstUnservedIndex(\n replicaReadyStates: readonly ReplicaReadyState[],\n viewSyncer: ServingLagViewSyncer,\n): number {\n const firstReadyAfterCreation = lowerBoundReplicaReadyTimeMs(\n replicaReadyStates,\n viewSyncer.createdAtMs,\n );\n const firstAfterServedVersion =\n viewSyncer.servedVersion === null\n ? 0\n : upperBoundWatermark(replicaReadyStates, viewSyncer.servedVersion);\n\n const firstUnservedIndex = Math.max(\n firstReadyAfterCreation,\n firstAfterServedVersion,\n );\n return firstUnservedIndex < replicaReadyStates.length\n ? firstUnservedIndex\n : -1;\n}\n\nfunction percentileNearestRank(\n sortedValues: readonly number[],\n percentile: number,\n): number {\n if (sortedValues.length === 0) {\n return 0;\n }\n const index = Math.min(\n sortedValues.length - 1,\n Math.max(0, Math.ceil((percentile / 100) * sortedValues.length) - 1),\n );\n return sortedValues[index];\n}\n\nfunction computeServingLagDistributionMs(\n now: number,\n replicaReadyStates: ReplicaReadyState[],\n viewSyncers: Iterable<ServingLagViewSyncer>,\n): ServingLagDistribution {\n const lags: number[] = [];\n let laggingClientGroups = 0;\n let firstNeededIndex = replicaReadyStates.length;\n\n for (const viewSyncer of viewSyncers) {\n if (!viewSyncer.servingLagEligible) {\n continue;\n }\n\n const firstUnservedIndex = findFirstUnservedIndex(\n replicaReadyStates,\n viewSyncer,\n );\n if (firstUnservedIndex === -1) {\n lags.push(0);\n continue;\n }\n\n firstNeededIndex = Math.min(firstNeededIndex, firstUnservedIndex);\n const lagMs = Math.max(\n 0,\n now - replicaReadyStates[firstUnservedIndex].replicaReadyTimeMs,\n );\n lags.push(lagMs);\n if (lagMs > 0) {\n laggingClientGroups++;\n }\n }\n\n pruneReplicaReadyStates(replicaReadyStates, firstNeededIndex);\n\n lags.sort((a, b) => a - b);\n return {\n activeClientGroups: lags.length,\n laggingClientGroups,\n // The percentile fields are for the legacy serving_lag_stats gauge.\n // The native view_syncer_lag histogram records lagsMs and computes\n // percentiles in Prometheus/AMP.\n minMs: lags[0] ?? 0,\n p50Ms: percentileNearestRank(lags, 50),\n p75Ms: percentileNearestRank(lags, 75),\n p99Ms: percentileNearestRank(lags, 99),\n maxMs: lags.at(-1) ?? 0,\n lagsMs: lags,\n };\n}\n\nexport function computeServingLagStatsMs(\n now: number,\n replicaReadyStates: ReplicaReadyState[],\n viewSyncers: Iterable<ServingLagViewSyncer>,\n): ServingLagStats {\n const distribution = computeServingLagDistributionMs(\n now,\n replicaReadyStates,\n viewSyncers,\n );\n return {\n activeClientGroups: distribution.activeClientGroups,\n laggingClientGroups: distribution.laggingClientGroups,\n minMs: distribution.minMs,\n p50Ms: distribution.p50Ms,\n p75Ms: distribution.p75Ms,\n p99Ms: distribution.p99Ms,\n maxMs: distribution.maxMs,\n };\n}\n\nexport function computeMaxServingLagMs(\n now: number,\n replicaReadyStates: ReplicaReadyState[],\n viewSyncers: Iterable<ServingLagViewSyncer>,\n): number {\n return computeServingLagStatsMs(now, replicaReadyStates, viewSyncers).maxMs;\n}\n\nfunction getWebSocketServerOptions(config: ZeroConfig): ServerOptions {\n const options: ServerOptions = {\n noServer: true,\n maxPayload: config.websocketMaxPayloadBytes,\n };\n\n if (config.websocketCompression) {\n options.perMessageDeflate = true;\n\n if (config.websocketCompressionOptions) {\n try {\n const compressionOptions = JSON.parse(\n config.websocketCompressionOptions,\n );\n options.perMessageDeflate = compressionOptions;\n } catch (e) {\n throw new Error(\n `Failed to parse ZERO_WEBSOCKET_COMPRESSION_OPTIONS: ${String(e)}. Expected valid JSON.`,\n );\n }\n }\n }\n\n return options;\n}\n\n/**\n * The Syncer worker receives websocket handoffs for \"/sync\" connections\n * from the Dispatcher in the main thread, and creates websocket\n * {@link Connection}s with a corresponding {@link ViewSyncer}, {@link Mutagen},\n * and {@link Subscription} to version notifications from the Replicator\n * worker.\n */\nexport class Syncer implements SingletonService {\n readonly id = `syncer-${pid}`;\n readonly #lc: LogContext;\n readonly #viewSyncers: ServiceRunner<ViewSyncer & ActivityBasedService>;\n readonly #mutagens: ServiceRunner<Mutagen & Service> | undefined;\n readonly #pushers: ServiceRunner<Pusher & Service> | undefined;\n readonly #connections = new Map<string, Connection>();\n readonly #pendingConnections = new Map<string, number>();\n readonly #clientGroupGenerations = new Map<string, number>();\n readonly #drainCoordinator = new DrainCoordinator();\n readonly #parent: Worker;\n readonly #wss: WebSocketServer;\n readonly #stopped = resolver();\n readonly #config: ZeroConfig;\n readonly #validateLegacyJWT: ValidateLegacyJWT | undefined;\n readonly #replicaReadyStates: ReplicaReadyState[] = [];\n readonly #webSocketOpenConnections = getOrCreateUpDownCounter(\n 'sync',\n 'websocket.open_connections',\n 'Open client WebSocket connections.',\n );\n readonly #webSocketConnectionAttempts = getOrCreateCounter(\n 'sync',\n 'websocket.connection_attempts',\n 'Client WebSocket connection attempts.',\n );\n readonly #webSocketConnectionSuccesses = getOrCreateCounter(\n 'sync',\n 'websocket.connection_successes',\n 'Client WebSocket connections successfully initialized.',\n );\n readonly #webSocketConnectionFailures = getOrCreateCounter(\n 'sync',\n 'websocket.connection_failures',\n 'Client WebSocket connection attempts that failed before initialization.',\n );\n readonly #viewSyncerLag = getOrCreateNativeHistogram(\n 'sync',\n 'view_syncer_lag',\n {\n description:\n 'Lag from replica-ready change to ViewSyncer output for active ' +\n 'client groups. A change is output after IVM advancement, CVR flush, ' +\n 'and pokeEnd.',\n unit: 's',\n },\n );\n #servingLagDistributionCache: ServingLagDistribution | undefined;\n #servingLagDistributionCacheClearQueued = false;\n #viewSyncerLagSampleInterval: ReturnType<typeof setInterval> | undefined;\n #eligibilityLogInterval: ReturnType<typeof setInterval> | undefined;\n\n constructor(\n lc: LogContext,\n config: ZeroConfig,\n viewSyncerFactory: (\n id: string,\n sub: Subscription<ReplicaState>,\n drainCoordinator: DrainCoordinator,\n ) => ViewSyncer & ActivityBasedService,\n mutagenFactory: ((id: string) => Mutagen & Service) | undefined,\n pusherFactory:\n | ((\n id: string,\n connContextManager: ConnectionContextManager,\n ) => Pusher & Service)\n | undefined,\n parent: Worker,\n validateLegacyJWT: ValidateLegacyJWT | undefined,\n ) {\n this.#config = config;\n this.#validateLegacyJWT = validateLegacyJWT;\n // Relays notifications from the parent thread subscription\n // to ViewSyncers within this thread.\n const notifier = createNotifierFrom(lc, parent, state =>\n this.#recordReplicaReadyState(state),\n );\n subscribeTo(lc, parent);\n\n this.#lc = lc;\n this.#parent = parent;\n this.#viewSyncers = new ServiceRunner(\n lc,\n id => viewSyncerFactory(id, notifier.subscribe(), this.#drainCoordinator),\n v => v.keepalive(),\n id => {\n if ((this.#pendingConnections.get(id) ?? 0) === 0) {\n const generation = this.#clientGroupGenerations.get(id);\n this.#parent.send<ClientGroupStatusMessage>([\n 'clientGroupStatus',\n {\n clientGroupID: id,\n active: false,\n ...(generation !== undefined ? {generation} : {}),\n },\n ]);\n this.#clientGroupGenerations.delete(id);\n }\n },\n );\n if (mutagenFactory) {\n this.#mutagens = new ServiceRunner(lc, mutagenFactory, m => m.hasRefs());\n }\n if (pusherFactory) {\n this.#pushers = new ServiceRunner(\n lc,\n id =>\n pusherFactory(\n id,\n this.#viewSyncers.getService(id).connContextManager,\n ),\n p => p.hasRefs(),\n );\n }\n this.#wss = new WebSocketServer(getWebSocketServerOptions(config));\n\n installWebSocketReceiver(\n lc,\n this.#wss,\n this.#receiveConnection,\n this.#parent,\n params => {\n this.#releasePending(params.clientGroupID, params.generation);\n },\n params => {\n this.#pendingConnections.set(\n params.clientGroupID,\n (this.#pendingConnections.get(params.clientGroupID) ?? 0) + 1,\n );\n if (params.generation !== undefined) {\n const current = this.#clientGroupGenerations.get(\n params.clientGroupID,\n );\n if (current === undefined || params.generation > current) {\n this.#clientGroupGenerations.set(\n params.clientGroupID,\n params.generation,\n );\n }\n }\n },\n );\n\n setActiveClientGroupsGetter(() => this.#viewSyncers.size);\n\n getOrCreateGauge(\n 'sync',\n 'active-client-groups',\n 'Number of active client groups',\n ).addCallback(result => result.observe(this.#viewSyncers.size));\n\n getOrCreateGauge(\n 'sync',\n 'queries',\n 'Active queries (pipelines) across all client groups',\n ).addCallback(result => {\n let total = 0;\n for (const vs of this.#viewSyncers.getServices()) {\n total += vs.queryCount;\n }\n result.observe(total);\n });\n\n getOrCreateGauge(\n 'sync',\n 'rows',\n 'Tracked rows across all client groups',\n ).addCallback(result => {\n let total = 0;\n for (const vs of this.#viewSyncers.getServices()) {\n total += vs.rowCount;\n }\n result.observe(total);\n });\n\n getOrCreateGauge('sync', 'serving_lag', {\n description:\n 'Maximum time active ViewSyncer client groups have had unserved ' +\n 'replica changes. A change is served after IVM advancement, CVR flush, ' +\n 'and pokeEnd.',\n unit: 'millisecond',\n }).addCallback(result => {\n result.observe(this.#computeServingLagDistribution().maxMs);\n });\n\n getOrCreateGauge('sync', 'serving_lag_stats', {\n description:\n 'Distribution of time active ViewSyncer client groups have had ' +\n 'unserved replica changes. A change is served after IVM advancement, ' +\n 'CVR flush, and pokeEnd.',\n unit: 'millisecond',\n }).addCallback(result => {\n const stats = this.#computeServingLagDistribution();\n result.observe(stats.minMs, {stat: 'min'});\n result.observe(stats.p50Ms, {stat: 'p50'});\n result.observe(stats.p75Ms, {stat: 'p75'});\n result.observe(stats.p99Ms, {stat: 'p99'});\n result.observe(stats.maxMs, {stat: 'max'});\n });\n\n getOrCreateGauge(\n 'sync',\n 'serving_lagging_client_groups',\n 'Number of active client groups with unserved replica changes',\n ).addCallback(result => {\n result.observe(this.#computeServingLagDistribution().laggingClientGroups);\n });\n\n getOrCreateGauge(\n 'sync',\n 'pipelines_total',\n 'Query pipelines across all client groups on this worker, by query ' +\n 'type. The ratio to sync.pipelines_unique is the dedup factor ' +\n 'available to shared pipeline advancement.',\n ).addCallback(result => {\n const stats = this.#computePipelineDedupStats();\n result.observe(stats.clientTotal, {type: 'client'});\n result.observe(stats.internalTotal, {type: 'internal'});\n });\n\n getOrCreateGauge(\n 'sync',\n 'pipelines_unique',\n 'Distinct (clientSchema, transformationHash) pipelines across all ' +\n 'client groups on this worker, by query type',\n ).addCallback(result => {\n const stats = this.#computePipelineDedupStats();\n result.observe(stats.clientHashes.size, {type: 'client'});\n result.observe(stats.internalUnique, {type: 'internal'});\n });\n\n getOrCreateGauge(\n 'sync',\n 'client_schemas',\n 'Distinct client schemas across all client groups on this worker',\n ).addCallback(result => {\n result.observe(this.#computePipelineDedupStats().clientSchemas);\n });\n\n this.#viewSyncerLagSampleInterval = setInterval(\n () => this.#recordViewSyncerLagSamples(),\n VIEW_SYNCER_LAG_SAMPLE_INTERVAL_MS,\n );\n this.#viewSyncerLagSampleInterval.unref?.();\n\n this.#eligibilityLogInterval = setInterval(\n () => this.#logSharedAdvanceEligibility(),\n SHARED_ADVANCE_ELIGIBILITY_LOG_INTERVAL_MS,\n );\n this.#eligibilityLogInterval.unref?.();\n }\n\n #computeServingLagDistribution(): ServingLagDistribution {\n if (this.#servingLagDistributionCache) {\n return this.#servingLagDistributionCache;\n }\n\n const distribution = computeServingLagDistributionMs(\n Date.now(),\n this.#replicaReadyStates,\n this.#viewSyncers.getServices(),\n );\n this.#servingLagDistributionCache = distribution;\n if (!this.#servingLagDistributionCacheClearQueued) {\n this.#servingLagDistributionCacheClearQueued = true;\n queueMicrotask(() => {\n this.#servingLagDistributionCache = undefined;\n this.#servingLagDistributionCacheClearQueued = false;\n });\n }\n return distribution;\n }\n\n #recordViewSyncerLagSamples(): void {\n const {lagsMs} = this.#computeServingLagDistribution();\n for (const lagMs of lagsMs) {\n this.#viewSyncerLag.recordMs(lagMs);\n }\n }\n\n #computePipelineDedupStats(): PipelineDedupStats {\n return computePipelineDedupStats(this.#viewSyncers.getServices());\n }\n\n #logSharedAdvanceEligibility(): void {\n const stats = this.#computePipelineDedupStats();\n if (stats.clientTotal === 0) {\n return;\n }\n const unique = stats.clientHashes.size;\n const topDuplicated = [...stats.clientHashes.entries()]\n .filter(([, {count}]) => count > 1)\n .sort((a, b) => b[1].count - a[1].count)\n .slice(0, 10)\n .map(([key, {count, queryName}]) => ({key, count, queryName}));\n this.#lc.info?.('shared-advance-eligibility', {\n clientPipelines: stats.clientTotal,\n uniqueClientPipelines: unique,\n dedupFactor: Number((stats.clientTotal / Math.max(unique, 1)).toFixed(2)),\n internalPipelines: stats.internalTotal,\n clientSchemas: stats.clientSchemas,\n topDuplicated,\n });\n }\n\n #recordReplicaReadyState(state: ReplicaState): void {\n if (\n state.watermark === undefined ||\n state.replicaReadyTimeMs === undefined\n ) {\n return;\n }\n const last = this.#replicaReadyStates.at(-1);\n if (last && last.watermark >= state.watermark) {\n return;\n }\n this.#replicaReadyStates.push({\n watermark: state.watermark,\n replicaReadyTimeMs: state.replicaReadyTimeMs,\n });\n if (this.#viewSyncers.size === 0) {\n this.#replicaReadyStates.length = 0;\n return;\n }\n boundReplicaReadyStates(this.#replicaReadyStates);\n }\n\n readonly #createConnection = async (ws: WebSocket, params: ConnectParams) => {\n const metricAttributes = {\n [PROTOCOL_VERSION_ATTRIBUTE]: params.protocolVersion,\n };\n let connectionResultRecorded = false;\n const recordConnectionFailure = (reason: string) => {\n if (connectionResultRecorded) {\n return;\n }\n connectionResultRecorded = true;\n this.#webSocketConnectionFailures.add(1, {\n ...metricAttributes,\n [FAILURE_REASON_ATTRIBUTE]: reason,\n });\n };\n const recordConnectionSuccessMetric = () => {\n if (connectionResultRecorded) {\n return;\n }\n connectionResultRecorded = true;\n this.#webSocketConnectionSuccesses.add(1, metricAttributes);\n };\n\n this.#webSocketConnectionAttempts.add(1, metricAttributes);\n this.#webSocketOpenConnections.add(1, metricAttributes);\n ws.once('close', () => {\n this.#webSocketOpenConnections.add(-1, metricAttributes);\n });\n\n this.#lc.debug?.(\n 'creating connection',\n params.clientGroupID,\n params.clientID,\n );\n recordConnectionAttempted();\n const {clientID, clientGroupID, auth, userID} = params;\n const hasProvidedAuth = auth !== undefined && auth !== '';\n const incomingUserID = userID ?? null;\n\n if (hasProvidedAuth) {\n const tokenOptions = tokenConfigOptions(this.#config.auth ?? {});\n\n const hasPushOrMutate =\n this.#config?.push?.url !== undefined ||\n this.#config?.mutate?.url !== undefined;\n const hasQueries =\n this.#config?.query?.url !== undefined ||\n this.#config?.getQueries?.url !== undefined;\n\n // must either have one of the token options set or have custom mutations & queries enabled\n const hasExactlyOneTokenOption = tokenOptions.length === 1;\n const hasCustomEndpoints = hasPushOrMutate && hasQueries;\n if (!hasExactlyOneTokenOption && !hasCustomEndpoints) {\n recordConnectionFailure('configuration');\n throw new Error(\n 'Exactly one of jwk, secret, or jwksUrl must be set in order to verify tokens but actually the following were set: ' +\n JSON.stringify(tokenOptions) +\n '. You may also set both ZERO_MUTATE_URL and ZERO_QUERY_URL to enable custom mutations and queries without passing token verification options.',\n );\n }\n }\n\n let initialAuth: Auth | undefined;\n\n // Verify JWT BEFORE touching existing connections - prevents unauthenticated\n // attackers from force-disconnecting legitimate users via DoS.\n try {\n initialAuth = await resolveAuth(\n this.#lc\n .withContext('clientGroupID', clientGroupID)\n .withContext('clientID', clientID),\n // no previous auth, since this is a new connection, and resolveAuth is\n // connection scoped, not client group scoped\n undefined,\n incomingUserID,\n auth,\n this.#validateLegacyJWT,\n );\n } catch (e) {\n if (isProtocolError(e)) {\n this.#lc.warn?.(\n 'Rejecting sync connection during initial auth resolution',\n {\n clientGroupID,\n clientID,\n incomingUserID,\n hasProvidedAuth,\n errorKind: e.message,\n },\n );\n recordConnectionFailure('auth');\n sendError(this.#lc, ws, e.errorBody);\n ws.close(3000, e.errorBody.message);\n return;\n }\n recordConnectionFailure('internal');\n throw e;\n }\n\n // Resolving auth can take a while (e.g. a remote JWKS fetch), during\n // which the client may have disconnected. A Connection created for a\n // closed socket never receives the 'close' event and is thus never\n // cleaned up, leaking its timer, its registrations in the connection\n // context manager and its refs on the mutagen and pusher services.\n if (ws.readyState !== WebSocket.OPEN) {\n this.#lc.debug?.('websocket closed while resolving auth', {\n clientGroupID,\n clientID,\n readyState: ws.readyState,\n });\n recordConnectionFailure('closed_during_auth');\n return;\n }\n\n const viewSyncer = this.#viewSyncers.getService(clientGroupID);\n const connContextManager = viewSyncer.connContextManager;\n const group = connContextManager.getGroupState();\n\n // TODO(0xcadams): we only check for user ID mismatch here if the group is\n // already validated. This prevents wrong-user reconnects from evicting a\n // healthy connection, but it does not protect against same-user reconnects\n // with an invalid opaque token. The long-term fix is to keep the replacement\n // connection pending until its auth is fully validated, and only then replace\n // the existing socket.\n if (\n group.pinnedUser !== undefined &&\n group.pinnedUser.id !== incomingUserID\n ) {\n const error = new ProtocolError({\n kind: ErrorKind.Unauthorized,\n message:\n 'Client groups are pinned to a single userID. Connection userID does not match existing client group userID.',\n origin: ErrorOrigin.ZeroCache,\n });\n recordConnectionFailure('user_mismatch');\n sendError(this.#lc, ws, error.errorBody);\n ws.close(3000, error.message);\n return;\n }\n\n // Check for and close existing connections AFTER auth is validated\n const existing = this.#connections.get(clientID);\n if (existing) {\n this.#lc.debug?.(\n `client ${clientID} already connected, closing existing connection`,\n );\n existing.close(`replaced by ${params.wsID}`);\n }\n\n connContextManager.registerConnection(\n {clientID, wsID: params.wsID},\n params,\n initialAuth,\n );\n\n const mutagen = this.#mutagens?.getService(clientGroupID);\n const pusher = this.#pushers?.getService(clientGroupID);\n // a new connection is using the mutagen and pusher. Bump their ref counts.\n mutagen?.ref();\n pusher?.ref();\n\n let connection: Connection;\n try {\n connection = new Connection(\n this.#lc,\n params,\n ws,\n this.#config.allowLegacyQueries,\n new SyncerWsMessageHandler(\n this.#lc,\n params,\n connContextManager,\n viewSyncer,\n mutagen,\n pusher,\n ),\n () => {\n connContextManager.closeConnection({\n clientID,\n wsID: params.wsID,\n });\n if (this.#connections.get(clientID) === connection) {\n this.#connections.delete(clientID);\n }\n // Connection is closed. We can unref the mutagen and pusher.\n // If their ref counts are zero, they will stop themselves and set themselves invalid.\n mutagen?.unref();\n pusher?.unref();\n },\n );\n } catch (e) {\n recordConnectionFailure('internal');\n connContextManager.closeConnection({clientID, wsID: params.wsID});\n mutagen?.unref();\n pusher?.unref();\n throw e;\n }\n\n this.#connections.set(clientID, connection);\n\n if (connection.init()) {\n recordConnectionSuccessMetric();\n recordConnectionSuccess();\n } else {\n recordConnectionFailure('protocol_version');\n }\n\n if (params.initConnectionMsg) {\n this.#lc.debug?.(\n 'handling init connection message from sec header',\n params.clientGroupID,\n params.clientID,\n );\n await connection.handleInitConnection(\n JSON.stringify(params.initConnectionMsg),\n );\n }\n };\n\n readonly #receiveConnection = async (\n ws: WebSocket,\n params: ConnectParams,\n ) => {\n try {\n await this.#createConnection(ws, params);\n } finally {\n this.#releasePending(params.clientGroupID, params.generation);\n }\n };\n\n #releasePending(clientGroupID: string, generation: number | undefined): void {\n const remaining = (this.#pendingConnections.get(clientGroupID) ?? 1) - 1;\n if (remaining <= 0) {\n this.#pendingConnections.delete(clientGroupID);\n if (!this.#viewSyncers.hasService(clientGroupID)) {\n const gen =\n this.#clientGroupGenerations.get(clientGroupID) ?? generation;\n this.#parent.send<ClientGroupStatusMessage>([\n 'clientGroupStatus',\n {\n clientGroupID,\n active: false,\n ...(gen !== undefined ? {generation: gen} : {}),\n },\n ]);\n this.#clientGroupGenerations.delete(clientGroupID);\n }\n } else {\n this.#pendingConnections.set(clientGroupID, remaining);\n }\n }\n\n run() {\n return this.#stopped.promise;\n }\n\n /**\n * Graceful shutdown involves shutting down view syncers one at a time, pausing\n * for the duration of view syncer's hydration between each one. This paces the\n * disconnects to avoid creating a backlog of hydrations in the receiving server\n * when the clients reconnect.\n */\n async drain() {\n const start = Date.now();\n this.#lc.info?.(`draining ${this.#viewSyncers.size} view-syncers`);\n\n this.#drainCoordinator.drainNextIn(0);\n\n while (this.#viewSyncers.size) {\n await this.#drainCoordinator.forceDrainTimeout;\n\n // Pick an arbitrary view syncer to force drain.\n for (const vs of this.#viewSyncers.getServices()) {\n this.#lc.debug?.(`draining view-syncer ${vs.id} (forced)`);\n // When this drain or an elective drain completes, the forceDrainTimeout will\n // resolve after the next drain interval.\n void vs.stop();\n break;\n }\n }\n this.#lc.info?.(`finished draining (${Date.now() - start} ms)`);\n }\n\n stop() {\n clearInterval(this.#viewSyncerLagSampleInterval);\n clearInterval(this.#eligibilityLogInterval);\n this.#wss.close();\n this.#stopped.resolve();\n return promiseVoid;\n }\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * The current version of Zero.
4
4
  */
5
- var version = "1.12.0-canary.0";
5
+ var version = "1.12.0-canary.1";
6
6
  //#endregion
7
7
  export { version };
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocicorp/zero",
3
- "version": "1.12.0-canary.0",
3
+ "version": "1.12.0-canary.1",
4
4
  "description": "Zero is a web framework for serverless web development.",
5
5
  "homepage": "https://zero.rocicorp.dev",
6
6
  "bugs": {