@rocicorp/zero 1.8.0-canary.3 → 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.
Files changed (38) hide show
  1. package/out/z2s/src/compiler.d.ts +2 -1
  2. package/out/z2s/src/compiler.d.ts.map +1 -1
  3. package/out/z2s/src/compiler.js +41 -4
  4. package/out/z2s/src/compiler.js.map +1 -1
  5. package/out/zero/package.js +1 -1
  6. package/out/zero/package.js.map +1 -1
  7. package/out/zero-cache/src/custom/fetch.d.ts +6 -1
  8. package/out/zero-cache/src/custom/fetch.d.ts.map +1 -1
  9. package/out/zero-cache/src/custom/fetch.js +255 -119
  10. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  11. package/out/zero-cache/src/custom/metrics.d.ts +31 -0
  12. package/out/zero-cache/src/custom/metrics.d.ts.map +1 -0
  13. package/out/zero-cache/src/custom/metrics.js +47 -0
  14. package/out/zero-cache/src/custom/metrics.js.map +1 -0
  15. package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
  16. package/out/zero-cache/src/custom-queries/transform-query.js +1 -1
  17. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  18. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.d.ts.map +1 -1
  19. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.js +22 -3
  20. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.js.map +1 -1
  21. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.d.ts.map +1 -1
  22. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js +21 -2
  23. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js.map +1 -1
  24. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  25. package/out/zero-cache/src/services/litestream/commands.js +166 -46
  26. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  27. package/out/zero-cache/src/services/litestream/metrics.d.ts +30 -0
  28. package/out/zero-cache/src/services/litestream/metrics.d.ts.map +1 -0
  29. package/out/zero-cache/src/services/litestream/metrics.js +112 -0
  30. package/out/zero-cache/src/services/litestream/metrics.js.map +1 -0
  31. package/out/zero-cache/src/services/mutagen/pusher.d.ts.map +1 -1
  32. package/out/zero-cache/src/services/mutagen/pusher.js +9 -3
  33. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  34. package/out/zero-client/src/client/version.js +1 -1
  35. package/out/zero-protocol/src/error.js +1 -1
  36. package/out/zqlite/src/query-builder.js +5 -3
  37. package/out/zqlite/src/query-builder.js.map +1 -1
  38. package/package.json +1 -1
@@ -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":"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"}
@@ -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,