@rocicorp/zero 1.8.0-canary.4 → 1.8.0-canary.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  var package_default = {
2
2
  name: "@rocicorp/zero",
3
- version: "1.8.0-canary.4",
3
+ version: "1.8.0-canary.5",
4
4
  description: "Zero is a web framework for serverless web development.",
5
5
  homepage: "https://zero.rocicorp.dev",
6
6
  bugs: { "url": "https://bugs.rocicorp.dev" },
@@ -1 +1 @@
1
- {"version":3,"file":"package.js","names":[],"sources":["../../package.json"],"sourcesContent":["{\n \"name\": \"@rocicorp/zero\",\n \"version\": \"1.8.0-canary.4\",\n \"description\": \"Zero is a web framework for serverless web development.\",\n \"homepage\": \"https://zero.rocicorp.dev\",\n \"bugs\": {\n \"url\": \"https://bugs.rocicorp.dev\"\n },\n \"license\": \"Apache-2.0\",\n \"author\": \"Rocicorp, Inc.\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/rocicorp/mono.git\",\n \"directory\": \"packages/zero\"\n },\n \"bin\": {\n \"analyze-query\": \"./out/zero/src/analyze-query.js\",\n \"ast-to-zql\": \"./out/zero/src/ast-to-zql.js\",\n \"transform-query\": \"./out/zero/src/transform-query.js\",\n \"zero-build-schema\": \"./out/zero/src/build-schema.js\",\n \"zero-cache\": \"./out/zero/src/cli.js\",\n \"zero-cache-dev\": \"./out/zero/src/zero-cache-dev.js\",\n \"zero-deploy-permissions\": \"./out/zero/src/deploy-permissions.js\",\n \"zero-out\": \"./out/zero/src/zero-out.js\"\n },\n \"files\": [\n \"out\",\n \"!*.tsbuildinfo\"\n ],\n \"type\": \"module\",\n \"main\": \"out/zero/src/zero.js\",\n \"module\": \"out/zero/src/zero.js\",\n \"types\": \"out/zero/src/zero.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./out/zero/src/zero.d.ts\",\n \"default\": \"./out/zero/src/zero.js\"\n },\n \"./analyze\": {\n \"types\": \"./out/zero/src/analyze.d.ts\",\n \"default\": \"./out/zero/src/analyze.js\"\n },\n \"./bindings\": {\n \"types\": \"./out/zero/src/bindings.d.ts\",\n \"default\": \"./out/zero/src/bindings.js\"\n },\n \"./change-protocol/v0\": {\n \"types\": \"./out/zero/src/change-protocol/v0.d.ts\",\n \"default\": \"./out/zero/src/change-protocol/v0.js\"\n },\n \"./expo-sqlite\": {\n \"types\": \"./out/zero/src/expo-sqlite.d.ts\",\n \"default\": \"./out/zero/src/expo-sqlite.js\"\n },\n \"./op-sqlite\": {\n \"types\": \"./out/zero/src/op-sqlite.d.ts\",\n \"default\": \"./out/zero/src/op-sqlite.js\"\n },\n \"./pg\": {\n \"types\": \"./out/zero/src/pg.d.ts\",\n \"default\": \"./out/zero/src/pg.js\"\n },\n \"./react\": {\n \"types\": \"./out/zero/src/react.d.ts\",\n \"default\": \"./out/zero/src/react.js\"\n },\n \"./react-native\": {\n \"types\": \"./out/zero/src/react-native.d.ts\",\n \"default\": \"./out/zero/src/react-native.js\"\n },\n \"./server\": {\n \"types\": \"./out/zero/src/server.d.ts\",\n \"default\": \"./out/zero/src/server.js\"\n },\n \"./server/adapters/drizzle\": {\n \"types\": \"./out/zero/src/adapters/drizzle.d.ts\",\n \"default\": \"./out/zero/src/adapters/drizzle.js\"\n },\n \"./server/adapters/kysely\": {\n \"types\": \"./out/zero/src/adapters/kysely.d.ts\",\n \"default\": \"./out/zero/src/adapters/kysely.js\"\n },\n \"./server/adapters/prisma\": {\n \"types\": \"./out/zero/src/adapters/prisma.d.ts\",\n \"default\": \"./out/zero/src/adapters/prisma.js\"\n },\n \"./server/adapters/pg\": {\n \"types\": \"./out/zero/src/adapters/pg.d.ts\",\n \"default\": \"./out/zero/src/adapters/pg.js\"\n },\n \"./server/adapters/postgresjs\": {\n \"types\": \"./out/zero/src/adapters/postgresjs.d.ts\",\n \"default\": \"./out/zero/src/adapters/postgresjs.js\"\n },\n \"./solid\": {\n \"types\": \"./out/zero/src/solid.d.ts\",\n \"default\": \"./out/zero/src/solid.js\"\n },\n \"./sqlite\": {\n \"types\": \"./out/zero/src/sqlite.d.ts\",\n \"default\": \"./out/zero/src/sqlite.js\"\n },\n \"./zqlite\": {\n \"types\": \"./out/zero/src/zqlite.d.ts\",\n \"default\": \"./out/zero/src/zqlite.js\"\n }\n },\n \"scripts\": {\n \"build\": \"node --experimental-strip-types --no-warnings tool/build.ts\",\n \"build:watch\": \"node --experimental-strip-types --no-warnings tool/build.ts --watch\",\n \"check-types\": \"tsc -p tsconfig.client.json && tsc -p tsconfig.server.json\",\n \"check-types:client:watch\": \"tsc -p tsconfig.client.json --watch\",\n \"check-types:server:watch\": \"tsc -p tsconfig.server.json --watch\",\n \"format\": \"oxfmt .\",\n \"check-format\": \"oxfmt --check .\",\n \"lint\": \"oxlint --quiet --config ../../oxlint.config.ts src/\",\n \"docs\": \"node --experimental-strip-types --no-warnings tool/generate-docs.ts\",\n \"docs:server\": \"node --watch --experimental-strip-types --no-warnings tool/generate-docs.ts --server\",\n \"fmt\": \"oxfmt .\",\n \"check-fmt\": \"oxfmt --check .\"\n },\n \"dependencies\": {\n \"@badrap/valita\": \"0.3.11\",\n \"@databases/escape-identifier\": \"^1.0.3\",\n \"@databases/sql\": \"^3.3.0\",\n \"@dotenvx/dotenvx\": \"^1.39.0\",\n \"@drdgvhbh/postgres-error-codes\": \"^0.0.6\",\n \"@fastify/cors\": \"^10.0.0\",\n \"@fastify/websocket\": \"^11.0.0\",\n \"@google-cloud/precise-date\": \"^4.0.0\",\n \"@opentelemetry/api\": \"^1.9.0\",\n \"@opentelemetry/api-logs\": \"^0.218.0\",\n \"@opentelemetry/auto-instrumentations-node\": \"^0.76.0\",\n \"@opentelemetry/exporter-metrics-otlp-http\": \"^0.218.0\",\n \"@opentelemetry/resources\": \"^2.7.1\",\n \"@opentelemetry/sdk-metrics\": \"^2.7.1\",\n \"@opentelemetry/sdk-node\": \"^0.218.0\",\n \"@opentelemetry/sdk-trace-node\": \"^2.7.1\",\n \"@opentelemetry/semantic-conventions\": \"^1.41.1\",\n \"@postgresql-typed/oids\": \"^0.2.0\",\n \"@rocicorp/lock\": \"^1.0.4\",\n \"@rocicorp/logger\": \"^6.1.0\",\n \"@rocicorp/resolver\": \"^1.0.2\",\n \"@rocicorp/zero-sqlite3\": \"^1.1.2\",\n \"@standard-schema/spec\": \"^1.0.0\",\n \"@types/basic-auth\": \"^1.1.8\",\n \"@types/ws\": \"^8.5.12\",\n \"basic-auth\": \"^2.0.1\",\n \"chalk-template\": \"^1.1.0\",\n \"chokidar\": \"^4.0.1\",\n \"cloudevents\": \"^10.0.0\",\n \"command-line-args\": \"^6.0.1\",\n \"command-line-usage\": \"^7.0.3\",\n \"compare-utf8\": \"^0.2.0\",\n \"defu\": \"^6.1.4\",\n \"eventemitter3\": \"^5.0.1\",\n \"fastify\": \"^5.0.0\",\n \"is-in-subnet\": \"^4.0.1\",\n \"jose\": \"^5.9.3\",\n \"js-xxhash\": \"^4.0.0\",\n \"json-custom-numbers\": \"^3.1.1\",\n \"kasi\": \"^1.1.0\",\n \"nanoid\": \"^5.1.2\",\n \"oxfmt\": \"^0.45.0\",\n \"parse-prometheus-text-format\": \"^1.1.1\",\n \"pg-format\": \"npm:pg-format-fix@^1.0.5\",\n \"postgres\": \"3.4.7\",\n \"semver\": \"^7.5.4\",\n \"url-pattern\": \"^1.0.3\",\n \"urlpattern-polyfill\": \"^10.1.0\",\n \"ws\": \"^8.18.1\"\n },\n \"devDependencies\": {\n \"@op-engineering/op-sqlite\": \">=15\",\n \"@vitest/runner\": \"^4.1.6\",\n \"analyze-query\": \"workspace:*\",\n \"ast-to-zql\": \"workspace:*\",\n \"expo-sqlite\": \">=15\",\n \"replicache\": \"workspace:*\",\n \"shared\": \"workspace:*\",\n \"syncpack\": \"^14.3.0\",\n \"typedoc\": \"^0.28.17\",\n \"typedoc-plugin-markdown\": \"^4.10.0\",\n \"typescript\": \"~6.0.2\",\n \"vite\": \"^8.0.13\",\n \"vitest\": \"^4.1.6\",\n \"zero-cache\": \"workspace:*\",\n \"zero-client\": \"workspace:*\",\n \"zero-pg\": \"workspace:*\",\n \"zero-react\": \"workspace:*\",\n \"zero-server\": \"workspace:*\",\n \"zero-solid\": \"workspace:*\",\n \"zqlite\": \"workspace:*\"\n },\n \"peerDependencies\": {\n \"@op-engineering/op-sqlite\": \">=15\",\n \"drizzle-orm\": \"^0.45.2\",\n \"expo-sqlite\": \">=15\",\n \"kysely\": \"^0.28.17\",\n \"pg\": \"^8.16.3\",\n \"react\": \"^19.2.6\",\n \"solid-js\": \"^1.9.4\"\n },\n \"peerDependenciesMeta\": {\n \"@op-engineering/op-sqlite\": {\n \"optional\": true\n },\n \"drizzle-orm\": {\n \"optional\": true\n },\n \"expo-sqlite\": {\n \"optional\": true\n },\n \"kysely\": {\n \"optional\": true\n },\n \"pg\": {\n \"optional\": true\n },\n \"react\": {\n \"optional\": true\n },\n \"solid-js\": {\n \"optional\": true\n }\n },\n \"engines\": {\n \"node\": \">=22\"\n },\n \"packageManager\": \"pnpm@11.5.3\"\n}\n"],"mappings":""}
1
+ {"version":3,"file":"package.js","names":[],"sources":["../../package.json"],"sourcesContent":["{\n \"name\": \"@rocicorp/zero\",\n \"version\": \"1.8.0-canary.5\",\n \"description\": \"Zero is a web framework for serverless web development.\",\n \"homepage\": \"https://zero.rocicorp.dev\",\n \"bugs\": {\n \"url\": \"https://bugs.rocicorp.dev\"\n },\n \"license\": \"Apache-2.0\",\n \"author\": \"Rocicorp, Inc.\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/rocicorp/mono.git\",\n \"directory\": \"packages/zero\"\n },\n \"bin\": {\n \"analyze-query\": \"./out/zero/src/analyze-query.js\",\n \"ast-to-zql\": \"./out/zero/src/ast-to-zql.js\",\n \"transform-query\": \"./out/zero/src/transform-query.js\",\n \"zero-build-schema\": \"./out/zero/src/build-schema.js\",\n \"zero-cache\": \"./out/zero/src/cli.js\",\n \"zero-cache-dev\": \"./out/zero/src/zero-cache-dev.js\",\n \"zero-deploy-permissions\": \"./out/zero/src/deploy-permissions.js\",\n \"zero-out\": \"./out/zero/src/zero-out.js\"\n },\n \"files\": [\n \"out\",\n \"!*.tsbuildinfo\"\n ],\n \"type\": \"module\",\n \"main\": \"out/zero/src/zero.js\",\n \"module\": \"out/zero/src/zero.js\",\n \"types\": \"out/zero/src/zero.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./out/zero/src/zero.d.ts\",\n \"default\": \"./out/zero/src/zero.js\"\n },\n \"./analyze\": {\n \"types\": \"./out/zero/src/analyze.d.ts\",\n \"default\": \"./out/zero/src/analyze.js\"\n },\n \"./bindings\": {\n \"types\": \"./out/zero/src/bindings.d.ts\",\n \"default\": \"./out/zero/src/bindings.js\"\n },\n \"./change-protocol/v0\": {\n \"types\": \"./out/zero/src/change-protocol/v0.d.ts\",\n \"default\": \"./out/zero/src/change-protocol/v0.js\"\n },\n \"./expo-sqlite\": {\n \"types\": \"./out/zero/src/expo-sqlite.d.ts\",\n \"default\": \"./out/zero/src/expo-sqlite.js\"\n },\n \"./op-sqlite\": {\n \"types\": \"./out/zero/src/op-sqlite.d.ts\",\n \"default\": \"./out/zero/src/op-sqlite.js\"\n },\n \"./pg\": {\n \"types\": \"./out/zero/src/pg.d.ts\",\n \"default\": \"./out/zero/src/pg.js\"\n },\n \"./react\": {\n \"types\": \"./out/zero/src/react.d.ts\",\n \"default\": \"./out/zero/src/react.js\"\n },\n \"./react-native\": {\n \"types\": \"./out/zero/src/react-native.d.ts\",\n \"default\": \"./out/zero/src/react-native.js\"\n },\n \"./server\": {\n \"types\": \"./out/zero/src/server.d.ts\",\n \"default\": \"./out/zero/src/server.js\"\n },\n \"./server/adapters/drizzle\": {\n \"types\": \"./out/zero/src/adapters/drizzle.d.ts\",\n \"default\": \"./out/zero/src/adapters/drizzle.js\"\n },\n \"./server/adapters/kysely\": {\n \"types\": \"./out/zero/src/adapters/kysely.d.ts\",\n \"default\": \"./out/zero/src/adapters/kysely.js\"\n },\n \"./server/adapters/prisma\": {\n \"types\": \"./out/zero/src/adapters/prisma.d.ts\",\n \"default\": \"./out/zero/src/adapters/prisma.js\"\n },\n \"./server/adapters/pg\": {\n \"types\": \"./out/zero/src/adapters/pg.d.ts\",\n \"default\": \"./out/zero/src/adapters/pg.js\"\n },\n \"./server/adapters/postgresjs\": {\n \"types\": \"./out/zero/src/adapters/postgresjs.d.ts\",\n \"default\": \"./out/zero/src/adapters/postgresjs.js\"\n },\n \"./solid\": {\n \"types\": \"./out/zero/src/solid.d.ts\",\n \"default\": \"./out/zero/src/solid.js\"\n },\n \"./sqlite\": {\n \"types\": \"./out/zero/src/sqlite.d.ts\",\n \"default\": \"./out/zero/src/sqlite.js\"\n },\n \"./zqlite\": {\n \"types\": \"./out/zero/src/zqlite.d.ts\",\n \"default\": \"./out/zero/src/zqlite.js\"\n }\n },\n \"scripts\": {\n \"build\": \"node --experimental-strip-types --no-warnings tool/build.ts\",\n \"build:watch\": \"node --experimental-strip-types --no-warnings tool/build.ts --watch\",\n \"check-types\": \"tsc -p tsconfig.client.json && tsc -p tsconfig.server.json\",\n \"check-types:client:watch\": \"tsc -p tsconfig.client.json --watch\",\n \"check-types:server:watch\": \"tsc -p tsconfig.server.json --watch\",\n \"format\": \"oxfmt .\",\n \"check-format\": \"oxfmt --check .\",\n \"lint\": \"oxlint --quiet --config ../../oxlint.config.ts src/\",\n \"docs\": \"node --experimental-strip-types --no-warnings tool/generate-docs.ts\",\n \"docs:server\": \"node --watch --experimental-strip-types --no-warnings tool/generate-docs.ts --server\",\n \"fmt\": \"oxfmt .\",\n \"check-fmt\": \"oxfmt --check .\"\n },\n \"dependencies\": {\n \"@badrap/valita\": \"0.3.11\",\n \"@databases/escape-identifier\": \"^1.0.3\",\n \"@databases/sql\": \"^3.3.0\",\n \"@dotenvx/dotenvx\": \"^1.39.0\",\n \"@drdgvhbh/postgres-error-codes\": \"^0.0.6\",\n \"@fastify/cors\": \"^10.0.0\",\n \"@fastify/websocket\": \"^11.0.0\",\n \"@google-cloud/precise-date\": \"^4.0.0\",\n \"@opentelemetry/api\": \"^1.9.0\",\n \"@opentelemetry/api-logs\": \"^0.218.0\",\n \"@opentelemetry/auto-instrumentations-node\": \"^0.76.0\",\n \"@opentelemetry/exporter-metrics-otlp-http\": \"^0.218.0\",\n \"@opentelemetry/resources\": \"^2.7.1\",\n \"@opentelemetry/sdk-metrics\": \"^2.7.1\",\n \"@opentelemetry/sdk-node\": \"^0.218.0\",\n \"@opentelemetry/sdk-trace-node\": \"^2.7.1\",\n \"@opentelemetry/semantic-conventions\": \"^1.41.1\",\n \"@postgresql-typed/oids\": \"^0.2.0\",\n \"@rocicorp/lock\": \"^1.0.4\",\n \"@rocicorp/logger\": \"^6.1.0\",\n \"@rocicorp/resolver\": \"^1.0.2\",\n \"@rocicorp/zero-sqlite3\": \"^1.1.2\",\n \"@standard-schema/spec\": \"^1.0.0\",\n \"@types/basic-auth\": \"^1.1.8\",\n \"@types/ws\": \"^8.5.12\",\n \"basic-auth\": \"^2.0.1\",\n \"chalk-template\": \"^1.1.0\",\n \"chokidar\": \"^4.0.1\",\n \"cloudevents\": \"^10.0.0\",\n \"command-line-args\": \"^6.0.1\",\n \"command-line-usage\": \"^7.0.3\",\n \"compare-utf8\": \"^0.2.0\",\n \"defu\": \"^6.1.4\",\n \"eventemitter3\": \"^5.0.1\",\n \"fastify\": \"^5.0.0\",\n \"is-in-subnet\": \"^4.0.1\",\n \"jose\": \"^5.9.3\",\n \"js-xxhash\": \"^4.0.0\",\n \"json-custom-numbers\": \"^3.1.1\",\n \"kasi\": \"^1.1.0\",\n \"nanoid\": \"^5.1.2\",\n \"oxfmt\": \"^0.45.0\",\n \"parse-prometheus-text-format\": \"^1.1.1\",\n \"pg-format\": \"npm:pg-format-fix@^1.0.5\",\n \"postgres\": \"3.4.7\",\n \"semver\": \"^7.5.4\",\n \"url-pattern\": \"^1.0.3\",\n \"urlpattern-polyfill\": \"^10.1.0\",\n \"ws\": \"^8.18.1\"\n },\n \"devDependencies\": {\n \"@op-engineering/op-sqlite\": \">=15\",\n \"@vitest/runner\": \"^4.1.6\",\n \"analyze-query\": \"workspace:*\",\n \"ast-to-zql\": \"workspace:*\",\n \"expo-sqlite\": \">=15\",\n \"replicache\": \"workspace:*\",\n \"shared\": \"workspace:*\",\n \"syncpack\": \"^14.3.0\",\n \"typedoc\": \"^0.28.17\",\n \"typedoc-plugin-markdown\": \"^4.10.0\",\n \"typescript\": \"~6.0.2\",\n \"vite\": \"^8.0.13\",\n \"vitest\": \"^4.1.6\",\n \"zero-cache\": \"workspace:*\",\n \"zero-client\": \"workspace:*\",\n \"zero-pg\": \"workspace:*\",\n \"zero-react\": \"workspace:*\",\n \"zero-server\": \"workspace:*\",\n \"zero-solid\": \"workspace:*\",\n \"zqlite\": \"workspace:*\"\n },\n \"peerDependencies\": {\n \"@op-engineering/op-sqlite\": \">=15\",\n \"drizzle-orm\": \"^0.45.2\",\n \"expo-sqlite\": \">=15\",\n \"kysely\": \"^0.28.17\",\n \"pg\": \"^8.16.3\",\n \"react\": \"^19.2.6\",\n \"solid-js\": \"^1.9.4\"\n },\n \"peerDependenciesMeta\": {\n \"@op-engineering/op-sqlite\": {\n \"optional\": true\n },\n \"drizzle-orm\": {\n \"optional\": true\n },\n \"expo-sqlite\": {\n \"optional\": true\n },\n \"kysely\": {\n \"optional\": true\n },\n \"pg\": {\n \"optional\": true\n },\n \"react\": {\n \"optional\": true\n },\n \"solid-js\": {\n \"optional\": true\n }\n },\n \"engines\": {\n \"node\": \">=22\"\n },\n \"packageManager\": \"pnpm@11.5.3\"\n}\n"],"mappings":""}
@@ -4,6 +4,7 @@ import type { ReadonlyJSONValue } from '../../../shared/src/json.ts';
4
4
  import { type Type } from '../../../shared/src/valita.ts';
5
5
  import type { ConnectionContext } from '../services/view-syncer/connection-context-manager.ts';
6
6
  import { type ShardID } from '../types/shards.ts';
7
+ import { type ApiCleanupType, type ApiOperation } from './metrics.ts';
7
8
  /**
8
9
  * Compiles and validates a URLPattern from configuration.
9
10
  *
@@ -15,7 +16,11 @@ import { type ShardID } from '../types/shards.ts';
15
16
  */
16
17
  export declare function compileUrlPattern(pattern: string): URLPattern;
17
18
  export declare const getBodyPreview: (res: Response, lc: LogContext) => Promise<string | undefined>;
18
- export declare function fetchFromAPIServer<TValidator extends Type>(validator: TValidator, source: 'push' | 'transform', lc: LogContext, ctx: ConnectionContext, shard: ShardID, body: ReadonlyJSONValue): Promise<import("@badrap/valita").Infer<TValidator>>;
19
+ export type FetchMetricsOptions = {
20
+ operation: ApiOperation;
21
+ cleanupType?: ApiCleanupType | undefined;
22
+ };
23
+ export declare function fetchFromAPIServer<TValidator extends Type>(validator: TValidator, source: 'push' | 'transform', lc: LogContext, ctx: ConnectionContext, shard: ShardID, body: ReadonlyJSONValue, metricsOpts: FetchMetricsOptions): Promise<import("@badrap/valita").Infer<TValidator>>;
19
24
  /**
20
25
  * Returns true if the url matches one of the allowedUrlPatterns.
21
26
  *
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/custom/fetch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAW,MAAM,kBAAkB,CAAC;AAC3D,OAAO,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,6BAA6B,CAAC;AAInE,OAAO,EAAC,KAAK,IAAI,EAAC,MAAM,+BAA+B,CAAC;AAKxD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,uDAAuD,CAAC;AAE7F,OAAO,EAAiB,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAIhE;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAQ7D;AAED,eAAO,MAAM,cAAc,GACzB,KAAK,QAAQ,EACb,IAAI,UAAU,KACb,OAAO,CAAC,MAAM,GAAG,SAAS,CAkB5B,CAAC;AAIF,wBAAsB,kBAAkB,CAAC,UAAU,SAAS,IAAI,EAC9D,SAAS,EAAE,UAAU,EACrB,MAAM,EAAE,MAAM,GAAG,WAAW,EAC5B,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,iBAAiB,uDAwNxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,SAAS,UAAU,EAAE,GACxC,OAAO,CAOT"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/custom/fetch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAW,MAAM,kBAAkB,CAAC;AAC3D,OAAO,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,6BAA6B,CAAC;AAInE,OAAO,EAAC,KAAK,IAAI,EAAC,MAAM,+BAA+B,CAAC;AAaxD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,uDAAuD,CAAC;AAE7F,OAAO,EAAiB,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAQL,KAAK,cAAc,EAEnB,KAAK,YAAY,EAGlB,MAAM,cAAc,CAAC;AAItB;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAQ7D;AAED,eAAO,MAAM,cAAc,GACzB,KAAK,QAAQ,EACb,IAAI,UAAU,KACb,OAAO,CAAC,MAAM,GAAG,SAAS,CAkB5B,CAAC;AASF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,YAAY,CAAC;IACxB,WAAW,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CAC1C,CAAC;AAEF,wBAAsB,kBAAkB,CAAC,UAAU,SAAS,IAAI,EAC9D,SAAS,EAAE,UAAU,EACrB,MAAM,EAAE,MAAM,GAAG,WAAW,EAC5B,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,iBAAiB,EACvB,WAAW,EAAE,mBAAmB,uDA8QjC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,SAAS,UAAU,EAAE,GACxC,OAAO,CAOT"}
@@ -1,15 +1,17 @@
1
- import { assert, unreachable } from "../../../shared/src/asserts.js";
1
+ import { unreachable } from "../../../shared/src/asserts.js";
2
2
  import { sleep } from "../../../shared/src/sleep.js";
3
3
  import "../../../shared/src/valita.js";
4
4
  import { getErrorMessage } from "../../../shared/src/error.js";
5
5
  import { must } from "../../../shared/src/must.js";
6
6
  import { PushFailed, TransformFailed } from "../../../zero-protocol/src/error-kind-enum.js";
7
7
  import { ZeroCache } from "../../../zero-protocol/src/error-origin-enum.js";
8
- import { HTTP, Internal, Parse } from "../../../zero-protocol/src/error-reason-enum.js";
9
- import { isProtocolError } from "../../../zero-protocol/src/error.js";
8
+ import { HTTP, Internal, Parse, UnsupportedPushVersion } from "../../../zero-protocol/src/error-reason-enum.js";
9
+ import { errorBodySchema, isProtocolError } from "../../../zero-protocol/src/error.js";
10
+ import { pushErrorSchema } from "../../../zero-protocol/src/push.js";
10
11
  import { upstreamSchema } from "../types/shards.js";
11
12
  import { randInt } from "../../../shared/src/rand.js";
12
13
  import { ProtocolErrorWithLevel } from "../types/error-with-level.js";
14
+ import { apiAttemptDuration, apiAttempts, apiInFlight, apiRequestDuration, apiRequests } from "./metrics.js";
13
15
  import { context, propagation } from "@opentelemetry/api";
14
16
  import "urlpattern-polyfill";
15
17
  //#region ../zero-cache/src/custom/fetch.ts
@@ -40,128 +42,178 @@ var getBodyPreview = async (res, lc) => {
40
42
  }
41
43
  };
42
44
  var MAX_ATTEMPTS = 4;
43
- async function fetchFromAPIServer(validator, source, lc, ctx, shard, body) {
44
- const fetchFromAPIServerID = randInt(1, Number.MAX_SAFE_INTEGER).toString(36);
45
- lc = lc.withContext("fetchFromAPIServerID", fetchFromAPIServerID).withContext("source", source);
46
- const fetchConfig = source === "push" ? ctx.mutateContext : ctx.queryContext;
47
- const url = must(fetchConfig.url, `Fetch config for ${source} is missing URL`);
48
- const headerOptions = fetchConfig.headerOptions;
49
- lc.debug?.("fetchFromAPIServer called", { url });
50
- if (!urlMatch(url, fetchConfig.allowedUrlPatterns ?? [])) throw new ProtocolErrorWithLevel(source === "push" ? {
51
- kind: PushFailed,
52
- origin: ZeroCache,
53
- reason: Internal,
54
- message: `URL "${url}" is not allowed by the ZERO_MUTATE_URL configuration`,
55
- mutationIDs: []
56
- } : {
57
- kind: TransformFailed,
58
- origin: ZeroCache,
59
- reason: Internal,
60
- message: `URL "${url}" is not allowed by the ZERO_QUERY_URL configuration`,
61
- queryIDs: []
62
- }, "warn");
63
- const headers = { "Content-Type": "application/json" };
64
- if (headerOptions.apiKey) headers["X-Api-Key"] = headerOptions.apiKey;
65
- Object.assign(headers, headerOptions.customHeaders, headerOptions.requestHeaders);
66
- if (ctx.auth?.raw) headers["Authorization"] = `Bearer ${ctx.auth.raw}`;
67
- if (headerOptions.cookie) headers["Cookie"] = headerOptions.cookie;
68
- if (headerOptions.origin) headers["Origin"] = headerOptions.origin;
69
- propagation.inject(context.active(), headers);
70
- const urlObj = new URL(url);
71
- const params = new URLSearchParams(urlObj.search);
72
- for (const reserved of reservedParams) assert(!params.has(reserved), `The push URL cannot contain the reserved query param "${reserved}"`);
73
- params.append("schema", upstreamSchema(shard));
74
- params.append("appID", shard.appID);
75
- urlObj.search = params.toString();
76
- const finalUrl = urlObj.toString();
77
- for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
78
- lc = lc.withContext("fetchFromAPIServerAttempt", attempt);
79
- lc.debug?.("fetch from API server attempt");
80
- const shouldRetry = async () => {
81
- if (attempt < MAX_ATTEMPTS) {
82
- const delayMs = getBackoffDelayMs(attempt);
83
- lc.debug?.(`fetch from API server retrying in ${delayMs} ms`);
84
- await sleep(delayMs);
85
- return true;
86
- }
87
- lc.debug?.("fetch from API server reached max attempts, not retrying");
88
- return false;
89
- };
90
- try {
91
- const response = await fetch(finalUrl, {
92
- method: "POST",
93
- headers,
94
- body: JSON.stringify(body)
45
+ async function fetchFromAPIServer(validator, source, lc, ctx, shard, body, metricsOpts) {
46
+ const metricAttrs = metricsOpts.operation === "cleanup" && metricsOpts.cleanupType !== void 0 ? {
47
+ operation: metricsOpts.operation,
48
+ cleanup_type: metricsOpts.cleanupType
49
+ } : { operation: metricsOpts.operation };
50
+ const requestStart = performance.now();
51
+ let requestMetricAttrs;
52
+ let attemptCount = 0;
53
+ apiInFlight().add(1, metricAttrs);
54
+ try {
55
+ const fetchFromAPIServerID = randInt(1, Number.MAX_SAFE_INTEGER).toString(36);
56
+ lc = lc.withContext("fetchFromAPIServerID", fetchFromAPIServerID).withContext("source", source);
57
+ const fetchConfig = source === "push" ? ctx.mutateContext : ctx.queryContext;
58
+ const url = must(fetchConfig.url, `Fetch config for ${source} is missing URL`);
59
+ const headerOptions = fetchConfig.headerOptions;
60
+ lc.debug?.("fetchFromAPIServer called", { url });
61
+ if (!urlMatch(url, fetchConfig.allowedUrlPatterns ?? [])) {
62
+ const errorBody = apiFailedBody(source, Internal, source === "push" ? `URL "${url}" is not allowed by the ZERO_MUTATE_URL configuration` : `URL "${url}" is not allowed by the ZERO_QUERY_URL configuration`);
63
+ requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {
64
+ result: "url_not_allowed",
65
+ attemptCount,
66
+ errorBody
95
67
  });
96
- if (!response.ok) {
97
- const bodyPreview = await getBodyPreview(response, lc);
98
- lc.warn?.("fetch from API server returned non-OK status", {
99
- url: finalUrl,
100
- status: response.status,
101
- bodyPreview
102
- });
103
- if ((response.status === 502 || response.status === 504) && await shouldRetry()) continue;
104
- throw new ProtocolErrorWithLevel(source === "push" ? {
105
- kind: PushFailed,
106
- origin: ZeroCache,
107
- reason: HTTP,
108
- status: response.status,
109
- bodyPreview,
110
- message: `Fetch from API server returned non-OK status ${response.status}`,
111
- mutationIDs: []
112
- } : {
113
- kind: TransformFailed,
114
- origin: ZeroCache,
115
- reason: HTTP,
116
- status: response.status,
117
- bodyPreview,
118
- message: `Fetch from API server returned non-OK status ${response.status}`,
119
- queryIDs: []
120
- }, "warn");
121
- }
68
+ throw new ProtocolErrorWithLevel(errorBody, "warn");
69
+ }
70
+ const headers = { "Content-Type": "application/json" };
71
+ if (headerOptions.apiKey) headers["X-Api-Key"] = headerOptions.apiKey;
72
+ Object.assign(headers, headerOptions.customHeaders, headerOptions.requestHeaders);
73
+ if (ctx.auth?.raw) headers["Authorization"] = `Bearer ${ctx.auth.raw}`;
74
+ if (headerOptions.cookie) headers["Cookie"] = headerOptions.cookie;
75
+ if (headerOptions.origin) headers["Origin"] = headerOptions.origin;
76
+ propagation.inject(context.active(), headers);
77
+ const urlObj = new URL(url);
78
+ const params = new URLSearchParams(urlObj.search);
79
+ for (const reserved of reservedParams) if (params.has(reserved)) {
80
+ requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {
81
+ result: "config_error",
82
+ attemptCount
83
+ });
84
+ throw new Error(`The push URL cannot contain the reserved query param "${reserved}"`);
85
+ }
86
+ params.append("schema", upstreamSchema(shard));
87
+ params.append("appID", shard.appID);
88
+ urlObj.search = params.toString();
89
+ const finalUrl = urlObj.toString();
90
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
91
+ attemptCount = attempt;
92
+ lc = lc.withContext("fetchFromAPIServerAttempt", attempt);
93
+ lc.debug?.("fetch from API server attempt");
94
+ const sleepBeforeRetry = async () => {
95
+ if (attempt < MAX_ATTEMPTS) {
96
+ const delayMs = getBackoffDelayMs(attempt);
97
+ lc.debug?.(`fetch from API server retrying in ${delayMs} ms`);
98
+ await sleep(delayMs);
99
+ return true;
100
+ }
101
+ lc.debug?.("fetch from API server reached max attempts, not retrying");
102
+ return false;
103
+ };
104
+ const attemptStart = performance.now();
122
105
  try {
123
- const json = await response.json();
124
- const result = validator.parse(json, { mode: "passthrough" });
125
- lc.debug?.("fetch from API server succeeded");
126
- return result;
106
+ const response = await fetch(finalUrl, {
107
+ method: "POST",
108
+ headers,
109
+ body: JSON.stringify(body)
110
+ });
111
+ if (!response.ok) {
112
+ const bodyPreview = await getBodyPreview(response, lc);
113
+ lc.warn?.("fetch from API server returned non-OK status", {
114
+ url: finalUrl,
115
+ status: response.status,
116
+ bodyPreview
117
+ });
118
+ const willRetry = (response.status === 502 || response.status === 504) && attempt < MAX_ATTEMPTS;
119
+ recordApiAttempt(performance.now() - attemptStart, metricAttrs, {
120
+ attempt,
121
+ result: "http_error",
122
+ willRetry,
123
+ response
124
+ });
125
+ if (willRetry && await sleepBeforeRetry()) continue;
126
+ const errorBody = apiFailedBody(source, HTTP, `Fetch from API server returned non-OK status ${response.status}`, response, bodyPreview);
127
+ requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {
128
+ result: "http_error",
129
+ attemptCount,
130
+ response,
131
+ errorBody
132
+ });
133
+ throw new ProtocolErrorWithLevel(errorBody, "warn");
134
+ }
135
+ try {
136
+ const json = await response.json();
137
+ const result = validator.parse(json, { mode: "passthrough" });
138
+ const apiError = apiErrorFromResult(result);
139
+ if (apiError) {
140
+ recordApiAttempt(performance.now() - attemptStart, metricAttrs, {
141
+ attempt,
142
+ result: "api_error",
143
+ willRetry: false,
144
+ response,
145
+ errorBody: apiError
146
+ });
147
+ requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {
148
+ result: "api_error",
149
+ attemptCount,
150
+ response,
151
+ errorBody: apiError
152
+ });
153
+ } else {
154
+ recordApiAttempt(performance.now() - attemptStart, metricAttrs, {
155
+ attempt,
156
+ result: "success",
157
+ willRetry: false,
158
+ response
159
+ });
160
+ requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {
161
+ result: "success",
162
+ attemptCount,
163
+ response
164
+ });
165
+ }
166
+ lc.debug?.("fetch from API server succeeded");
167
+ return result;
168
+ } catch (error) {
169
+ lc.warn?.("failed to parse response", { url: finalUrl }, error);
170
+ const errorBody = apiFailedBody(source, Parse, `Failed to parse response from API server: ${getErrorMessage(error)}`);
171
+ recordApiAttempt(performance.now() - attemptStart, metricAttrs, {
172
+ attempt,
173
+ result: "parse_error",
174
+ willRetry: false,
175
+ response,
176
+ errorBody
177
+ });
178
+ requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {
179
+ result: "parse_error",
180
+ attemptCount,
181
+ response,
182
+ errorBody
183
+ });
184
+ throw new ProtocolErrorWithLevel(errorBody, "warn", { cause: error });
185
+ }
127
186
  } catch (error) {
128
- lc.warn?.("failed to parse response", { url: finalUrl }, error);
129
- throw new ProtocolErrorWithLevel(source === "push" ? {
130
- kind: PushFailed,
131
- origin: ZeroCache,
132
- reason: Parse,
133
- message: `Failed to parse response from API server: ${getErrorMessage(error)}`,
134
- mutationIDs: []
135
- } : {
136
- kind: TransformFailed,
137
- origin: ZeroCache,
138
- reason: Parse,
139
- message: `Failed to parse response from API server: ${getErrorMessage(error)}`,
140
- queryIDs: []
141
- }, "warn", { cause: error });
187
+ if (isProtocolError(error)) throw error;
188
+ const isFetchFailed = error instanceof TypeError && error.message === "fetch failed";
189
+ let logLevel = isFetchFailed ? "warn" : "error";
190
+ lc[logLevel]?.("fetch from API server threw error", { url: finalUrl }, error);
191
+ const willRetry = isFetchFailed && attempt < MAX_ATTEMPTS;
192
+ recordApiAttempt(performance.now() - attemptStart, metricAttrs, {
193
+ attempt,
194
+ result: "fetch_error",
195
+ willRetry
196
+ });
197
+ if (willRetry && await sleepBeforeRetry()) continue;
198
+ const errorBody = apiFailedBody(source, Internal, `Fetch from API server threw error: ${getErrorMessage(error)}`);
199
+ requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {
200
+ result: "fetch_error",
201
+ attemptCount,
202
+ errorBody
203
+ });
204
+ throw new ProtocolErrorWithLevel(errorBody, logLevel, { cause: error });
142
205
  }
143
- } catch (error) {
144
- if (isProtocolError(error)) throw error;
145
- const isFetchFailed = error instanceof TypeError && error.message === "fetch failed";
146
- let logLevel = isFetchFailed ? "warn" : "error";
147
- lc[logLevel]?.("fetch from API server threw error", { url: finalUrl }, error);
148
- if (isFetchFailed && await shouldRetry()) continue;
149
- throw new ProtocolErrorWithLevel(source === "push" ? {
150
- kind: PushFailed,
151
- origin: ZeroCache,
152
- reason: Internal,
153
- message: `Fetch from API server threw error: ${getErrorMessage(error)}`,
154
- mutationIDs: []
155
- } : {
156
- kind: TransformFailed,
157
- origin: ZeroCache,
158
- reason: Internal,
159
- message: `Fetch from API server threw error: ${getErrorMessage(error)}`,
160
- queryIDs: []
161
- }, logLevel, { cause: error });
162
206
  }
207
+ unreachable();
208
+ } finally {
209
+ const attrs = requestMetricAttrs ?? apiRequestMetricAttrs(metricAttrs, {
210
+ result: attemptCount === 0 ? "config_error" : "fetch_error",
211
+ attemptCount
212
+ });
213
+ apiRequests().add(1, attrs);
214
+ apiRequestDuration().recordMs(performance.now() - requestStart, attrs);
215
+ apiInFlight().add(-1, metricAttrs);
163
216
  }
164
- unreachable();
165
217
  }
166
218
  /**
167
219
  * Returns true if the url matches one of the allowedUrlPatterns.
@@ -189,6 +241,90 @@ function urlMatch(url, allowedUrlPatterns) {
189
241
  function getBackoffDelayMs(attempt) {
190
242
  return Math.min(1e3, 100 * Math.pow(2, attempt - 1) + Math.random() * 100);
191
243
  }
244
+ function apiFailedBody(source, reason, message, response, bodyPreview) {
245
+ if (source === "push") return reason === "http" ? {
246
+ kind: PushFailed,
247
+ origin: ZeroCache,
248
+ reason,
249
+ status: response?.status ?? 0,
250
+ ...bodyPreview !== void 0 ? { bodyPreview } : {},
251
+ message,
252
+ mutationIDs: []
253
+ } : {
254
+ kind: PushFailed,
255
+ origin: ZeroCache,
256
+ reason,
257
+ message,
258
+ mutationIDs: []
259
+ };
260
+ return reason === "http" ? {
261
+ kind: TransformFailed,
262
+ origin: ZeroCache,
263
+ reason,
264
+ status: response?.status ?? 0,
265
+ ...bodyPreview !== void 0 ? { bodyPreview } : {},
266
+ message,
267
+ queryIDs: []
268
+ } : {
269
+ kind: TransformFailed,
270
+ origin: ZeroCache,
271
+ reason,
272
+ message,
273
+ queryIDs: []
274
+ };
275
+ }
276
+ function apiErrorFromResult(result) {
277
+ const parsed = errorBodySchema.try(result, { mode: "passthrough" });
278
+ if (parsed.ok) return parsed.value;
279
+ if (Array.isArray(result) && result[0] === "transformFailed") {
280
+ const legacyTransformFailed = errorBodySchema.try(result[1], { mode: "passthrough" });
281
+ return legacyTransformFailed.ok ? legacyTransformFailed.value : void 0;
282
+ }
283
+ const legacyPushError = pushErrorSchema.try(result, { mode: "passthrough" });
284
+ return legacyPushError.ok ? {
285
+ kind: PushFailed,
286
+ reason: legacyPushErrorReason(legacyPushError.value.error)
287
+ } : void 0;
288
+ }
289
+ function legacyPushErrorReason(error) {
290
+ switch (error) {
291
+ case "http": return HTTP;
292
+ case "unsupportedPushVersion": return UnsupportedPushVersion;
293
+ case "unsupportedSchemaVersion":
294
+ case "zeroPusher": return Internal;
295
+ }
296
+ }
297
+ function apiRequestMetricAttrs(baseAttrs, { result, attemptCount, response, errorBody }) {
298
+ return {
299
+ ...baseAttrs,
300
+ result,
301
+ attempt_count: attemptCount,
302
+ ...apiResponseErrorMetricAttrs(response, errorBody)
303
+ };
304
+ }
305
+ function apiResponseErrorMetricAttrs(response, errorBody) {
306
+ const attrs = {};
307
+ if (response) {
308
+ attrs.http_status_code = response.status;
309
+ attrs.http_status_class = `${Math.floor(response.status / 100)}xx`;
310
+ }
311
+ if (errorBody) {
312
+ attrs.error_kind = errorBody.kind;
313
+ if (errorBody.reason !== void 0) attrs.error_reason = errorBody.reason;
314
+ }
315
+ return attrs;
316
+ }
317
+ function recordApiAttempt(durationMs, baseAttrs, { attempt, result, willRetry, response, errorBody }) {
318
+ const attrs = {
319
+ ...baseAttrs,
320
+ attempt,
321
+ result,
322
+ will_retry: willRetry,
323
+ ...apiResponseErrorMetricAttrs(response, errorBody)
324
+ };
325
+ apiAttempts().add(1, attrs);
326
+ apiAttemptDuration().recordMs(durationMs, attrs);
327
+ }
192
328
  //#endregion
193
329
  export { compileUrlPattern, fetchFromAPIServer };
194
330
 
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.js","names":[],"sources":["../../../../../zero-cache/src/custom/fetch.ts"],"sourcesContent":["import {context, propagation} from '@opentelemetry/api';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport 'urlpattern-polyfill';\nimport {assert, unreachable} from '../../../shared/src/asserts.ts';\nimport {getErrorMessage} from '../../../shared/src/error.ts';\nimport type {ReadonlyJSONValue} from '../../../shared/src/json.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {randInt} from '../../../shared/src/rand.ts';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport {type Type} from '../../../shared/src/valita.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../zero-protocol/src/error-reason.ts';\nimport {isProtocolError} from '../../../zero-protocol/src/error.ts';\nimport type {ConnectionContext} from '../services/view-syncer/connection-context-manager.ts';\nimport {ProtocolErrorWithLevel} from '../types/error-with-level.ts';\nimport {upstreamSchema, type ShardID} from '../types/shards.ts';\n\nconst reservedParams = ['schema', 'appID'];\n\n/**\n * Compiles and validates a URLPattern from configuration.\n *\n * Patterns must be full URLs (e.g., \"https://api.example.com/endpoint\").\n * URLPattern automatically sets search and hash to wildcard ('*'),\n * which means query parameters and fragments are ignored during matching.\n *\n * @throws Error if the pattern is an invalid URLPattern\n */\nexport function compileUrlPattern(pattern: string): URLPattern {\n try {\n return new URLPattern(pattern);\n } catch (e) {\n throw new Error(\n `Invalid URLPattern in URL configuration: \"${pattern}\". Error: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n}\n\nexport const getBodyPreview = async (\n res: Response,\n lc: LogContext,\n): Promise<string | undefined> => {\n try {\n const body = await res.clone().text();\n if (body.length > 512) {\n return body.slice(0, 512) + '...';\n }\n return body;\n } catch (e) {\n lc.warn?.(\n 'failed to get body preview',\n {\n url: res.url,\n },\n e,\n );\n }\n\n return undefined;\n};\n\nconst MAX_ATTEMPTS = 4;\n\nexport async function fetchFromAPIServer<TValidator extends Type>(\n validator: TValidator,\n source: 'push' | 'transform',\n lc: LogContext,\n ctx: ConnectionContext,\n shard: ShardID,\n body: ReadonlyJSONValue,\n) {\n const fetchFromAPIServerID = randInt(1, Number.MAX_SAFE_INTEGER).toString(36);\n lc = lc\n .withContext('fetchFromAPIServerID', fetchFromAPIServerID)\n .withContext('source', source);\n\n const fetchConfig = source === 'push' ? ctx.mutateContext : ctx.queryContext;\n const url = must(\n fetchConfig.url,\n `Fetch config for ${source} is missing URL`,\n );\n const headerOptions = fetchConfig.headerOptions;\n\n lc.debug?.('fetchFromAPIServer called', {\n url,\n });\n\n if (!urlMatch(url, fetchConfig.allowedUrlPatterns ?? [])) {\n throw new ProtocolErrorWithLevel(\n source === 'push'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `URL \"${url}\" is not allowed by the ZERO_MUTATE_URL configuration`,\n mutationIDs: [],\n }\n : {\n kind: ErrorKind.TransformFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `URL \"${url}\" is not allowed by the ZERO_QUERY_URL configuration`,\n queryIDs: [],\n },\n 'warn',\n );\n }\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n if (headerOptions.apiKey) {\n headers['X-Api-Key'] = headerOptions.apiKey;\n }\n Object.assign(\n headers,\n headerOptions.customHeaders,\n headerOptions.requestHeaders,\n );\n if (ctx.auth?.raw) {\n headers['Authorization'] = `Bearer ${ctx.auth.raw}`;\n }\n if (headerOptions.cookie) {\n headers['Cookie'] = headerOptions.cookie;\n }\n if (headerOptions.origin) {\n headers['Origin'] = headerOptions.origin;\n }\n propagation.inject(context.active(), headers);\n\n const urlObj = new URL(url);\n const params = new URLSearchParams(urlObj.search);\n\n for (const reserved of reservedParams) {\n assert(\n !params.has(reserved),\n `The push URL cannot contain the reserved query param \"${reserved}\"`,\n );\n }\n\n params.append('schema', upstreamSchema(shard));\n params.append('appID', shard.appID);\n\n urlObj.search = params.toString();\n\n const finalUrl = urlObj.toString();\n\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n lc = lc.withContext('fetchFromAPIServerAttempt', attempt);\n lc.debug?.('fetch from API server attempt');\n const shouldRetry = async () => {\n if (attempt < MAX_ATTEMPTS) {\n const delayMs = getBackoffDelayMs(attempt);\n lc.debug?.(`fetch from API server retrying in ${delayMs} ms`);\n await sleep(delayMs);\n return true;\n }\n lc.debug?.('fetch from API server reached max attempts, not retrying');\n return false;\n };\n try {\n const response = await fetch(finalUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const bodyPreview = await getBodyPreview(response, lc);\n lc.warn?.('fetch from API server returned non-OK status', {\n url: finalUrl,\n status: response.status,\n bodyPreview,\n });\n // Bad Gateway or Gateway Timeout indicate the server was not reached\n // We retry these if we have retries remaining.\n if (\n (response.status === 502 || response.status === 504) &&\n (await shouldRetry())\n ) {\n continue;\n }\n\n throw new ProtocolErrorWithLevel(\n source === 'push'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n status: response.status,\n bodyPreview,\n message: `Fetch from API server returned non-OK status ${response.status}`,\n mutationIDs: [],\n }\n : {\n kind: ErrorKind.TransformFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n status: response.status,\n bodyPreview,\n message: `Fetch from API server returned non-OK status ${response.status}`,\n queryIDs: [],\n },\n 'warn',\n );\n }\n\n try {\n const json = await response.json();\n const result = validator.parse(json, {\n mode: 'passthrough',\n });\n lc.debug?.('fetch from API server succeeded');\n return result;\n } catch (error) {\n lc.warn?.(\n 'failed to parse response',\n {\n url: finalUrl,\n },\n error,\n );\n\n throw new ProtocolErrorWithLevel(\n source === 'push'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Parse,\n message: `Failed to parse response from API server: ${getErrorMessage(error)}`,\n mutationIDs: [],\n }\n : {\n kind: ErrorKind.TransformFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Parse,\n message: `Failed to parse response from API server: ${getErrorMessage(error)}`,\n queryIDs: [],\n },\n 'warn',\n {cause: error},\n );\n }\n } catch (error) {\n if (isProtocolError(error)) {\n throw error;\n }\n\n const isFetchFailed =\n error instanceof TypeError && error.message === 'fetch failed';\n // unexpected/unknown errors should be logged at 'error' level so they\n // are investigated\n let logLevel: LogLevel = isFetchFailed ? 'warn' : 'error';\n lc[logLevel]?.(\n 'fetch from API server threw error',\n {url: finalUrl},\n error,\n );\n\n if (isFetchFailed && (await shouldRetry())) {\n continue;\n }\n\n throw new ProtocolErrorWithLevel(\n source === 'push'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Fetch from API server threw error: ${getErrorMessage(error)}`,\n mutationIDs: [],\n }\n : {\n kind: ErrorKind.TransformFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Fetch from API server threw error: ${getErrorMessage(error)}`,\n queryIDs: [],\n },\n logLevel,\n {cause: error},\n );\n }\n }\n unreachable();\n}\n\n/**\n * Returns true if the url matches one of the allowedUrlPatterns.\n *\n * URLPattern automatically ignores query parameters and hash fragments during matching\n * because it sets search and hash to wildcard ('*') by default.\n *\n * Example URLPattern patterns:\n * - \"https://api.example.com/endpoint\" - Exact match for a specific URL\n * - \"https://*.example.com/endpoint\" - Matches any single subdomain (e.g., \"https://api.example.com/endpoint\")\n * - \"https://*.*.example.com/endpoint\" - Matches two subdomains (e.g., \"https://api.v1.example.com/endpoint\")\n * - \"https://api.example.com/*\" - Matches any path under /\n * - \"https://api.example.com/:version/endpoint\" - Matches with named parameter (e.g., \"https://api.example.com/v1/endpoint\")\n */\nexport function urlMatch(\n url: string,\n allowedUrlPatterns: readonly URLPattern[],\n): boolean {\n for (const pattern of allowedUrlPatterns) {\n if (pattern.test(url)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Returns the delay in milliseconds for the next retry attempt using exponential backoff with jitter.\n *\n * The delay assumes the first retry is attempt 1.\n * The formula is: `min(1000, 100 * 2^(attempt - 1) + jitter)` where jitter is between 0 and 100ms.\n */\nfunction getBackoffDelayMs(attempt: number): number {\n return Math.min(1000, 100 * Math.pow(2, attempt - 1) + Math.random() * 100);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fetch.js","names":[],"sources":["../../../../../zero-cache/src/custom/fetch.ts"],"sourcesContent":["import {context, propagation} from '@opentelemetry/api';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport 'urlpattern-polyfill';\nimport {unreachable} from '../../../shared/src/asserts.ts';\nimport {getErrorMessage} from '../../../shared/src/error.ts';\nimport type {ReadonlyJSONValue} from '../../../shared/src/json.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {randInt} from '../../../shared/src/rand.ts';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport {type Type} from '../../../shared/src/valita.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../zero-protocol/src/error-reason.ts';\nimport {\n errorBodySchema,\n isProtocolError,\n type ErrorBody,\n} from '../../../zero-protocol/src/error.ts';\nimport {\n pushErrorSchema,\n type PushError,\n} from '../../../zero-protocol/src/push.ts';\nimport type {ConnectionContext} from '../services/view-syncer/connection-context-manager.ts';\nimport {ProtocolErrorWithLevel} from '../types/error-with-level.ts';\nimport {upstreamSchema, type ShardID} from '../types/shards.ts';\nimport {\n apiAttemptDuration,\n apiAttempts,\n apiInFlight,\n apiRequestDuration,\n apiRequests,\n type ApiAttemptMetricAttrs,\n type ApiAttemptResult,\n type ApiCleanupType,\n type ApiMetricBaseAttrs,\n type ApiOperation,\n type ApiRequestMetricAttrs,\n type ApiRequestResult,\n} from './metrics.ts';\n\nconst reservedParams = ['schema', 'appID'];\n\n/**\n * Compiles and validates a URLPattern from configuration.\n *\n * Patterns must be full URLs (e.g., \"https://api.example.com/endpoint\").\n * URLPattern automatically sets search and hash to wildcard ('*'),\n * which means query parameters and fragments are ignored during matching.\n *\n * @throws Error if the pattern is an invalid URLPattern\n */\nexport function compileUrlPattern(pattern: string): URLPattern {\n try {\n return new URLPattern(pattern);\n } catch (e) {\n throw new Error(\n `Invalid URLPattern in URL configuration: \"${pattern}\". Error: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n}\n\nexport const getBodyPreview = async (\n res: Response,\n lc: LogContext,\n): Promise<string | undefined> => {\n try {\n const body = await res.clone().text();\n if (body.length > 512) {\n return body.slice(0, 512) + '...';\n }\n return body;\n } catch (e) {\n lc.warn?.(\n 'failed to get body preview',\n {\n url: res.url,\n },\n e,\n );\n }\n\n return undefined;\n};\n\nconst MAX_ATTEMPTS = 4;\n\ntype ApiFailedReason =\n | typeof ErrorReason.HTTP\n | typeof ErrorReason.Parse\n | typeof ErrorReason.Internal;\n\nexport type FetchMetricsOptions = {\n operation: ApiOperation;\n cleanupType?: ApiCleanupType | undefined;\n};\n\nexport async function fetchFromAPIServer<TValidator extends Type>(\n validator: TValidator,\n source: 'push' | 'transform',\n lc: LogContext,\n ctx: ConnectionContext,\n shard: ShardID,\n body: ReadonlyJSONValue,\n metricsOpts: FetchMetricsOptions,\n) {\n const metricAttrs: ApiMetricBaseAttrs =\n metricsOpts.operation === 'cleanup' && metricsOpts.cleanupType !== undefined\n ? {\n operation: metricsOpts.operation,\n cleanup_type: metricsOpts.cleanupType,\n }\n : {operation: metricsOpts.operation};\n const requestStart = performance.now();\n let requestMetricAttrs: ApiRequestMetricAttrs | undefined;\n let attemptCount = 0;\n apiInFlight().add(1, metricAttrs);\n\n try {\n const fetchFromAPIServerID = randInt(1, Number.MAX_SAFE_INTEGER).toString(\n 36,\n );\n lc = lc\n .withContext('fetchFromAPIServerID', fetchFromAPIServerID)\n .withContext('source', source);\n\n const fetchConfig =\n source === 'push' ? ctx.mutateContext : ctx.queryContext;\n const url = must(\n fetchConfig.url,\n `Fetch config for ${source} is missing URL`,\n );\n const headerOptions = fetchConfig.headerOptions;\n\n lc.debug?.('fetchFromAPIServer called', {\n url,\n });\n\n if (!urlMatch(url, fetchConfig.allowedUrlPatterns ?? [])) {\n const errorBody = apiFailedBody(\n source,\n ErrorReason.Internal,\n source === 'push'\n ? `URL \"${url}\" is not allowed by the ZERO_MUTATE_URL configuration`\n : `URL \"${url}\" is not allowed by the ZERO_QUERY_URL configuration`,\n );\n requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {\n result: 'url_not_allowed',\n attemptCount,\n errorBody,\n });\n throw new ProtocolErrorWithLevel(errorBody, 'warn');\n }\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n if (headerOptions.apiKey) {\n headers['X-Api-Key'] = headerOptions.apiKey;\n }\n Object.assign(\n headers,\n headerOptions.customHeaders,\n headerOptions.requestHeaders,\n );\n if (ctx.auth?.raw) {\n headers['Authorization'] = `Bearer ${ctx.auth.raw}`;\n }\n if (headerOptions.cookie) {\n headers['Cookie'] = headerOptions.cookie;\n }\n if (headerOptions.origin) {\n headers['Origin'] = headerOptions.origin;\n }\n propagation.inject(context.active(), headers);\n\n const urlObj = new URL(url);\n const params = new URLSearchParams(urlObj.search);\n\n for (const reserved of reservedParams) {\n if (params.has(reserved)) {\n requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {\n result: 'config_error',\n attemptCount,\n });\n throw new Error(\n `The push URL cannot contain the reserved query param \"${reserved}\"`,\n );\n }\n }\n\n params.append('schema', upstreamSchema(shard));\n params.append('appID', shard.appID);\n\n urlObj.search = params.toString();\n\n const finalUrl = urlObj.toString();\n\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n attemptCount = attempt;\n lc = lc.withContext('fetchFromAPIServerAttempt', attempt);\n lc.debug?.('fetch from API server attempt');\n const sleepBeforeRetry = async () => {\n if (attempt < MAX_ATTEMPTS) {\n const delayMs = getBackoffDelayMs(attempt);\n lc.debug?.(`fetch from API server retrying in ${delayMs} ms`);\n await sleep(delayMs);\n return true;\n }\n lc.debug?.('fetch from API server reached max attempts, not retrying');\n return false;\n };\n const attemptStart = performance.now();\n try {\n const response = await fetch(finalUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const bodyPreview = await getBodyPreview(response, lc);\n lc.warn?.('fetch from API server returned non-OK status', {\n url: finalUrl,\n status: response.status,\n bodyPreview,\n });\n // Bad Gateway or Gateway Timeout indicate the server was not reached\n // We retry these if we have retries remaining.\n const willRetry =\n (response.status === 502 || response.status === 504) &&\n attempt < MAX_ATTEMPTS;\n recordApiAttempt(performance.now() - attemptStart, metricAttrs, {\n attempt,\n result: 'http_error',\n willRetry,\n response,\n });\n if (willRetry && (await sleepBeforeRetry())) {\n continue;\n }\n\n const errorBody = apiFailedBody(\n source,\n ErrorReason.HTTP,\n `Fetch from API server returned non-OK status ${response.status}`,\n response,\n bodyPreview,\n );\n requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {\n result: 'http_error',\n attemptCount,\n response,\n errorBody,\n });\n throw new ProtocolErrorWithLevel(errorBody, 'warn');\n }\n\n try {\n const json = await response.json();\n const result = validator.parse(json, {\n mode: 'passthrough',\n });\n const apiError = apiErrorFromResult(result);\n if (apiError) {\n recordApiAttempt(performance.now() - attemptStart, metricAttrs, {\n attempt,\n result: 'api_error',\n willRetry: false,\n response,\n errorBody: apiError,\n });\n requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {\n result: 'api_error',\n attemptCount,\n response,\n errorBody: apiError,\n });\n } else {\n recordApiAttempt(performance.now() - attemptStart, metricAttrs, {\n attempt,\n result: 'success',\n willRetry: false,\n response,\n });\n requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {\n result: 'success',\n attemptCount,\n response,\n });\n }\n lc.debug?.('fetch from API server succeeded');\n return result;\n } catch (error) {\n lc.warn?.(\n 'failed to parse response',\n {\n url: finalUrl,\n },\n error,\n );\n\n const errorBody = apiFailedBody(\n source,\n ErrorReason.Parse,\n `Failed to parse response from API server: ${getErrorMessage(error)}`,\n );\n recordApiAttempt(performance.now() - attemptStart, metricAttrs, {\n attempt,\n result: 'parse_error',\n willRetry: false,\n response,\n errorBody,\n });\n requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {\n result: 'parse_error',\n attemptCount,\n response,\n errorBody,\n });\n throw new ProtocolErrorWithLevel(errorBody, 'warn', {cause: error});\n }\n } catch (error) {\n if (isProtocolError(error)) {\n throw error;\n }\n\n const isFetchFailed =\n error instanceof TypeError && error.message === 'fetch failed';\n // unexpected/unknown errors should be logged at 'error' level so they\n // are investigated\n let logLevel: LogLevel = isFetchFailed ? 'warn' : 'error';\n lc[logLevel]?.(\n 'fetch from API server threw error',\n {url: finalUrl},\n error,\n );\n\n const willRetry = isFetchFailed && attempt < MAX_ATTEMPTS;\n recordApiAttempt(performance.now() - attemptStart, metricAttrs, {\n attempt,\n result: 'fetch_error',\n willRetry,\n });\n\n if (willRetry && (await sleepBeforeRetry())) {\n continue;\n }\n\n const errorBody = apiFailedBody(\n source,\n ErrorReason.Internal,\n `Fetch from API server threw error: ${getErrorMessage(error)}`,\n );\n requestMetricAttrs = apiRequestMetricAttrs(metricAttrs, {\n result: 'fetch_error',\n attemptCount,\n errorBody,\n });\n throw new ProtocolErrorWithLevel(errorBody, logLevel, {cause: error});\n }\n }\n unreachable();\n } finally {\n const attrs =\n requestMetricAttrs ??\n apiRequestMetricAttrs(metricAttrs, {\n result: attemptCount === 0 ? 'config_error' : 'fetch_error',\n attemptCount,\n });\n apiRequests().add(1, attrs);\n apiRequestDuration().recordMs(performance.now() - requestStart, attrs);\n apiInFlight().add(-1, metricAttrs);\n }\n}\n\n/**\n * Returns true if the url matches one of the allowedUrlPatterns.\n *\n * URLPattern automatically ignores query parameters and hash fragments during matching\n * because it sets search and hash to wildcard ('*') by default.\n *\n * Example URLPattern patterns:\n * - \"https://api.example.com/endpoint\" - Exact match for a specific URL\n * - \"https://*.example.com/endpoint\" - Matches any single subdomain (e.g., \"https://api.example.com/endpoint\")\n * - \"https://*.*.example.com/endpoint\" - Matches two subdomains (e.g., \"https://api.v1.example.com/endpoint\")\n * - \"https://api.example.com/*\" - Matches any path under /\n * - \"https://api.example.com/:version/endpoint\" - Matches with named parameter (e.g., \"https://api.example.com/v1/endpoint\")\n */\nexport function urlMatch(\n url: string,\n allowedUrlPatterns: readonly URLPattern[],\n): boolean {\n for (const pattern of allowedUrlPatterns) {\n if (pattern.test(url)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Returns the delay in milliseconds for the next retry attempt using exponential backoff with jitter.\n *\n * The delay assumes the first retry is attempt 1.\n * The formula is: `min(1000, 100 * 2^(attempt - 1) + jitter)` where jitter is between 0 and 100ms.\n */\nfunction getBackoffDelayMs(attempt: number): number {\n return Math.min(1000, 100 * Math.pow(2, attempt - 1) + Math.random() * 100);\n}\n\nfunction apiFailedBody(\n source: 'push' | 'transform',\n reason: ApiFailedReason,\n message: string,\n response?: Response | undefined,\n bodyPreview?: string | undefined,\n): ErrorBody {\n if (source === 'push') {\n return reason === ErrorReason.HTTP\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason,\n status: response?.status ?? 0,\n ...(bodyPreview !== undefined ? {bodyPreview} : {}),\n message,\n mutationIDs: [],\n }\n : {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason,\n message,\n mutationIDs: [],\n };\n }\n\n return reason === ErrorReason.HTTP\n ? {\n kind: ErrorKind.TransformFailed,\n origin: ErrorOrigin.ZeroCache,\n reason,\n status: response?.status ?? 0,\n ...(bodyPreview !== undefined ? {bodyPreview} : {}),\n message,\n queryIDs: [],\n }\n : {\n kind: ErrorKind.TransformFailed,\n origin: ErrorOrigin.ZeroCache,\n reason,\n message,\n queryIDs: [],\n };\n}\n\ntype ApiErrorMetricBody = {\n kind: ErrorBody['kind'];\n reason?: string | undefined;\n};\n\nfunction apiErrorFromResult(result: unknown): ApiErrorMetricBody | undefined {\n const parsed = errorBodySchema.try(result, {mode: 'passthrough'});\n if (parsed.ok) {\n return parsed.value;\n }\n\n if (Array.isArray(result) && result[0] === 'transformFailed') {\n const legacyTransformFailed = errorBodySchema.try(result[1], {\n mode: 'passthrough',\n });\n return legacyTransformFailed.ok ? legacyTransformFailed.value : undefined;\n }\n\n const legacyPushError = pushErrorSchema.try(result, {mode: 'passthrough'});\n return legacyPushError.ok\n ? {\n kind: ErrorKind.PushFailed,\n reason: legacyPushErrorReason(legacyPushError.value.error),\n }\n : undefined;\n}\n\nfunction legacyPushErrorReason(error: PushError['error']): string {\n switch (error) {\n case 'http':\n return ErrorReason.HTTP;\n case 'unsupportedPushVersion':\n return ErrorReason.UnsupportedPushVersion;\n case 'unsupportedSchemaVersion':\n case 'zeroPusher':\n return ErrorReason.Internal;\n }\n}\n\ntype ApiResponseErrorMetricAttrs = Pick<\n ApiRequestMetricAttrs,\n 'http_status_code' | 'http_status_class' | 'error_kind' | 'error_reason'\n>;\n\ntype ApiRequestMetricAttrsOptions = {\n result: ApiRequestResult;\n attemptCount: number;\n response?: Response | undefined;\n errorBody?: ApiErrorMetricBody | undefined;\n};\n\ntype ApiAttemptMetricAttrsOptions = {\n attempt: number;\n result: ApiAttemptResult;\n willRetry: boolean;\n response?: Response | undefined;\n errorBody?: ApiErrorMetricBody | undefined;\n};\n\nfunction apiRequestMetricAttrs(\n baseAttrs: ApiMetricBaseAttrs,\n {result, attemptCount, response, errorBody}: ApiRequestMetricAttrsOptions,\n): ApiRequestMetricAttrs {\n return {\n ...baseAttrs,\n result,\n attempt_count: attemptCount,\n ...apiResponseErrorMetricAttrs(response, errorBody),\n };\n}\n\nfunction apiResponseErrorMetricAttrs(\n response: Response | undefined,\n errorBody: ApiErrorMetricBody | undefined,\n): ApiResponseErrorMetricAttrs {\n const attrs: ApiResponseErrorMetricAttrs = {};\n\n if (response) {\n attrs.http_status_code = response.status;\n attrs.http_status_class = `${Math.floor(response.status / 100)}xx`;\n }\n\n if (errorBody) {\n attrs.error_kind = errorBody.kind;\n if (errorBody.reason !== undefined) {\n attrs.error_reason = errorBody.reason;\n }\n }\n\n return attrs;\n}\n\nfunction recordApiAttempt(\n durationMs: number,\n baseAttrs: ApiMetricBaseAttrs,\n {\n attempt,\n result,\n willRetry,\n response,\n errorBody,\n }: ApiAttemptMetricAttrsOptions,\n) {\n const attrs: ApiAttemptMetricAttrs = {\n ...baseAttrs,\n attempt,\n result,\n will_retry: willRetry,\n ...apiResponseErrorMetricAttrs(response, errorBody),\n };\n apiAttempts().add(1, attrs);\n apiAttemptDuration().recordMs(durationMs, attrs);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAwCA,IAAM,iBAAiB,CAAC,UAAU,OAAO;;;;;;;;;;AAWzC,SAAgB,kBAAkB,SAA6B;CAC7D,IAAI;EACF,OAAO,IAAI,WAAW,OAAO;CAC/B,SAAS,GAAG;EACV,MAAM,IAAI,MACR,6CAA6C,QAAQ,YAAY,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,GAC5G;CACF;AACF;AAEA,IAAa,iBAAiB,OAC5B,KACA,OACgC;CAChC,IAAI;EACF,MAAM,OAAO,MAAM,IAAI,MAAM,EAAE,KAAK;EACpC,IAAI,KAAK,SAAS,KAChB,OAAO,KAAK,MAAM,GAAG,GAAG,IAAI;EAE9B,OAAO;CACT,SAAS,GAAG;EACV,GAAG,OACD,8BACA,EACE,KAAK,IAAI,IACX,GACA,CACF;CACF;AAGF;AAEA,IAAM,eAAe;AAYrB,eAAsB,mBACpB,WACA,QACA,IACA,KACA,OACA,MACA,aACA;CACA,MAAM,cACJ,YAAY,cAAc,aAAa,YAAY,gBAAgB,KAAA,IAC/D;EACE,WAAW,YAAY;EACvB,cAAc,YAAY;CAC5B,IACA,EAAC,WAAW,YAAY,UAAS;CACvC,MAAM,eAAe,YAAY,IAAI;CACrC,IAAI;CACJ,IAAI,eAAe;CACnB,YAAY,EAAE,IAAI,GAAG,WAAW;CAEhC,IAAI;EACF,MAAM,uBAAuB,QAAQ,GAAG,OAAO,gBAAgB,EAAE,SAC/D,EACF;EACA,KAAK,GACF,YAAY,wBAAwB,oBAAoB,EACxD,YAAY,UAAU,MAAM;EAE/B,MAAM,cACJ,WAAW,SAAS,IAAI,gBAAgB,IAAI;EAC9C,MAAM,MAAM,KACV,YAAY,KACZ,oBAAoB,OAAO,gBAC7B;EACA,MAAM,gBAAgB,YAAY;EAElC,GAAG,QAAQ,6BAA6B,EACtC,IACF,CAAC;EAED,IAAI,CAAC,SAAS,KAAK,YAAY,sBAAsB,CAAC,CAAC,GAAG;GACxD,MAAM,YAAY,cAChB,QACA,UACA,WAAW,SACP,QAAQ,IAAI,yDACZ,QAAQ,IAAI,qDAClB;GACA,qBAAqB,sBAAsB,aAAa;IACtD,QAAQ;IACR;IACA;GACF,CAAC;GACD,MAAM,IAAI,uBAAuB,WAAW,MAAM;EACpD;EACA,MAAM,UAAkC,EACtC,gBAAgB,mBAClB;EAEA,IAAI,cAAc,QAChB,QAAQ,eAAe,cAAc;EAEvC,OAAO,OACL,SACA,cAAc,eACd,cAAc,cAChB;EACA,IAAI,IAAI,MAAM,KACZ,QAAQ,mBAAmB,UAAU,IAAI,KAAK;EAEhD,IAAI,cAAc,QAChB,QAAQ,YAAY,cAAc;EAEpC,IAAI,cAAc,QAChB,QAAQ,YAAY,cAAc;EAEpC,YAAY,OAAO,QAAQ,OAAO,GAAG,OAAO;EAE5C,MAAM,SAAS,IAAI,IAAI,GAAG;EAC1B,MAAM,SAAS,IAAI,gBAAgB,OAAO,MAAM;EAEhD,KAAK,MAAM,YAAY,gBACrB,IAAI,OAAO,IAAI,QAAQ,GAAG;GACxB,qBAAqB,sBAAsB,aAAa;IACtD,QAAQ;IACR;GACF,CAAC;GACD,MAAM,IAAI,MACR,yDAAyD,SAAS,EACpE;EACF;EAGF,OAAO,OAAO,UAAU,eAAe,KAAK,CAAC;EAC7C,OAAO,OAAO,SAAS,MAAM,KAAK;EAElC,OAAO,SAAS,OAAO,SAAS;EAEhC,MAAM,WAAW,OAAO,SAAS;EAEjC,KAAK,IAAI,UAAU,GAAG,WAAW,cAAc,WAAW;GACxD,eAAe;GACf,KAAK,GAAG,YAAY,6BAA6B,OAAO;GACxD,GAAG,QAAQ,+BAA+B;GAC1C,MAAM,mBAAmB,YAAY;IACnC,IAAI,UAAU,cAAc;KAC1B,MAAM,UAAU,kBAAkB,OAAO;KACzC,GAAG,QAAQ,qCAAqC,QAAQ,IAAI;KAC5D,MAAM,MAAM,OAAO;KACnB,OAAO;IACT;IACA,GAAG,QAAQ,0DAA0D;IACrE,OAAO;GACT;GACA,MAAM,eAAe,YAAY,IAAI;GACrC,IAAI;IACF,MAAM,WAAW,MAAM,MAAM,UAAU;KACrC,QAAQ;KACR;KACA,MAAM,KAAK,UAAU,IAAI;IAC3B,CAAC;IAED,IAAI,CAAC,SAAS,IAAI;KAChB,MAAM,cAAc,MAAM,eAAe,UAAU,EAAE;KACrD,GAAG,OAAO,gDAAgD;MACxD,KAAK;MACL,QAAQ,SAAS;MACjB;KACF,CAAC;KAGD,MAAM,aACH,SAAS,WAAW,OAAO,SAAS,WAAW,QAChD,UAAU;KACZ,iBAAiB,YAAY,IAAI,IAAI,cAAc,aAAa;MAC9D;MACA,QAAQ;MACR;MACA;KACF,CAAC;KACD,IAAI,aAAc,MAAM,iBAAiB,GACvC;KAGF,MAAM,YAAY,cAChB,QACA,MACA,gDAAgD,SAAS,UACzD,UACA,WACF;KACA,qBAAqB,sBAAsB,aAAa;MACtD,QAAQ;MACR;MACA;MACA;KACF,CAAC;KACD,MAAM,IAAI,uBAAuB,WAAW,MAAM;IACpD;IAEA,IAAI;KACF,MAAM,OAAO,MAAM,SAAS,KAAK;KACjC,MAAM,SAAS,UAAU,MAAM,MAAM,EACnC,MAAM,cACR,CAAC;KACD,MAAM,WAAW,mBAAmB,MAAM;KAC1C,IAAI,UAAU;MACZ,iBAAiB,YAAY,IAAI,IAAI,cAAc,aAAa;OAC9D;OACA,QAAQ;OACR,WAAW;OACX;OACA,WAAW;MACb,CAAC;MACD,qBAAqB,sBAAsB,aAAa;OACtD,QAAQ;OACR;OACA;OACA,WAAW;MACb,CAAC;KACH,OAAO;MACL,iBAAiB,YAAY,IAAI,IAAI,cAAc,aAAa;OAC9D;OACA,QAAQ;OACR,WAAW;OACX;MACF,CAAC;MACD,qBAAqB,sBAAsB,aAAa;OACtD,QAAQ;OACR;OACA;MACF,CAAC;KACH;KACA,GAAG,QAAQ,iCAAiC;KAC5C,OAAO;IACT,SAAS,OAAO;KACd,GAAG,OACD,4BACA,EACE,KAAK,SACP,GACA,KACF;KAEA,MAAM,YAAY,cAChB,QACA,OACA,6CAA6C,gBAAgB,KAAK,GACpE;KACA,iBAAiB,YAAY,IAAI,IAAI,cAAc,aAAa;MAC9D;MACA,QAAQ;MACR,WAAW;MACX;MACA;KACF,CAAC;KACD,qBAAqB,sBAAsB,aAAa;MACtD,QAAQ;MACR;MACA;MACA;KACF,CAAC;KACD,MAAM,IAAI,uBAAuB,WAAW,QAAQ,EAAC,OAAO,MAAK,CAAC;IACpE;GACF,SAAS,OAAO;IACd,IAAI,gBAAgB,KAAK,GACvB,MAAM;IAGR,MAAM,gBACJ,iBAAiB,aAAa,MAAM,YAAY;IAGlD,IAAI,WAAqB,gBAAgB,SAAS;IAClD,GAAG,YACD,qCACA,EAAC,KAAK,SAAQ,GACd,KACF;IAEA,MAAM,YAAY,iBAAiB,UAAU;IAC7C,iBAAiB,YAAY,IAAI,IAAI,cAAc,aAAa;KAC9D;KACA,QAAQ;KACR;IACF,CAAC;IAED,IAAI,aAAc,MAAM,iBAAiB,GACvC;IAGF,MAAM,YAAY,cAChB,QACA,UACA,sCAAsC,gBAAgB,KAAK,GAC7D;IACA,qBAAqB,sBAAsB,aAAa;KACtD,QAAQ;KACR;KACA;IACF,CAAC;IACD,MAAM,IAAI,uBAAuB,WAAW,UAAU,EAAC,OAAO,MAAK,CAAC;GACtE;EACF;EACA,YAAY;CACd,UAAU;EACR,MAAM,QACJ,sBACA,sBAAsB,aAAa;GACjC,QAAQ,iBAAiB,IAAI,iBAAiB;GAC9C;EACF,CAAC;EACH,YAAY,EAAE,IAAI,GAAG,KAAK;EAC1B,mBAAmB,EAAE,SAAS,YAAY,IAAI,IAAI,cAAc,KAAK;EACrE,YAAY,EAAE,IAAI,IAAI,WAAW;CACnC;AACF;;;;;;;;;;;;;;AAeA,SAAgB,SACd,KACA,oBACS;CACT,KAAK,MAAM,WAAW,oBACpB,IAAI,QAAQ,KAAK,GAAG,GAClB,OAAO;CAGX,OAAO;AACT;;;;;;;AAQA,SAAS,kBAAkB,SAAyB;CAClD,OAAO,KAAK,IAAI,KAAM,MAAM,KAAK,IAAI,GAAG,UAAU,CAAC,IAAI,KAAK,OAAO,IAAI,GAAG;AAC5E;AAEA,SAAS,cACP,QACA,QACA,SACA,UACA,aACW;CACX,IAAI,WAAW,QACb,OAAO,WAAW,SACd;EACE,MAAM;EACN,QAAQ;EACR;EACA,QAAQ,UAAU,UAAU;EAC5B,GAAI,gBAAgB,KAAA,IAAY,EAAC,YAAW,IAAI,CAAC;EACjD;EACA,aAAa,CAAC;CAChB,IACA;EACE,MAAM;EACN,QAAQ;EACR;EACA;EACA,aAAa,CAAC;CAChB;CAGN,OAAO,WAAW,SACd;EACE,MAAM;EACN,QAAQ;EACR;EACA,QAAQ,UAAU,UAAU;EAC5B,GAAI,gBAAgB,KAAA,IAAY,EAAC,YAAW,IAAI,CAAC;EACjD;EACA,UAAU,CAAC;CACb,IACA;EACE,MAAM;EACN,QAAQ;EACR;EACA;EACA,UAAU,CAAC;CACb;AACN;AAOA,SAAS,mBAAmB,QAAiD;CAC3E,MAAM,SAAS,gBAAgB,IAAI,QAAQ,EAAC,MAAM,cAAa,CAAC;CAChE,IAAI,OAAO,IACT,OAAO,OAAO;CAGhB,IAAI,MAAM,QAAQ,MAAM,KAAK,OAAO,OAAO,mBAAmB;EAC5D,MAAM,wBAAwB,gBAAgB,IAAI,OAAO,IAAI,EAC3D,MAAM,cACR,CAAC;EACD,OAAO,sBAAsB,KAAK,sBAAsB,QAAQ,KAAA;CAClE;CAEA,MAAM,kBAAkB,gBAAgB,IAAI,QAAQ,EAAC,MAAM,cAAa,CAAC;CACzE,OAAO,gBAAgB,KACnB;EACE,MAAM;EACN,QAAQ,sBAAsB,gBAAgB,MAAM,KAAK;CAC3D,IACA,KAAA;AACN;AAEA,SAAS,sBAAsB,OAAmC;CAChE,QAAQ,OAAR;EACE,KAAK,QACH,OAAO;EACT,KAAK,0BACH,OAAO;EACT,KAAK;EACL,KAAK,cACH,OAAO;CACX;AACF;AAsBA,SAAS,sBACP,WACA,EAAC,QAAQ,cAAc,UAAU,aACV;CACvB,OAAO;EACL,GAAG;EACH;EACA,eAAe;EACf,GAAG,4BAA4B,UAAU,SAAS;CACpD;AACF;AAEA,SAAS,4BACP,UACA,WAC6B;CAC7B,MAAM,QAAqC,CAAC;CAE5C,IAAI,UAAU;EACZ,MAAM,mBAAmB,SAAS;EAClC,MAAM,oBAAoB,GAAG,KAAK,MAAM,SAAS,SAAS,GAAG,EAAE;CACjE;CAEA,IAAI,WAAW;EACb,MAAM,aAAa,UAAU;EAC7B,IAAI,UAAU,WAAW,KAAA,GACvB,MAAM,eAAe,UAAU;CAEnC;CAEA,OAAO;AACT;AAEA,SAAS,iBACP,YACA,WACA,EACE,SACA,QACA,WACA,UACA,aAEF;CACA,MAAM,QAA+B;EACnC,GAAG;EACH;EACA;EACA,YAAY;EACZ,GAAG,4BAA4B,UAAU,SAAS;CACpD;CACA,YAAY,EAAE,IAAI,GAAG,KAAK;CAC1B,mBAAmB,EAAE,SAAS,YAAY,KAAK;AACjD"}
@@ -0,0 +1,31 @@
1
+ export type ApiOperation = 'mutate' | 'query' | 'cleanup' | 'validate_auth';
2
+ export type ApiCleanupType = 'single' | 'bulk';
3
+ export type ApiMetricBaseAttrs = {
4
+ operation: ApiOperation;
5
+ cleanup_type?: ApiCleanupType | undefined;
6
+ };
7
+ export type ApiRequestResult = 'success' | 'api_error' | 'http_error' | 'parse_error' | 'fetch_error' | 'url_not_allowed' | 'config_error';
8
+ export type ApiAttemptResult = 'success' | 'api_error' | 'http_error' | 'parse_error' | 'fetch_error';
9
+ export type ApiRequestMetricAttrs = ApiMetricBaseAttrs & {
10
+ result: ApiRequestResult;
11
+ attempt_count: number;
12
+ http_status_code?: number | undefined;
13
+ http_status_class?: `${string}xx` | undefined;
14
+ error_kind?: string | undefined;
15
+ error_reason?: string | undefined;
16
+ };
17
+ export type ApiAttemptMetricAttrs = ApiMetricBaseAttrs & {
18
+ attempt: number;
19
+ result: ApiAttemptResult;
20
+ will_retry: boolean;
21
+ http_status_code?: number | undefined;
22
+ http_status_class?: `${string}xx` | undefined;
23
+ error_kind?: string | undefined;
24
+ error_reason?: string | undefined;
25
+ };
26
+ export declare function apiRequests(): import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
27
+ export declare function apiRequestDuration(): import("../observability/metrics.ts").LatencyHistogram;
28
+ export declare function apiAttempts(): import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
29
+ export declare function apiAttemptDuration(): import("../observability/metrics.ts").LatencyHistogram;
30
+ export declare function apiInFlight(): import("@opentelemetry/api").UpDownCounter<import("@opentelemetry/api").Attributes>;
31
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/custom/metrics.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,eAAe,CAAC;AAC5E,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE/C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,YAAY,CAAC;IACxB,YAAY,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CAC3C,CAAC;AAEF,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,WAAW,GACX,YAAY,GACZ,aAAa,GACb,aAAa,GACb,iBAAiB,GACjB,cAAc,CAAC;AAEnB,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,WAAW,GACX,YAAY,GACZ,aAAa,GACb,aAAa,CAAC;AAElB,MAAM,MAAM,qBAAqB,GAAG,kBAAkB,GAAG;IACvD,MAAM,EAAE,gBAAgB,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,iBAAiB,CAAC,EAAE,GAAG,MAAM,IAAI,GAAG,SAAS,CAAC;IAC9C,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,kBAAkB,GAAG;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,iBAAiB,CAAC,EAAE,GAAG,MAAM,IAAI,GAAG,SAAS,CAAC;IAC9C,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC,CAAC;AAOF,wBAAgB,WAAW,kFAM1B;AAED,wBAAgB,kBAAkB,2DAMjC;AAED,wBAAgB,WAAW,kFAM1B;AAED,wBAAgB,kBAAkB,2DAMjC;AAED,wBAAgB,WAAW,wFAM1B"}
@@ -0,0 +1,47 @@
1
+ import { getOrCreateCounter, getOrCreateHistogram, getOrCreateUpDownCounter } from "../observability/metrics.js";
2
+ //#region ../zero-cache/src/custom/metrics.ts
3
+ var API_DURATION_HISTOGRAM_BOUNDARIES_S = [
4
+ .001,
5
+ .002,
6
+ .005,
7
+ .01,
8
+ .02,
9
+ .05,
10
+ .1,
11
+ .2,
12
+ .5,
13
+ 1,
14
+ 2,
15
+ 5,
16
+ 10,
17
+ 30,
18
+ 60,
19
+ 120
20
+ ];
21
+ function apiRequests() {
22
+ return getOrCreateCounter("server", "api.requests", "API requests, labeled by operation and result.");
23
+ }
24
+ function apiRequestDuration() {
25
+ return getOrCreateHistogram("server", "api.request_duration", {
26
+ description: "End-to-end API request duration, including retries.",
27
+ unit: "s",
28
+ bucketBoundaries: API_DURATION_HISTOGRAM_BOUNDARIES_S
29
+ });
30
+ }
31
+ function apiAttempts() {
32
+ return getOrCreateCounter("server", "api.attempts", "API HTTP fetch attempts");
33
+ }
34
+ function apiAttemptDuration() {
35
+ return getOrCreateHistogram("server", "api.attempt_duration", {
36
+ description: "API HTTP fetch attempt duration, excluding retry sleep.",
37
+ unit: "s",
38
+ bucketBoundaries: API_DURATION_HISTOGRAM_BOUNDARIES_S
39
+ });
40
+ }
41
+ function apiInFlight() {
42
+ return getOrCreateUpDownCounter("server", "api.in_flight", "API requests currently in flight.");
43
+ }
44
+ //#endregion
45
+ export { apiAttemptDuration, apiAttempts, apiInFlight, apiRequestDuration, apiRequests };
46
+
47
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","names":[],"sources":["../../../../../zero-cache/src/custom/metrics.ts"],"sourcesContent":["import {\n getOrCreateCounter,\n getOrCreateHistogram,\n getOrCreateUpDownCounter,\n} from '../observability/metrics.ts';\n\nexport type ApiOperation = 'mutate' | 'query' | 'cleanup' | 'validate_auth';\nexport type ApiCleanupType = 'single' | 'bulk';\n\nexport type ApiMetricBaseAttrs = {\n operation: ApiOperation;\n cleanup_type?: ApiCleanupType | undefined;\n};\n\nexport type ApiRequestResult =\n | 'success'\n | 'api_error'\n | 'http_error'\n | 'parse_error'\n | 'fetch_error'\n | 'url_not_allowed'\n | 'config_error';\n\nexport type ApiAttemptResult =\n | 'success'\n | 'api_error'\n | 'http_error'\n | 'parse_error'\n | 'fetch_error';\n\nexport type ApiRequestMetricAttrs = ApiMetricBaseAttrs & {\n result: ApiRequestResult;\n attempt_count: number;\n http_status_code?: number | undefined;\n http_status_class?: `${string}xx` | undefined;\n error_kind?: string | undefined;\n error_reason?: string | undefined;\n};\n\nexport type ApiAttemptMetricAttrs = ApiMetricBaseAttrs & {\n attempt: number;\n result: ApiAttemptResult;\n will_retry: boolean;\n http_status_code?: number | undefined;\n http_status_class?: `${string}xx` | undefined;\n error_kind?: string | undefined;\n error_reason?: string | undefined;\n};\n\nconst API_DURATION_HISTOGRAM_BOUNDARIES_S = [\n 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60,\n 120,\n];\n\nexport function apiRequests() {\n return getOrCreateCounter(\n 'server',\n 'api.requests',\n 'API requests, labeled by operation and result.',\n );\n}\n\nexport function apiRequestDuration() {\n return getOrCreateHistogram('server', 'api.request_duration', {\n description: 'End-to-end API request duration, including retries.',\n unit: 's',\n bucketBoundaries: API_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\nexport function apiAttempts() {\n return getOrCreateCounter(\n 'server',\n 'api.attempts',\n 'API HTTP fetch attempts',\n );\n}\n\nexport function apiAttemptDuration() {\n return getOrCreateHistogram('server', 'api.attempt_duration', {\n description: 'API HTTP fetch attempt duration, excluding retry sleep.',\n unit: 's',\n bucketBoundaries: API_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\nexport function apiInFlight() {\n return getOrCreateUpDownCounter(\n 'server',\n 'api.in_flight',\n 'API requests currently in flight.',\n );\n}\n"],"mappings":";;AAiDA,IAAM,sCAAsC;CAC1C;CAAO;CAAO;CAAO;CAAM;CAAM;CAAM;CAAK;CAAK;CAAK;CAAG;CAAG;CAAG;CAAI;CAAI;CACvE;AACF;AAEA,SAAgB,cAAc;CAC5B,OAAO,mBACL,UACA,gBACA,gDACF;AACF;AAEA,SAAgB,qBAAqB;CACnC,OAAO,qBAAqB,UAAU,wBAAwB;EAC5D,aAAa;EACb,MAAM;EACN,kBAAkB;CACpB,CAAC;AACH;AAEA,SAAgB,cAAc;CAC5B,OAAO,mBACL,UACA,gBACA,yBACF;AACF;AAEA,SAAgB,qBAAqB;CACnC,OAAO,qBAAqB,UAAU,wBAAwB;EAC5D,aAAa;EACb,MAAM;EACN,kBAAkB;CACpB,CAAC;AACH;AAEA,SAAgB,cAAc;CAC5B,OAAO,yBACL,UACA,iBACA,mCACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"transform-query.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/custom-queries/transform-query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAKjD,OAAO,EACL,KAAK,YAAY,EAGlB,MAAM,8CAA8C,CAAC;AAItD,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,qCAAqC,CAAC;AAE7C,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,4CAA4C,CAAC;AACpD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,4BAA4B,CAAC;AAErE,OAAO,KAAK,EACV,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,uDAAuD,CAAC;AAC/D,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,yCAAyC,CAAC;AAC/E,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAIhD,MAAM,MAAM,iBAAiB,GACzB;IACE,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,UAAU,EAAE,oBAAoB,CAAC;CAClC,GACD,mBAAmB,CAAC;AAExB,MAAM,MAAM,uBAAuB,GAC/B;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,mBAAmB,CAAC;CAC7B,GACD,CAAC;IACC,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,CAAC,oBAAoB,GAAG,YAAY,CAAC,EAAE,CAAC;CACjD,GAAG,CACA;IACE,MAAM,EAAE,IAAI,CAAC;CACd,GACD;IACE,MAAM,EAAE,KAAK,CAAC;IACd,UAAU,EAAE,oBAAoB,CAAC;CAClC,CACJ,CAAC,CAAC;AAEP;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,sBAAsB;;gBAKrB,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO;IAM1C;;;;;;;;OAQG;IACG,QAAQ,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAM5D,SAAS,CACb,GAAG,EAAE,iBAAiB,EACtB,OAAO,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC;CAwIpC"}
1
+ {"version":3,"file":"transform-query.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/custom-queries/transform-query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAKjD,OAAO,EACL,KAAK,YAAY,EAGlB,MAAM,8CAA8C,CAAC;AAItD,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,qCAAqC,CAAC;AAE7C,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,4CAA4C,CAAC;AACpD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,4BAA4B,CAAC;AAErE,OAAO,KAAK,EACV,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,uDAAuD,CAAC;AAC/D,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,yCAAyC,CAAC;AAC/E,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAIhD,MAAM,MAAM,iBAAiB,GACzB;IACE,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,UAAU,EAAE,oBAAoB,CAAC;CAClC,GACD,mBAAmB,CAAC;AAExB,MAAM,MAAM,uBAAuB,GAC/B;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,mBAAmB,CAAC;CAC7B,GACD,CAAC;IACC,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,CAAC,oBAAoB,GAAG,YAAY,CAAC,EAAE,CAAC;CACjD,GAAG,CACA;IACE,MAAM,EAAE,IAAI,CAAC;CACd,GACD;IACE,MAAM,EAAE,KAAK,CAAC;IACd,UAAU,EAAE,oBAAoB,CAAC;CAClC,CACJ,CAAC,CAAC;AAEP;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,sBAAsB;;gBAKrB,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO;IAM1C;;;;;;;;OAQG;IACG,QAAQ,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAM5D,SAAS,CACb,GAAG,EAAE,iBAAiB,EACtB,OAAO,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC;CAyIpC"}
@@ -103,7 +103,7 @@ var CustomQueryTransformer = class {
103
103
  async #requestTransform(ctx, request, operation) {
104
104
  const queryIDs = request.map(({ id }) => id);
105
105
  try {
106
- const transformResponse = await startAsyncSpan(tracer, "customQueryTransformer.fetchFromAPIServer", () => fetchFromAPIServer(queryResponseSchema, "transform", this.#lc, ctx, this.#shard, ["transform", request]));
106
+ const transformResponse = await startAsyncSpan(tracer, "customQueryTransformer.fetchFromAPIServer", () => fetchFromAPIServer(queryResponseSchema, "transform", this.#lc, ctx, this.#shard, ["transform", request], { operation: operation === "validate" ? "validate_auth" : "query" }));
107
107
  if ("kind" in transformResponse) return transformResponse.kind === "QueryResponse" ? {
108
108
  kind: "QueryResponse",
109
109
  queries: transformResponse.queries,
@@ -1 +1 @@
1
- {"version":3,"file":"transform-query.js","names":["#shard","#cache","#lc","#requestTransform"],"sources":["../../../../../zero-cache/src/custom-queries/transform-query.ts"],"sourcesContent":["import {trace} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {startAsyncSpan} from '../../../otel/src/span.ts';\nimport {TimedCache} from '../../../shared/src/cache.ts';\nimport {getErrorMessage} from '../../../shared/src/error.ts';\nimport {sortedEntries} from '../../../shared/src/sorted-entries.ts';\nimport {\n type ErroredQuery,\n type TransformRequestBody,\n type TransformRequestMessage,\n} from '../../../zero-protocol/src/custom-queries.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../zero-protocol/src/error-reason.ts';\nimport {\n isProtocolError,\n type TransformFailedBody,\n} from '../../../zero-protocol/src/error.ts';\nimport {hashOfAST} from '../../../zero-protocol/src/query-hash.ts';\nimport {\n queryResponseSchema,\n type QueryResponseBody,\n} from '../../../zero-protocol/src/query-server.ts';\nimport type {TransformedAndHashed} from '../auth/read-authorizer.ts';\nimport {fetchFromAPIServer} from '../custom/fetch.ts';\nimport type {\n ConnectionContext,\n ConnectionValidation,\n} from '../services/view-syncer/connection-context-manager.ts';\nimport type {CustomQueryRecord} from '../services/view-syncer/schema/types.ts';\nimport type {ShardID} from '../types/shards.ts';\n\nconst tracer = trace.getTracer('custom-query-transformer');\n\nexport type TransformResponse =\n | {\n kind: 'QueryResponse';\n queries: QueryResponseBody;\n validation: ConnectionValidation;\n }\n | TransformFailedBody;\n\nexport type HashedTransformResponse =\n | {\n kind: 'failed';\n result: TransformFailedBody;\n }\n | ({\n kind: 'success';\n result: (TransformedAndHashed | ErroredQuery)[];\n } & (\n | {\n cached: true;\n }\n | {\n cached: false;\n validation: ConnectionValidation;\n }\n ));\n\n/**\n * Transforms a custom query by calling the user's API server.\n * Caches the transformed queries for 5 seconds to avoid unnecessary API calls.\n *\n * Error responses are not cached as the user may want to retry the query\n * and the error may be transient.\n *\n * The TTL was chosen to be 5 seconds since custom query requests come with\n * a token which itself may have a short TTL (e.g., 10 seconds).\n *\n * Token expiration isn't expected to be exact so this 5 second\n * caching shouldn't cause unexpected behavior. E.g., many JWT libraries\n * implement leeway for expiration checks: https://github.com/panva/jose/blob/main/docs/jwt/verify/interfaces/JWTVerifyOptions.md#clocktolerance\n *\n * The ViewSyncer will call the API server 3-4 times with the exact same queries\n * if we do not cache requests.\n *\n * Caching is safe here because the cache key encodes the effective request\n * identity used for `/query`: auth, userID, forwarded cookies, origin,\n * custom headers, target URL, and the query hash itself.\n */\nexport class CustomQueryTransformer {\n readonly #shard: ShardID;\n readonly #cache: TimedCache<TransformedAndHashed>;\n readonly #lc: LogContext;\n\n constructor(lc: LogContext, shard: ShardID) {\n this.#shard = shard;\n this.#lc = lc;\n this.#cache = new TimedCache(5000); // 5 seconds cache TTL\n }\n\n /**\n * Forces the empty `/query` validation request used by auth maintenance.\n *\n * This stays separate from `transform()` because `transform([], ...)`\n * short-circuits locally and never hits the API server, while validation\n * still needs to make the request so auth failures are surfaced.\n * Successful validation is intentionally opaque because callers only care\n * whether the request succeeded or failed.\n */\n async validate(ctx: ConnectionContext): Promise<TransformResponse> {\n const response = await this.#requestTransform(ctx, [], 'validate');\n\n return response;\n }\n\n async transform(\n ctx: ConnectionContext,\n queries: Iterable<CustomQueryRecord>,\n ): Promise<HashedTransformResponse> {\n const request: TransformRequestBody = [];\n const cachedResponses: TransformedAndHashed[] = [];\n\n // split queries into cached and uncached\n for (const query of queries) {\n const cacheKey = getCacheKey(ctx, query.id);\n const cached = this.#cache.get(cacheKey);\n if (cached) {\n cachedResponses.push(cached);\n } else {\n request.push({\n id: query.id,\n name: query.name,\n args: query.args,\n });\n }\n }\n\n let cached = true;\n\n if (request.length === 0) {\n return {\n kind: 'success',\n result: cachedResponses,\n cached: true,\n };\n } else {\n // we are hitting the server with at least one uncached query\n cached = false;\n }\n\n const response = await this.#requestTransform(ctx, request, 'transform');\n if (response.kind === 'TransformFailed') {\n return {\n kind: 'failed',\n result: response,\n };\n }\n\n const newResponses = response.queries.map(transformed => {\n if ('error' in transformed) {\n return transformed;\n }\n return {\n id: transformed.id,\n transformedAst: transformed.ast,\n transformationHash: hashOfAST(transformed.ast),\n } satisfies TransformedAndHashed;\n });\n\n for (const transformed of newResponses) {\n if ('error' in transformed) {\n // do not cache error responses\n continue;\n }\n const cacheKey = getCacheKey(ctx, transformed.id);\n this.#cache.set(cacheKey, transformed);\n }\n\n return {\n kind: 'success',\n result: [...newResponses, ...cachedResponses],\n validation: response.validation,\n cached,\n };\n }\n\n async #requestTransform(\n ctx: ConnectionContext,\n request: TransformRequestBody,\n operation: 'validate' | 'transform',\n ): Promise<TransformResponse> {\n const queryIDs = request.map(({id}) => id);\n\n try {\n const transformResponse = await startAsyncSpan(\n tracer,\n 'customQueryTransformer.fetchFromAPIServer',\n () =>\n fetchFromAPIServer(\n queryResponseSchema,\n 'transform',\n this.#lc,\n ctx,\n this.#shard,\n ['transform', request] satisfies TransformRequestMessage,\n ),\n );\n\n if ('kind' in transformResponse) {\n return transformResponse.kind === 'QueryResponse'\n ? {\n kind: 'QueryResponse',\n queries: transformResponse.queries,\n validation:\n transformResponse.userID !== undefined\n ? {\n kind: 'server-validated',\n validatedUserID: transformResponse.userID,\n }\n : {kind: 'client-fallback'},\n }\n : transformResponse;\n }\n\n if (transformResponse[0] === 'transformed') {\n // Legacy query response, so we use client fallback\n return {\n kind: 'QueryResponse',\n validation: {kind: 'client-fallback'},\n queries: transformResponse[1],\n };\n }\n\n return transformResponse[1];\n } catch (e) {\n if (\n isProtocolError(e) &&\n e.errorBody.kind === ErrorKind.TransformFailed\n ) {\n return {\n ...e.errorBody,\n queryIDs,\n } as const satisfies TransformFailedBody;\n }\n\n return {\n kind: ErrorKind.TransformFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to ${operation} queries: ${getErrorMessage(e)}`,\n queryIDs,\n } as const satisfies TransformFailedBody;\n }\n }\n}\n\nfunction getCacheKey(ctx: ConnectionContext, queryID: string) {\n // For custom queries, queryID is a hash of the name + args.\n // The apiKey is static for a given transformer instance.\n return JSON.stringify({\n queryID,\n token: ctx.auth?.raw,\n cookie: ctx.queryContext.headerOptions.cookie,\n origin: ctx.queryContext.headerOptions.origin,\n userID: ctx.user,\n url: ctx.queryContext.url,\n customHeaders: normalizedHeaders(\n ctx.queryContext.headerOptions.customHeaders,\n ),\n requestHeaders: normalizedHeaders(\n ctx.queryContext.headerOptions.requestHeaders,\n ),\n });\n}\n\nfunction normalizedHeaders(\n headers: Readonly<Record<string, string>> | undefined,\n) {\n if (!headers) {\n return undefined;\n }\n\n const forwardedHeaders = sortedEntries(headers);\n\n return forwardedHeaders.length === 0\n ? undefined\n : JSON.stringify(forwardedHeaders);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"transform-query.js","names":["#shard","#cache","#lc","#requestTransform"],"sources":["../../../../../zero-cache/src/custom-queries/transform-query.ts"],"sourcesContent":["import {trace} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {startAsyncSpan} from '../../../otel/src/span.ts';\nimport {TimedCache} from '../../../shared/src/cache.ts';\nimport {getErrorMessage} from '../../../shared/src/error.ts';\nimport {sortedEntries} from '../../../shared/src/sorted-entries.ts';\nimport {\n type ErroredQuery,\n type TransformRequestBody,\n type TransformRequestMessage,\n} from '../../../zero-protocol/src/custom-queries.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../zero-protocol/src/error-reason.ts';\nimport {\n isProtocolError,\n type TransformFailedBody,\n} from '../../../zero-protocol/src/error.ts';\nimport {hashOfAST} from '../../../zero-protocol/src/query-hash.ts';\nimport {\n queryResponseSchema,\n type QueryResponseBody,\n} from '../../../zero-protocol/src/query-server.ts';\nimport type {TransformedAndHashed} from '../auth/read-authorizer.ts';\nimport {fetchFromAPIServer} from '../custom/fetch.ts';\nimport type {\n ConnectionContext,\n ConnectionValidation,\n} from '../services/view-syncer/connection-context-manager.ts';\nimport type {CustomQueryRecord} from '../services/view-syncer/schema/types.ts';\nimport type {ShardID} from '../types/shards.ts';\n\nconst tracer = trace.getTracer('custom-query-transformer');\n\nexport type TransformResponse =\n | {\n kind: 'QueryResponse';\n queries: QueryResponseBody;\n validation: ConnectionValidation;\n }\n | TransformFailedBody;\n\nexport type HashedTransformResponse =\n | {\n kind: 'failed';\n result: TransformFailedBody;\n }\n | ({\n kind: 'success';\n result: (TransformedAndHashed | ErroredQuery)[];\n } & (\n | {\n cached: true;\n }\n | {\n cached: false;\n validation: ConnectionValidation;\n }\n ));\n\n/**\n * Transforms a custom query by calling the user's API server.\n * Caches the transformed queries for 5 seconds to avoid unnecessary API calls.\n *\n * Error responses are not cached as the user may want to retry the query\n * and the error may be transient.\n *\n * The TTL was chosen to be 5 seconds since custom query requests come with\n * a token which itself may have a short TTL (e.g., 10 seconds).\n *\n * Token expiration isn't expected to be exact so this 5 second\n * caching shouldn't cause unexpected behavior. E.g., many JWT libraries\n * implement leeway for expiration checks: https://github.com/panva/jose/blob/main/docs/jwt/verify/interfaces/JWTVerifyOptions.md#clocktolerance\n *\n * The ViewSyncer will call the API server 3-4 times with the exact same queries\n * if we do not cache requests.\n *\n * Caching is safe here because the cache key encodes the effective request\n * identity used for `/query`: auth, userID, forwarded cookies, origin,\n * custom headers, target URL, and the query hash itself.\n */\nexport class CustomQueryTransformer {\n readonly #shard: ShardID;\n readonly #cache: TimedCache<TransformedAndHashed>;\n readonly #lc: LogContext;\n\n constructor(lc: LogContext, shard: ShardID) {\n this.#shard = shard;\n this.#lc = lc;\n this.#cache = new TimedCache(5000); // 5 seconds cache TTL\n }\n\n /**\n * Forces the empty `/query` validation request used by auth maintenance.\n *\n * This stays separate from `transform()` because `transform([], ...)`\n * short-circuits locally and never hits the API server, while validation\n * still needs to make the request so auth failures are surfaced.\n * Successful validation is intentionally opaque because callers only care\n * whether the request succeeded or failed.\n */\n async validate(ctx: ConnectionContext): Promise<TransformResponse> {\n const response = await this.#requestTransform(ctx, [], 'validate');\n\n return response;\n }\n\n async transform(\n ctx: ConnectionContext,\n queries: Iterable<CustomQueryRecord>,\n ): Promise<HashedTransformResponse> {\n const request: TransformRequestBody = [];\n const cachedResponses: TransformedAndHashed[] = [];\n\n // split queries into cached and uncached\n for (const query of queries) {\n const cacheKey = getCacheKey(ctx, query.id);\n const cached = this.#cache.get(cacheKey);\n if (cached) {\n cachedResponses.push(cached);\n } else {\n request.push({\n id: query.id,\n name: query.name,\n args: query.args,\n });\n }\n }\n\n let cached = true;\n\n if (request.length === 0) {\n return {\n kind: 'success',\n result: cachedResponses,\n cached: true,\n };\n } else {\n // we are hitting the server with at least one uncached query\n cached = false;\n }\n\n const response = await this.#requestTransform(ctx, request, 'transform');\n if (response.kind === 'TransformFailed') {\n return {\n kind: 'failed',\n result: response,\n };\n }\n\n const newResponses = response.queries.map(transformed => {\n if ('error' in transformed) {\n return transformed;\n }\n return {\n id: transformed.id,\n transformedAst: transformed.ast,\n transformationHash: hashOfAST(transformed.ast),\n } satisfies TransformedAndHashed;\n });\n\n for (const transformed of newResponses) {\n if ('error' in transformed) {\n // do not cache error responses\n continue;\n }\n const cacheKey = getCacheKey(ctx, transformed.id);\n this.#cache.set(cacheKey, transformed);\n }\n\n return {\n kind: 'success',\n result: [...newResponses, ...cachedResponses],\n validation: response.validation,\n cached,\n };\n }\n\n async #requestTransform(\n ctx: ConnectionContext,\n request: TransformRequestBody,\n operation: 'validate' | 'transform',\n ): Promise<TransformResponse> {\n const queryIDs = request.map(({id}) => id);\n\n try {\n const transformResponse = await startAsyncSpan(\n tracer,\n 'customQueryTransformer.fetchFromAPIServer',\n () =>\n fetchFromAPIServer(\n queryResponseSchema,\n 'transform',\n this.#lc,\n ctx,\n this.#shard,\n ['transform', request] satisfies TransformRequestMessage,\n {operation: operation === 'validate' ? 'validate_auth' : 'query'},\n ),\n );\n\n if ('kind' in transformResponse) {\n return transformResponse.kind === 'QueryResponse'\n ? {\n kind: 'QueryResponse',\n queries: transformResponse.queries,\n validation:\n transformResponse.userID !== undefined\n ? {\n kind: 'server-validated',\n validatedUserID: transformResponse.userID,\n }\n : {kind: 'client-fallback'},\n }\n : transformResponse;\n }\n\n if (transformResponse[0] === 'transformed') {\n // Legacy query response, so we use client fallback\n return {\n kind: 'QueryResponse',\n validation: {kind: 'client-fallback'},\n queries: transformResponse[1],\n };\n }\n\n return transformResponse[1];\n } catch (e) {\n if (\n isProtocolError(e) &&\n e.errorBody.kind === ErrorKind.TransformFailed\n ) {\n return {\n ...e.errorBody,\n queryIDs,\n } as const satisfies TransformFailedBody;\n }\n\n return {\n kind: ErrorKind.TransformFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to ${operation} queries: ${getErrorMessage(e)}`,\n queryIDs,\n } as const satisfies TransformFailedBody;\n }\n }\n}\n\nfunction getCacheKey(ctx: ConnectionContext, queryID: string) {\n // For custom queries, queryID is a hash of the name + args.\n // The apiKey is static for a given transformer instance.\n return JSON.stringify({\n queryID,\n token: ctx.auth?.raw,\n cookie: ctx.queryContext.headerOptions.cookie,\n origin: ctx.queryContext.headerOptions.origin,\n userID: ctx.user,\n url: ctx.queryContext.url,\n customHeaders: normalizedHeaders(\n ctx.queryContext.headerOptions.customHeaders,\n ),\n requestHeaders: normalizedHeaders(\n ctx.queryContext.headerOptions.requestHeaders,\n ),\n });\n}\n\nfunction normalizedHeaders(\n headers: Readonly<Record<string, string>> | undefined,\n) {\n if (!headers) {\n return undefined;\n }\n\n const forwardedHeaders = sortedEntries(headers);\n\n return forwardedHeaders.length === 0\n ? undefined\n : JSON.stringify(forwardedHeaders);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"pusher.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pusher.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"names":[],"mappings":"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"}
@@ -84,7 +84,10 @@ var PusherService = class {
84
84
  await fetchFromAPIServer(mutateResponseSchema, "push", this.#lc, connCtx, {
85
85
  appID: this.#config.app.id,
86
86
  shardNum: this.#config.shard.num
87
- }, cleanupBody);
87
+ }, cleanupBody, {
88
+ operation: "cleanup",
89
+ cleanupType: "single"
90
+ });
88
91
  } catch (e) {
89
92
  this.#lc.warn?.("Failed to send cleanup mutation", { error: getErrorMessage(e) });
90
93
  }
@@ -121,7 +124,10 @@ var PusherService = class {
121
124
  await fetchFromAPIServer(mutateResponseSchema, "push", this.#lc, connCtx, {
122
125
  appID: this.#config.app.id,
123
126
  shardNum: this.#config.shard.num
124
- }, cleanupBody);
127
+ }, cleanupBody, {
128
+ operation: "cleanup",
129
+ cleanupType: "bulk"
130
+ });
125
131
  } catch (e) {
126
132
  this.#lc.warn?.("Failed to send bulk cleanup mutation", { error: getErrorMessage(e) });
127
133
  }
@@ -286,7 +292,7 @@ var PushWorker = class {
286
292
  const response = await fetchFromAPIServer(mutateResponseSchema, "push", this.#lc, entry.connCtx, {
287
293
  appID: this.#config.app.id,
288
294
  shardNum: this.#config.shard.num
289
- }, entry.push);
295
+ }, entry.push, { operation: "mutate" });
290
296
  if ("kind" in response && response.kind === "PushFailed" || "error" in response) {
291
297
  if (isAuthErrorBody(response)) {
292
298
  this.#lc.warn?.("Push auth failed; invalidating connection", {
@@ -1 +1 @@
1
- {"version":3,"file":"pusher.js","names":["#connContextManager","#pusher","#queue","#config","#lc","#isStopped","#refCount","#stopped","#clients","#customMutations","#pushes","#processPush","#fanOutResponses","#failDownstream"],"sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"sourcesContent":["import {context, propagation, ROOT_CONTEXT} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {groupBy} from '../../../../shared/src/arrays.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {getErrorMessage} from '../../../../shared/src/error.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../../zero-protocol/src/error-reason.ts';\nimport {\n isProtocolError,\n type PushFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport {\n mutateResponseSchema,\n type MutateResponse,\n} from '../../../../zero-protocol/src/mutate-server.ts';\nimport type {MutationID} from '../../../../zero-protocol/src/mutation-id.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {CLEANUP_RESULTS_MUTATION_NAME} from '../../../../zero-protocol/src/mutation.ts';\nimport {type PushBody} from '../../../../zero-protocol/src/push.ts';\nimport {authEquals, isAuthErrorBody} from '../../auth/auth.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport {fetchFromAPIServer} from '../../custom/fetch.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {HandlerResult, StreamResult} from '../../workers/connection.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport type {\n ConnectionContext,\n ConnectionContextManager,\n ConnectionSelector,\n} from '../view-syncer/connection-context-manager.ts';\n\nexport interface Pusher extends RefCountedService {\n initConnection(selector: ConnectionSelector): Source<Downstream>;\n enqueuePush(selector: ConnectionSelector, push: PushBody): HandlerResult;\n ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void>;\n deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void>;\n}\n\ntype Config = Pick<ZeroConfig, 'app' | 'shard'>;\n\n/**\n * Receives push messages from zero-client and forwards\n * them the the user's API server.\n *\n * If the user's API server is taking too long to process\n * the push, the PusherService will add the push to a queue\n * and send pushes in bulk the next time the user's API server\n * is available.\n *\n * - One PusherService exists per client group.\n * - Mutations for a given client are always sent in-order\n * - Mutations for different clients in the same group may be interleaved\n */\nexport class PusherService implements Service, Pusher {\n readonly id: string;\n readonly #connContextManager: ConnectionContextManager;\n readonly #pusher: PushWorker;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #config: Config;\n readonly #lc: LogContext;\n #stopped: Promise<void> | undefined;\n #refCount = 0;\n #isStopped = false;\n\n constructor(\n appConfig: Config,\n lc: LogContext,\n clientGroupID: string,\n connContextManager: ConnectionContextManager,\n ) {\n this.#connContextManager = connContextManager;\n this.#config = appConfig;\n this.#lc = lc.withContext('component', 'pusherService');\n this.#queue = new Queue();\n this.#pusher = new PushWorker(\n appConfig,\n lc,\n this.#connContextManager,\n this.#queue,\n );\n this.id = clientGroupID;\n }\n\n initConnection(selector: ConnectionSelector) {\n return this.#pusher.initConnection(selector);\n }\n\n enqueuePush(\n selector: ConnectionSelector,\n push: PushBody,\n ): Exclude<HandlerResult, StreamResult> {\n this.#pusher.enqueuePush(\n this.#connContextManager.mustGetConnectionContext(selector),\n push,\n );\n\n return {\n type: 'ok',\n };\n }\n\n async ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void> {\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: upToID.clientID,\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'single',\n clientGroupID: this.id,\n clientID: upToID.clientID,\n upToMutationID: upToID.id,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-${this.id}-${upToID.clientID}-${upToID.id}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n /**\n * Bulk cleanup is routed through the requester's push context.\n *\n * This assumes the client group shares a compatible push endpoint/auth\n * context.\n */\n async deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void> {\n if (clientIDs.length === 0) {\n return;\n }\n\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: clientIDs[0], // Use first client as sender\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'bulk',\n clientGroupID: this.id,\n clientIDs,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-bulk-${this.id}-${Date.now()}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send bulk cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n run(): Promise<void> {\n this.#stopped = this.#pusher.run();\n return this.#stopped;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return must(this.#stopped, 'Stop was called before `run`');\n }\n this.#isStopped = true;\n this.#queue.enqueue('stop');\n return must(this.#stopped, 'Stop was called before `run`');\n }\n}\n\ntype PusherEntry = {\n push: PushBody;\n connCtx: ConnectionContext;\n};\ntype PusherEntryOrStop = PusherEntry | 'stop';\n\n/**\n * Awaits items in the queue then drains and sends them all\n * to the user's API server.\n */\nclass PushWorker {\n readonly #connContextManager: ConnectionContextManager;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #lc: LogContext;\n readonly #config: Config;\n readonly #clients: Map<\n string,\n {wsID: string; downstream: Subscription<Downstream>}\n >;\n\n readonly #customMutations = getOrCreateCounter(\n 'mutation',\n 'custom',\n 'Number of custom mutations processed',\n );\n readonly #pushes = getOrCreateCounter(\n 'mutation',\n 'pushes',\n 'Number of pushes processed by the pusher',\n );\n\n constructor(\n config: Config,\n lc: LogContext,\n connContextManager: ConnectionContextManager,\n queue: Queue<PusherEntryOrStop>,\n ) {\n this.#lc = lc.withContext('component', 'pusher');\n this.#connContextManager = connContextManager;\n this.#queue = queue;\n this.#config = config;\n this.#clients = new Map();\n }\n\n /**\n * Returns a new downstream stream if the clientID,wsID pair has not been seen before.\n * If a clientID already exists with a different wsID, that client's downstream is cancelled.\n */\n initConnection(selector: ConnectionSelector) {\n const existing = this.#clients.get(selector.clientID);\n if (existing && existing.wsID === selector.wsID) {\n // already initialized for this socket\n throw new Error('Connection was already initialized');\n }\n\n // client is back on a new connection\n if (existing) {\n existing.downstream.cancel();\n }\n\n const downstream = Subscription.create<Downstream>({\n cleanup: () => {\n this.#clients.delete(selector.clientID);\n },\n });\n this.#clients.set(selector.clientID, {\n wsID: selector.wsID,\n downstream,\n });\n return downstream;\n }\n\n enqueuePush(connCtx: ConnectionContext, push: PushBody) {\n this.#queue.enqueue({\n push,\n connCtx,\n });\n }\n\n async run() {\n for (;;) {\n const task = await this.#queue.dequeue();\n const rest = this.#queue.drain();\n const [pushes, terminate] = combinePushes([task, ...rest]);\n for (const push of pushes) {\n const parentContext = push.push.traceparent\n ? propagation.extract(ROOT_CONTEXT, {\n traceparent: push.push.traceparent,\n })\n : context.active();\n const response = await context.with(parentContext, () =>\n this.#processPush(push),\n );\n await this.#fanOutResponses(response);\n }\n\n if (terminate) {\n break;\n }\n }\n }\n\n /**\n * 1. If the entire `push` fails, we send the error to relevant clients.\n * 2. If the push succeeds, we look for any mutation failure that should cause the connection to terminate\n * and terminate the connection for those clients.\n */\n #fanOutResponses(response: MutateResponse) {\n const connectionTerminations: (() => void)[] = [];\n\n // if the entire push failed, send that to the client.\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a push error.',\n response,\n );\n // TODO(0xcadams): Fanout is keyed only by clientID here. If a response arrives\n // after reconnect or re-auth, `#clients.get(clientID)` may point at a\n // newer wsID/revision and fail the replacement downstream instead.\n const groupedMutationIDs = groupBy(\n response.mutationIDs ?? [],\n m => m.clientID,\n );\n for (const [clientID, mutationIDs] of groupedMutationIDs) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n // We do not resolve mutations on the client if the push fails\n // as those mutations will be retried.\n if ('error' in response) {\n // This error code path will eventually be removed when we\n // no longer support the legacy push error format.\n const pushFailedBody: PushFailedBody =\n response.error === 'http'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n status: response.status,\n bodyPreview: response.details,\n mutationIDs,\n message: `Fetch from API server returned non-OK status ${response.status}`,\n }\n : response.error === 'unsupportedPushVersion'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.UnsupportedPushVersion,\n mutationIDs,\n message: `Unsupported push version`,\n }\n : {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Internal,\n mutationIDs,\n message:\n response.error === 'zeroPusher'\n ? response.details\n : response.error === 'unsupportedSchemaVersion'\n ? 'Unsupported schema version'\n : 'An unknown error occurred while pushing to the API server',\n };\n\n this.#failDownstream(client.downstream, pushFailedBody);\n } else {\n this.#failDownstream(client.downstream, response);\n }\n }\n } else {\n // Look for mutations results that should cause us to terminate the connection\n // TODO(0xcadams): Same stale-routing issue as above: fatal mutation results are\n // still mapped to the current downstream by clientID only.\n const groupedMutations = groupBy(response.mutations, m => m.id.clientID);\n for (const [clientID, mutations] of groupedMutations) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n let failure: PushFailedBody | undefined;\n let i = 0;\n for (; i < mutations.length; i++) {\n const m = mutations[i];\n if ('error' in m.result) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a mutation error.',\n m.result,\n );\n }\n // This error code path will eventually be removed,\n // keeping this for backwards compatibility, but the server\n // should now return a PushFailedBody with the mutationIDs\n if ('error' in m.result && m.result.error === 'oooMutation') {\n failure = {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.OutOfOrderMutation,\n message: 'mutation was out of order',\n details: m.result.details,\n mutationIDs: mutations.map(m => ({\n clientID: m.id.clientID,\n id: m.id.id,\n })),\n };\n break;\n }\n }\n\n if (failure && i < mutations.length - 1) {\n this.#lc.warn?.(\n 'push-response contains mutations after a mutation which should fatal the connection',\n );\n }\n\n if (failure) {\n connectionTerminations.push(() =>\n this.#failDownstream(client.downstream, failure),\n );\n }\n }\n }\n\n connectionTerminations.forEach(cb => cb());\n }\n\n async #processPush(entry: PusherEntry): Promise<MutateResponse> {\n this.#customMutations.add(entry.push.mutations.length, {\n clientGroupID: entry.push.clientGroupID,\n });\n this.#pushes.add(1, {\n clientGroupID: entry.push.clientGroupID,\n });\n\n // Record custom mutations for telemetry\n recordMutation('custom', entry.push.mutations.length);\n\n const url = must(\n entry.connCtx.mutateContext.url,\n 'ZERO_MUTATE_URL is not set',\n );\n\n this.#lc.debug?.(\n 'pushing to',\n url,\n 'with',\n entry.push.mutations.length,\n 'mutations',\n );\n\n let mutationIDs: MutationID[] = [];\n\n try {\n mutationIDs = entry.push.mutations.map(m => ({\n id: m.id,\n clientID: m.clientID,\n }));\n\n const response = await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n entry.connCtx,\n {\n appID: this.#config.app.id,\n shardNum: this.#config.shard.num,\n },\n entry.push,\n );\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n // A successful push also validates this connection's current auth snapshot.\n // That lets later shared work reuse it without trusting stale credentials.\n this.#connContextManager.validateConnection(\n entry.connCtx,\n entry.connCtx.revision,\n 'kind' in response &&\n response.kind === 'MutateResponse' &&\n response?.userID !== undefined\n ? {kind: 'server-validated', validatedUserID: response.userID}\n : {kind: 'client-fallback'},\n );\n return response;\n } catch (e) {\n if (isProtocolError(e) && e.errorBody.kind === ErrorKind.PushFailed) {\n const response = {\n ...e.errorBody,\n mutationIDs,\n } as const satisfies PushFailedBody;\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n\n if (isProtocolError(e) && isAuthErrorBody(e.errorBody)) {\n // The push completed far enough for local validation to reject the\n // connection, so invalidate it and surface the result as PushFailed.\n this.#lc.warn?.('Push validation failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: e.message,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n message: e.message,\n status: 401,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to push: ${getErrorMessage(e)}`,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n }\n\n #failDownstream(\n downstream: Subscription<Downstream>,\n errorBody: PushFailedBody,\n ): void {\n downstream.fail(new ProtocolErrorWithLevel(errorBody, 'warn'));\n }\n}\n\n/**\n * Pushes for different clients, sockets, or auth revisions could be interleaved.\n *\n * In order to batch safely, we only combine pushes from the same\n * clientID/wsID/revision snapshot.\n */\nexport function combinePushes(\n entries: readonly (PusherEntryOrStop | undefined)[],\n): [PusherEntry[], boolean] {\n const pushesByConnection = new Map<string, PusherEntry[]>();\n\n function collect() {\n const ret: PusherEntry[] = [];\n for (const entries of pushesByConnection.values()) {\n const composite: PusherEntry = {\n ...entries[0],\n push: {\n ...entries[0].push,\n mutations: [],\n },\n };\n ret.push(composite);\n for (const entry of entries) {\n assertAreCompatiblePushes(composite, entry);\n composite.push.mutations.push(...entry.push.mutations);\n }\n }\n return ret;\n }\n\n for (const entry of entries) {\n if (entry === 'stop' || entry === undefined) {\n return [collect(), true];\n }\n\n const key = `${entry.connCtx.clientID}:${entry.connCtx.wsID}:${entry.connCtx.revision}`;\n const existing = pushesByConnection.get(key);\n if (existing) {\n existing.push(entry);\n } else {\n pushesByConnection.set(key, [entry]);\n }\n }\n\n return [collect(), false] as const;\n}\n\n// These invariants should always be true for a given clientID.\n// If they are not, we have a bug in the code somewhere.\nfunction assertAreCompatiblePushes(left: PusherEntry, right: PusherEntry) {\n assert(\n left.connCtx.clientID === right.connCtx.clientID,\n 'clientID must be the same for all pushes',\n );\n assert(\n left.connCtx.wsID === right.connCtx.wsID,\n 'wsID must be the same for all pushes',\n );\n assert(\n left.connCtx.revision === right.connCtx.revision,\n 'revision must be the same for all pushes',\n );\n assert(\n authEquals(left.connCtx.auth, right.connCtx.auth),\n 'auth must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.schemaVersion === right.push.schemaVersion,\n 'schemaVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.pushVersion === right.push.pushVersion,\n 'pushVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.cookie ===\n right.connCtx.mutateContext.headerOptions.cookie,\n 'httpCookie must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.origin ===\n right.connCtx.mutateContext.headerOptions.origin,\n 'origin must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.user.id === right.connCtx.user.id,\n 'userID must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.url === right.connCtx.mutateContext.url,\n 'userPushURL must be the same for all pushes with the same clientID',\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pusher.js","names":["#connContextManager","#pusher","#queue","#config","#lc","#isStopped","#refCount","#stopped","#clients","#customMutations","#pushes","#processPush","#fanOutResponses","#failDownstream"],"sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"sourcesContent":["import {context, propagation, ROOT_CONTEXT} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {groupBy} from '../../../../shared/src/arrays.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {getErrorMessage} from '../../../../shared/src/error.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../../zero-protocol/src/error-reason.ts';\nimport {\n isProtocolError,\n type PushFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport {\n mutateResponseSchema,\n type MutateResponse,\n} from '../../../../zero-protocol/src/mutate-server.ts';\nimport type {MutationID} from '../../../../zero-protocol/src/mutation-id.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {CLEANUP_RESULTS_MUTATION_NAME} from '../../../../zero-protocol/src/mutation.ts';\nimport {type PushBody} from '../../../../zero-protocol/src/push.ts';\nimport {authEquals, isAuthErrorBody} from '../../auth/auth.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport {fetchFromAPIServer} from '../../custom/fetch.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {HandlerResult, StreamResult} from '../../workers/connection.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport type {\n ConnectionContext,\n ConnectionContextManager,\n ConnectionSelector,\n} from '../view-syncer/connection-context-manager.ts';\n\nexport interface Pusher extends RefCountedService {\n initConnection(selector: ConnectionSelector): Source<Downstream>;\n enqueuePush(selector: ConnectionSelector, push: PushBody): HandlerResult;\n ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void>;\n deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void>;\n}\n\ntype Config = Pick<ZeroConfig, 'app' | 'shard'>;\n\n/**\n * Receives push messages from zero-client and forwards\n * them the the user's API server.\n *\n * If the user's API server is taking too long to process\n * the push, the PusherService will add the push to a queue\n * and send pushes in bulk the next time the user's API server\n * is available.\n *\n * - One PusherService exists per client group.\n * - Mutations for a given client are always sent in-order\n * - Mutations for different clients in the same group may be interleaved\n */\nexport class PusherService implements Service, Pusher {\n readonly id: string;\n readonly #connContextManager: ConnectionContextManager;\n readonly #pusher: PushWorker;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #config: Config;\n readonly #lc: LogContext;\n #stopped: Promise<void> | undefined;\n #refCount = 0;\n #isStopped = false;\n\n constructor(\n appConfig: Config,\n lc: LogContext,\n clientGroupID: string,\n connContextManager: ConnectionContextManager,\n ) {\n this.#connContextManager = connContextManager;\n this.#config = appConfig;\n this.#lc = lc.withContext('component', 'pusherService');\n this.#queue = new Queue();\n this.#pusher = new PushWorker(\n appConfig,\n lc,\n this.#connContextManager,\n this.#queue,\n );\n this.id = clientGroupID;\n }\n\n initConnection(selector: ConnectionSelector) {\n return this.#pusher.initConnection(selector);\n }\n\n enqueuePush(\n selector: ConnectionSelector,\n push: PushBody,\n ): Exclude<HandlerResult, StreamResult> {\n this.#pusher.enqueuePush(\n this.#connContextManager.mustGetConnectionContext(selector),\n push,\n );\n\n return {\n type: 'ok',\n };\n }\n\n async ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void> {\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: upToID.clientID,\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'single',\n clientGroupID: this.id,\n clientID: upToID.clientID,\n upToMutationID: upToID.id,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-${this.id}-${upToID.clientID}-${upToID.id}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n {operation: 'cleanup', cleanupType: 'single'},\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n /**\n * Bulk cleanup is routed through the requester's push context.\n *\n * This assumes the client group shares a compatible push endpoint/auth\n * context.\n */\n async deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void> {\n if (clientIDs.length === 0) {\n return;\n }\n\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: clientIDs[0], // Use first client as sender\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'bulk',\n clientGroupID: this.id,\n clientIDs,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-bulk-${this.id}-${Date.now()}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n {operation: 'cleanup', cleanupType: 'bulk'},\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send bulk cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n run(): Promise<void> {\n this.#stopped = this.#pusher.run();\n return this.#stopped;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return must(this.#stopped, 'Stop was called before `run`');\n }\n this.#isStopped = true;\n this.#queue.enqueue('stop');\n return must(this.#stopped, 'Stop was called before `run`');\n }\n}\n\ntype PusherEntry = {\n push: PushBody;\n connCtx: ConnectionContext;\n};\ntype PusherEntryOrStop = PusherEntry | 'stop';\n\n/**\n * Awaits items in the queue then drains and sends them all\n * to the user's API server.\n */\nclass PushWorker {\n readonly #connContextManager: ConnectionContextManager;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #lc: LogContext;\n readonly #config: Config;\n readonly #clients: Map<\n string,\n {wsID: string; downstream: Subscription<Downstream>}\n >;\n\n readonly #customMutations = getOrCreateCounter(\n 'mutation',\n 'custom',\n 'Number of custom mutations processed',\n );\n readonly #pushes = getOrCreateCounter(\n 'mutation',\n 'pushes',\n 'Number of pushes processed by the pusher',\n );\n\n constructor(\n config: Config,\n lc: LogContext,\n connContextManager: ConnectionContextManager,\n queue: Queue<PusherEntryOrStop>,\n ) {\n this.#lc = lc.withContext('component', 'pusher');\n this.#connContextManager = connContextManager;\n this.#queue = queue;\n this.#config = config;\n this.#clients = new Map();\n }\n\n /**\n * Returns a new downstream stream if the clientID,wsID pair has not been seen before.\n * If a clientID already exists with a different wsID, that client's downstream is cancelled.\n */\n initConnection(selector: ConnectionSelector) {\n const existing = this.#clients.get(selector.clientID);\n if (existing && existing.wsID === selector.wsID) {\n // already initialized for this socket\n throw new Error('Connection was already initialized');\n }\n\n // client is back on a new connection\n if (existing) {\n existing.downstream.cancel();\n }\n\n const downstream = Subscription.create<Downstream>({\n cleanup: () => {\n this.#clients.delete(selector.clientID);\n },\n });\n this.#clients.set(selector.clientID, {\n wsID: selector.wsID,\n downstream,\n });\n return downstream;\n }\n\n enqueuePush(connCtx: ConnectionContext, push: PushBody) {\n this.#queue.enqueue({\n push,\n connCtx,\n });\n }\n\n async run() {\n for (;;) {\n const task = await this.#queue.dequeue();\n const rest = this.#queue.drain();\n const [pushes, terminate] = combinePushes([task, ...rest]);\n for (const push of pushes) {\n const parentContext = push.push.traceparent\n ? propagation.extract(ROOT_CONTEXT, {\n traceparent: push.push.traceparent,\n })\n : context.active();\n const response = await context.with(parentContext, () =>\n this.#processPush(push),\n );\n await this.#fanOutResponses(response);\n }\n\n if (terminate) {\n break;\n }\n }\n }\n\n /**\n * 1. If the entire `push` fails, we send the error to relevant clients.\n * 2. If the push succeeds, we look for any mutation failure that should cause the connection to terminate\n * and terminate the connection for those clients.\n */\n #fanOutResponses(response: MutateResponse) {\n const connectionTerminations: (() => void)[] = [];\n\n // if the entire push failed, send that to the client.\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a push error.',\n response,\n );\n // TODO(0xcadams): Fanout is keyed only by clientID here. If a response arrives\n // after reconnect or re-auth, `#clients.get(clientID)` may point at a\n // newer wsID/revision and fail the replacement downstream instead.\n const groupedMutationIDs = groupBy(\n response.mutationIDs ?? [],\n m => m.clientID,\n );\n for (const [clientID, mutationIDs] of groupedMutationIDs) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n // We do not resolve mutations on the client if the push fails\n // as those mutations will be retried.\n if ('error' in response) {\n // This error code path will eventually be removed when we\n // no longer support the legacy push error format.\n const pushFailedBody: PushFailedBody =\n response.error === 'http'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n status: response.status,\n bodyPreview: response.details,\n mutationIDs,\n message: `Fetch from API server returned non-OK status ${response.status}`,\n }\n : response.error === 'unsupportedPushVersion'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.UnsupportedPushVersion,\n mutationIDs,\n message: `Unsupported push version`,\n }\n : {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Internal,\n mutationIDs,\n message:\n response.error === 'zeroPusher'\n ? response.details\n : response.error === 'unsupportedSchemaVersion'\n ? 'Unsupported schema version'\n : 'An unknown error occurred while pushing to the API server',\n };\n\n this.#failDownstream(client.downstream, pushFailedBody);\n } else {\n this.#failDownstream(client.downstream, response);\n }\n }\n } else {\n // Look for mutations results that should cause us to terminate the connection\n // TODO(0xcadams): Same stale-routing issue as above: fatal mutation results are\n // still mapped to the current downstream by clientID only.\n const groupedMutations = groupBy(response.mutations, m => m.id.clientID);\n for (const [clientID, mutations] of groupedMutations) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n let failure: PushFailedBody | undefined;\n let i = 0;\n for (; i < mutations.length; i++) {\n const m = mutations[i];\n if ('error' in m.result) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a mutation error.',\n m.result,\n );\n }\n // This error code path will eventually be removed,\n // keeping this for backwards compatibility, but the server\n // should now return a PushFailedBody with the mutationIDs\n if ('error' in m.result && m.result.error === 'oooMutation') {\n failure = {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.OutOfOrderMutation,\n message: 'mutation was out of order',\n details: m.result.details,\n mutationIDs: mutations.map(m => ({\n clientID: m.id.clientID,\n id: m.id.id,\n })),\n };\n break;\n }\n }\n\n if (failure && i < mutations.length - 1) {\n this.#lc.warn?.(\n 'push-response contains mutations after a mutation which should fatal the connection',\n );\n }\n\n if (failure) {\n connectionTerminations.push(() =>\n this.#failDownstream(client.downstream, failure),\n );\n }\n }\n }\n\n connectionTerminations.forEach(cb => cb());\n }\n\n async #processPush(entry: PusherEntry): Promise<MutateResponse> {\n this.#customMutations.add(entry.push.mutations.length, {\n clientGroupID: entry.push.clientGroupID,\n });\n this.#pushes.add(1, {\n clientGroupID: entry.push.clientGroupID,\n });\n\n // Record custom mutations for telemetry\n recordMutation('custom', entry.push.mutations.length);\n\n const url = must(\n entry.connCtx.mutateContext.url,\n 'ZERO_MUTATE_URL is not set',\n );\n\n this.#lc.debug?.(\n 'pushing to',\n url,\n 'with',\n entry.push.mutations.length,\n 'mutations',\n );\n\n let mutationIDs: MutationID[] = [];\n\n try {\n mutationIDs = entry.push.mutations.map(m => ({\n id: m.id,\n clientID: m.clientID,\n }));\n\n const response = await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n entry.connCtx,\n {\n appID: this.#config.app.id,\n shardNum: this.#config.shard.num,\n },\n entry.push,\n {operation: 'mutate'},\n );\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n // A successful push also validates this connection's current auth snapshot.\n // That lets later shared work reuse it without trusting stale credentials.\n this.#connContextManager.validateConnection(\n entry.connCtx,\n entry.connCtx.revision,\n 'kind' in response &&\n response.kind === 'MutateResponse' &&\n response?.userID !== undefined\n ? {kind: 'server-validated', validatedUserID: response.userID}\n : {kind: 'client-fallback'},\n );\n return response;\n } catch (e) {\n if (isProtocolError(e) && e.errorBody.kind === ErrorKind.PushFailed) {\n const response = {\n ...e.errorBody,\n mutationIDs,\n } as const satisfies PushFailedBody;\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n\n if (isProtocolError(e) && isAuthErrorBody(e.errorBody)) {\n // The push completed far enough for local validation to reject the\n // connection, so invalidate it and surface the result as PushFailed.\n this.#lc.warn?.('Push validation failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: e.message,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n message: e.message,\n status: 401,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to push: ${getErrorMessage(e)}`,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n }\n\n #failDownstream(\n downstream: Subscription<Downstream>,\n errorBody: PushFailedBody,\n ): void {\n downstream.fail(new ProtocolErrorWithLevel(errorBody, 'warn'));\n }\n}\n\n/**\n * Pushes for different clients, sockets, or auth revisions could be interleaved.\n *\n * In order to batch safely, we only combine pushes from the same\n * clientID/wsID/revision snapshot.\n */\nexport function combinePushes(\n entries: readonly (PusherEntryOrStop | undefined)[],\n): [PusherEntry[], boolean] {\n const pushesByConnection = new Map<string, PusherEntry[]>();\n\n function collect() {\n const ret: PusherEntry[] = [];\n for (const entries of pushesByConnection.values()) {\n const composite: PusherEntry = {\n ...entries[0],\n push: {\n ...entries[0].push,\n mutations: [],\n },\n };\n ret.push(composite);\n for (const entry of entries) {\n assertAreCompatiblePushes(composite, entry);\n composite.push.mutations.push(...entry.push.mutations);\n }\n }\n return ret;\n }\n\n for (const entry of entries) {\n if (entry === 'stop' || entry === undefined) {\n return [collect(), true];\n }\n\n const key = `${entry.connCtx.clientID}:${entry.connCtx.wsID}:${entry.connCtx.revision}`;\n const existing = pushesByConnection.get(key);\n if (existing) {\n existing.push(entry);\n } else {\n pushesByConnection.set(key, [entry]);\n }\n }\n\n return [collect(), false] as const;\n}\n\n// These invariants should always be true for a given clientID.\n// If they are not, we have a bug in the code somewhere.\nfunction assertAreCompatiblePushes(left: PusherEntry, right: PusherEntry) {\n assert(\n left.connCtx.clientID === right.connCtx.clientID,\n 'clientID must be the same for all pushes',\n );\n assert(\n left.connCtx.wsID === right.connCtx.wsID,\n 'wsID must be the same for all pushes',\n );\n assert(\n left.connCtx.revision === right.connCtx.revision,\n 'revision must be the same for all pushes',\n );\n assert(\n authEquals(left.connCtx.auth, right.connCtx.auth),\n 'auth must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.schemaVersion === right.push.schemaVersion,\n 'schemaVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.pushVersion === right.push.pushVersion,\n 'pushVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.cookie ===\n right.connCtx.mutateContext.headerOptions.cookie,\n 'httpCookie must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.origin ===\n right.connCtx.mutateContext.headerOptions.origin,\n 'origin must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.user.id === right.connCtx.user.id,\n 'userID must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.url === right.connCtx.mutateContext.url,\n 'userPushURL must be the same for all pushes with the same clientID',\n );\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * The current version of Zero.
4
4
  */
5
- var version = "1.8.0-canary.4";
5
+ var version = "1.8.0-canary.5";
6
6
  //#endregion
7
7
  export { version };
8
8
 
@@ -86,6 +86,6 @@ function isProtocolError(error) {
86
86
  return error instanceof ProtocolError;
87
87
  }
88
88
  //#endregion
89
- export { ProtocolError, errorKindSchema, errorMessageSchema, isProtocolError, pushFailedBodySchema, transformFailedBodySchema };
89
+ export { ProtocolError, errorBodySchema, errorKindSchema, errorMessageSchema, isProtocolError, pushFailedBodySchema, transformFailedBodySchema };
90
90
 
91
91
  //# sourceMappingURL=error.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocicorp/zero",
3
- "version": "1.8.0-canary.4",
3
+ "version": "1.8.0-canary.5",
4
4
  "description": "Zero is a web framework for serverless web development.",
5
5
  "homepage": "https://zero.rocicorp.dev",
6
6
  "bugs": {