@reliantlabs/forge-web-runtime 0.12.0 → 0.13.2
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.
- package/README.md +51 -0
- package/dist/benign-errors.d.ts +33 -0
- package/dist/benign-errors.js +115 -0
- package/dist/benign-errors.js.map +1 -0
- package/dist/client-context.d.ts +35 -0
- package/dist/client-context.js +204 -0
- package/dist/client-context.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interceptors.js +10 -3
- package/dist/interceptors.js.map +1 -1
- package/dist/mock-transport.d.ts +41 -5
- package/dist/mock-transport.js +125 -7
- package/dist/mock-transport.js.map +1 -1
- package/dist/node.js +10 -0
- package/dist/node.js.map +1 -1
- package/dist/resource.d.ts +62 -3
- package/dist/resource.js +96 -17
- package/dist/resource.js.map +1 -1
- package/dist/service-hooks.js +8 -6
- package/dist/service-hooks.js.map +1 -1
- package/dist/session-cookie-fallback.d.ts +23 -0
- package/dist/session-cookie-fallback.js +203 -0
- package/dist/session-cookie-fallback.js.map +1 -0
- package/dist/telemetry-gate.d.ts +4 -0
- package/dist/telemetry-gate.js +25 -0
- package/dist/telemetry-gate.js.map +1 -1
- package/dist/telemetry.d.ts +29 -2
- package/dist/telemetry.js +370 -17
- package/dist/telemetry.js.map +1 -1
- package/dist/trace.d.ts +7 -0
- package/dist/trace.js +21 -2
- package/dist/trace.js.map +1 -1
- package/package.json +4 -2
package/dist/interceptors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interceptors.js","sourceRoot":"","sources":["../src/interceptors.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,qEAAqE;AACrE,gFAAgF;AAChF,0EAA0E;AAC1E,wEAAwE;AACxE,2EAA2E;AAC3E,0EAA0E;AAC1E,4EAA4E;AAC5E,yEAAyE;AACzE,6DAA6D;AAC7D,+BAA+B;AAC/B,EAAE;AACF,uEAAuE;AACvE,mEAAmE;AACnE,2EAA2E;AAC3E,UAAU;AACV,EAAE;AACF,yDAAyD;AACzD,EAAE;AACF,4DAA4D;AAC5D,oDAAoD;AACpD,EAAE;AACF,sDAAsD;AACtD,6DAA6D;AAC7D,qEAAqE;AACrE,0EAA0E;AAC1E,wEAAwE;AACxE,wEAAwE;AACxE,2EAA2E;AAC3E,sEAAsE;AACtE,4EAA4E;AAC5E,2EAA2E;AAC3E,eAAe;AACf,OAAO,EAAE,8BAA8B,EAAE,MAAM,wBAAwB,CAAC;AAExE,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA0B7D,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAE1E,8DAA8D;AAC9D,MAAM,UAAU,eAAe,CAAC,GAA6B;IAC3D,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACzD,IAAI,KAAK,EAAE,CAAC;YACV,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,KAAK,EAAE,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC,CAAC;AACJ,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,iBAAiB,CAAC,GAA6B;IAC7D,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC;QAC/B,IAAI,KAAK,EAAE,CAAC;YACV,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACnC,CAAC;QACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QAC7B,MAAM,cAAc,EAAE,CAAC;QACvB,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC/B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC,CAAC;AACJ,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,yBAAyB,CACvC,GAA6B;IAE7B,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QAC7B,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,qEAAqE;YACrE,4DAA4D;YAC5D,iBAAiB;YACjB,MAAM,UAAU,GAAG,cAAc,CAC/B,GAAG,EACH,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAC3C,CAAC;YACF,GAAG,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,CAAC;YAC1B,MAAM,UAAU,CAAC;QACnB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAS,aAAa,CAAC,GAAiC;IACtD,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC;IACrC,IACE,KAAK,KAAK,8BAA8B,CAAC,eAAe;QACxD,KAAK,KAAK,8BAA8B,CAAC,UAAU,EACnD,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAA6B;IAC5D,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,IAAI,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC;IACpC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QAC7B,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC;YACtC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QACD,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,SAAS,CAAC;YACR,IAAI,CAAC;gBACH,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;YACzB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,UAAU,GACd,GAAG,YAAY,kBAAkB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;gBAChE,IAAI,OAAO,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC;oBAC5C,MAAM,UAAU,CAAC;gBACnB,CAAC;gBACD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC;gBAC3D,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;gBACrB,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CACtC,MAAgC,EAAE;IAElC,OAAO;QACL,yBAAyB,CAAC,GAAG,CAAC;QAC9B,eAAe,CAAC,GAAG,CAAC;QACpB,iBAAiB,CAAC,GAAG,CAAC;QACtB,gBAAgB,EAAE;KACnB,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,0EAA0E;AAC1E,0EAA0E;AAC1E,8EAA8E;AAC9E,yEAAyE;AAEzE,OAAO,EACL,QAAQ,EACR,kBAAkB,EAClB,yBAAyB,EACzB,eAAe,EACf,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,cAAc,EACd,oBAAoB,GACrB,MAAM,YAAY,CAAC","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// This module is also the package's SECOND ENTRY POINT, published as\n// \"@reliantlabs/forge-web-runtime/interceptors\". It is the DOM-free, React-free\n// half of the runtime: the transport layer and nothing else. Importing it\n// pulls in exactly ./errors and ./trace — no React, no @types/react, no\n// window/document. That matters for React Native, where Expo pins React 18\n// while the package's components are typed against React 19: a mobile app\n// importing the full barrel would typecheck the components it never renders\n// and fail. Everything re-exported at the bottom of this file is part of\n// this entry point's surface; keep it React-free (guarded by\n// interceptors-entry.test.ts).\n//\n// The Connect transport interceptor stack. This is the one place every\n// frontend→backend call flows through, composed once and handed to\n// createConnectTransport by the app's own connect module. Order (outermost\n// first):\n//\n// error-normalize → auth → brand headers → traceparent\n//\n// error-normalize is outermost so every caller sees a typed\n// ConnectClientError instead of a raw ConnectError.\n//\n// RETRY OWNERSHIP — read before adding a retry layer.\n// The single retry owner is the app's React Query client: it\n// structurally knows reads from writes, so it retries transient READ\n// failures and NEVER retries a mutation — no duplicate writes. Each React\n// Query retry re-runs the queryFn, which re-runs this whole interceptor\n// chain (fresh token + traceparent), so nothing is lost by not retrying\n// here. `retryInterceptor` below is therefore NOT in the default chain; it\n// stays exported (idempotency-gated) only for callers who use the raw\n// transport OUTSIDE React Query. Putting it back in the default chain would\n// stack two retry layers AND retry non-idempotent mutations — the bug this\n// split fixed.\nimport { MethodOptions_IdempotencyLevel } from \"@bufbuild/protobuf/wkt\";\n\nimport { ConnectClientError, normalizeError } from \"./errors.js\";\nimport { telemetryReady } from \"./telemetry-gate.js\";\nimport { injectTraceContext, traceIdFrom } from \"./trace.js\";\n\nimport type {\n Interceptor,\n UnaryRequest,\n StreamRequest,\n} from \"@connectrpc/connect\";\n\n/**\n * RuntimeInterceptorConfig is the seam the app wires its runtime dependencies\n * through. Everything is optional: an unconfigured chain still propagates\n * traceparent and normalizes errors.\n */\nexport interface RuntimeInterceptorConfig {\n /** Returns the current bearer token, or null when unauthenticated. */\n getToken?: () => Promise<string | null> | string | null;\n /** Returns the active brand, attached as the X-Brand header. */\n getBrand?: () => string | null | undefined;\n /** Invoked with the typed error on every failed RPC (telemetry sink). */\n onError?: (err: ConnectClientError) => void;\n /** Max transparent retries for retryable unary failures. Default 2. */\n maxRetries?: number;\n /** Base backoff in ms; grows exponentially with jitter. Default 200. */\n retryBaseMs?: number;\n}\n\nconst sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));\n\n/** Attaches the bearer token from the app's auth provider. */\nexport function authInterceptor(cfg: RuntimeInterceptorConfig): Interceptor {\n return (next) => async (req) => {\n const token = cfg.getToken ? await cfg.getToken() : null;\n if (token) {\n req.header.set(\"Authorization\", `Bearer ${token}`);\n }\n return next(req);\n };\n}\n\n/** Attaches brand context headers when the app supplies them. */\nexport function headerInterceptor(cfg: RuntimeInterceptorConfig): Interceptor {\n return (next) => async (req) => {\n const brand = cfg.getBrand?.();\n if (brand) {\n req.header.set(\"X-Brand\", brand);\n }\n return next(req);\n };\n}\n\n/**\n * Propagates a W3C traceparent so the backend (otelconnect + otelhttp) joins\n * the same distributed trace. Always emits a valid header — see trace.ts.\n *\n * While browser telemetry is still starting it waits (bounded; see\n * telemetry-gate.ts) so the call is made through the SDK's patched fetch and\n * its traceparent names a span the browser really exports.\n */\nexport function traceInterceptor(): Interceptor {\n return (next) => async (req) => {\n await telemetryReady();\n injectTraceContext(req.header);\n return next(req);\n };\n}\n\n/** Converts any thrown failure into a typed ConnectClientError. */\nexport function errorNormalizeInterceptor(\n cfg: RuntimeInterceptorConfig,\n): Interceptor {\n return (next) => async (req) => {\n try {\n return await next(req);\n } catch (err) {\n // traceInterceptor (innermost) has written the traceparent onto this\n // same request by now: the fallback when the server sent no\n // traceresponse.\n const normalized = normalizeError(\n err,\n traceIdFrom(req.header.get(\"traceparent\")),\n );\n cfg.onError?.(normalized);\n throw normalized;\n }\n };\n}\n\n/**\n * retryEligible reports whether a request is SAFE to retry transparently:\n * a no-side-effects / idempotent method (declared via proto\n * `idempotency_level`), or one carrying an explicit Idempotency-Key header.\n * A plain mutation (the default IDEMPOTENCY_UNKNOWN) is never retried, so a\n * transparent retry can't produce a duplicate write.\n */\nfunction retryEligible(req: UnaryRequest | StreamRequest): boolean {\n const level = req.method.idempotency;\n if (\n level === MethodOptions_IdempotencyLevel.NO_SIDE_EFFECTS ||\n level === MethodOptions_IdempotencyLevel.IDEMPOTENT\n ) {\n return true;\n }\n return req.header.has(\"Idempotency-Key\");\n}\n\n/**\n * Transparently retries retryable failures with exponential backoff +\n * jitter — for IDEMPOTENT requests ONLY (see retryEligible). NOT part of the\n * default chain: React Query owns retries (see the retry-ownership note at\n * the top of this file). Exported for callers using the raw transport\n * outside React Query. Streaming RPCs are never retried (a partially-\n * consumed stream can't be safely replayed).\n */\nexport function retryInterceptor(cfg: RuntimeInterceptorConfig): Interceptor {\n const max = cfg.maxRetries ?? 2;\n const base = cfg.retryBaseMs ?? 200;\n return (next) => async (req) => {\n if (req.stream || !retryEligible(req)) {\n return next(req);\n }\n let attempt = 0;\n for (;;) {\n try {\n return await next(req);\n } catch (err) {\n const normalized =\n err instanceof ConnectClientError ? err : normalizeError(err);\n if (attempt >= max || !normalized.retryable) {\n throw normalized;\n }\n const backoff = base * 2 ** attempt + Math.random() * base;\n await sleep(backoff);\n attempt++;\n }\n }\n };\n}\n\n/**\n * buildRuntimeInterceptors composes the full stack in the correct order.\n * Pass the result to createConnectTransport's `interceptors` option.\n *\n * retryInterceptor is deliberately NOT included — React Query is the single\n * retry owner (see the retry-ownership note at the top of this file).\n */\nexport function buildRuntimeInterceptors(\n cfg: RuntimeInterceptorConfig = {},\n): Interceptor[] {\n return [\n errorNormalizeInterceptor(cfg),\n authInterceptor(cfg),\n headerInterceptor(cfg),\n traceInterceptor(),\n ];\n}\n\n// ── The rest of the transport-layer surface ──────────────────────────────\n//\n// These belong to this entry point because the interceptor stack's contract\n// is expressed in them: errorNormalizeInterceptor THROWS ConnectClientError\n// and RuntimeInterceptorConfig.onError RECEIVES one, so a consumer cannot\n// write a handler without the type; setTraceSampled is traceInterceptor's\n// only knob. A consumer of \"@reliantlabs/forge-web-runtime/interceptors\" must\n// never have to fall back to the React-typed barrel to complete the job.\n\nexport {\n codeName,\n ConnectClientError,\n FORGE_ERROR_REASON_HEADER,\n isRetryableCode,\n normalizeError,\n} from \"./errors.js\";\n\nexport {\n freshTraceparent,\n injectTraceContext,\n setTraceSampled,\n traceIdFrom,\n TRACEPARENT_RE,\n TRACERESPONSE_HEADER,\n} from \"./trace.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"interceptors.js","sourceRoot":"","sources":["../src/interceptors.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// This module is also the package's SECOND ENTRY POINT, published as\n// \"@reliantlabs/forge-web-runtime/interceptors\". It is the DOM-free, React-free\n// half of the runtime: the transport layer and nothing else. Importing it\n// pulls in exactly ./errors and ./trace — no React, no @types/react, no\n// window/document. That matters for React Native, where Expo pins React 18\n// while the package's components are typed against React 19: a mobile app\n// importing the full barrel would typecheck the components it never renders\n// and fail. Everything re-exported at the bottom of this file is part of\n// this entry point's surface; keep it React-free (guarded by\n// interceptors-entry.test.ts).\n//\n// The Connect transport interceptor stack. This is the one place every\n// frontend→backend call flows through, composed once and handed to\n// createConnectTransport by the app's own connect module. Order (outermost\n// first):\n//\n// error-normalize → auth → brand headers → traceparent\n//\n// error-normalize is outermost so every caller sees a typed\n// ConnectClientError instead of a raw ConnectError.\n//\n// RETRY OWNERSHIP — read before adding a retry layer.\n// The single retry owner is the app's React Query client: it\n// structurally knows reads from writes, so it retries transient READ\n// failures and NEVER retries a mutation — no duplicate writes. Each React\n// Query retry re-runs the queryFn, which re-runs this whole interceptor\n// chain (fresh token + traceparent), so nothing is lost by not retrying\n// here. `retryInterceptor` below is therefore NOT in the default chain; it\n// stays exported (idempotency-gated) only for callers who use the raw\n// transport OUTSIDE React Query. Putting it back in the default chain would\n// stack two retry layers AND retry non-idempotent mutations — the bug this\n// split fixed.\nimport { MethodOptions_IdempotencyLevel } from \"@bufbuild/protobuf/wkt\";\nimport { context } from \"@opentelemetry/api\";\n\nimport { ConnectClientError, normalizeError } from \"./errors.js\";\nimport { telemetryReady } from \"./telemetry-gate.js\";\nimport { injectTraceContext, traceIdFrom } from \"./trace.js\";\n\nimport type {\n Interceptor,\n UnaryRequest,\n StreamRequest,\n} from \"@connectrpc/connect\";\n\n/**\n * RuntimeInterceptorConfig is the seam the app wires its runtime dependencies\n * through. Everything is optional: an unconfigured chain still propagates\n * traceparent and normalizes errors.\n */\nexport interface RuntimeInterceptorConfig {\n /** Returns the current bearer token, or null when unauthenticated. */\n getToken?: () => Promise<string | null> | string | null;\n /** Returns the active brand, attached as the X-Brand header. */\n getBrand?: () => string | null | undefined;\n /** Invoked with the typed error on every failed RPC (telemetry sink). */\n onError?: (err: ConnectClientError) => void;\n /** Max transparent retries for retryable unary failures. Default 2. */\n maxRetries?: number;\n /** Base backoff in ms; grows exponentially with jitter. Default 200. */\n retryBaseMs?: number;\n}\n\nconst sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));\n\n/** Attaches the bearer token from the app's auth provider. */\nexport function authInterceptor(cfg: RuntimeInterceptorConfig): Interceptor {\n return (next) => async (req) => {\n const parent = context.active();\n const token = cfg.getToken ? await cfg.getToken() : null;\n if (token) {\n req.header.set(\"Authorization\", `Bearer ${token}`);\n }\n // Browser stack context managers cannot carry context through native\n // await. Restore the caller's context before entering the next boundary.\n return context.with(parent, () => next(req));\n };\n}\n\n/** Attaches brand context headers when the app supplies them. */\nexport function headerInterceptor(cfg: RuntimeInterceptorConfig): Interceptor {\n return (next) => async (req) => {\n const brand = cfg.getBrand?.();\n if (brand) {\n req.header.set(\"X-Brand\", brand);\n }\n return next(req);\n };\n}\n\n/**\n * Propagates a W3C traceparent so the backend (otelconnect + otelhttp) joins\n * the same distributed trace. Always emits a valid header — see trace.ts.\n *\n * While browser telemetry is still starting it waits (bounded; see\n * telemetry-gate.ts) so the call is made through the SDK's patched fetch and\n * its traceparent names a span the browser really exports.\n */\nexport function traceInterceptor(): Interceptor {\n return (next) => async (req) => {\n const parent = context.active();\n await telemetryReady();\n return context.with(parent, () => {\n injectTraceContext(req.header);\n return next(req);\n });\n };\n}\n\n/** Converts any thrown failure into a typed ConnectClientError. */\nexport function errorNormalizeInterceptor(\n cfg: RuntimeInterceptorConfig,\n): Interceptor {\n return (next) => async (req) => {\n try {\n return await next(req);\n } catch (err) {\n // traceInterceptor (innermost) has written the traceparent onto this\n // same request by now: the fallback when the server sent no\n // traceresponse.\n const normalized = normalizeError(\n err,\n traceIdFrom(req.header.get(\"traceparent\")),\n );\n cfg.onError?.(normalized);\n throw normalized;\n }\n };\n}\n\n/**\n * retryEligible reports whether a request is SAFE to retry transparently:\n * a no-side-effects / idempotent method (declared via proto\n * `idempotency_level`), or one carrying an explicit Idempotency-Key header.\n * A plain mutation (the default IDEMPOTENCY_UNKNOWN) is never retried, so a\n * transparent retry can't produce a duplicate write.\n */\nfunction retryEligible(req: UnaryRequest | StreamRequest): boolean {\n const level = req.method.idempotency;\n if (\n level === MethodOptions_IdempotencyLevel.NO_SIDE_EFFECTS ||\n level === MethodOptions_IdempotencyLevel.IDEMPOTENT\n ) {\n return true;\n }\n return req.header.has(\"Idempotency-Key\");\n}\n\n/**\n * Transparently retries retryable failures with exponential backoff +\n * jitter — for IDEMPOTENT requests ONLY (see retryEligible). NOT part of the\n * default chain: React Query owns retries (see the retry-ownership note at\n * the top of this file). Exported for callers using the raw transport\n * outside React Query. Streaming RPCs are never retried (a partially-\n * consumed stream can't be safely replayed).\n */\nexport function retryInterceptor(cfg: RuntimeInterceptorConfig): Interceptor {\n const max = cfg.maxRetries ?? 2;\n const base = cfg.retryBaseMs ?? 200;\n return (next) => async (req) => {\n if (req.stream || !retryEligible(req)) {\n return next(req);\n }\n let attempt = 0;\n for (;;) {\n try {\n return await next(req);\n } catch (err) {\n const normalized =\n err instanceof ConnectClientError ? err : normalizeError(err);\n if (attempt >= max || !normalized.retryable) {\n throw normalized;\n }\n const backoff = base * 2 ** attempt + Math.random() * base;\n await sleep(backoff);\n attempt++;\n }\n }\n };\n}\n\n/**\n * buildRuntimeInterceptors composes the full stack in the correct order.\n * Pass the result to createConnectTransport's `interceptors` option.\n *\n * retryInterceptor is deliberately NOT included — React Query is the single\n * retry owner (see the retry-ownership note at the top of this file).\n */\nexport function buildRuntimeInterceptors(\n cfg: RuntimeInterceptorConfig = {},\n): Interceptor[] {\n return [\n errorNormalizeInterceptor(cfg),\n authInterceptor(cfg),\n headerInterceptor(cfg),\n traceInterceptor(),\n ];\n}\n\n// ── The rest of the transport-layer surface ──────────────────────────────\n//\n// These belong to this entry point because the interceptor stack's contract\n// is expressed in them: errorNormalizeInterceptor THROWS ConnectClientError\n// and RuntimeInterceptorConfig.onError RECEIVES one, so a consumer cannot\n// write a handler without the type; setTraceSampled is traceInterceptor's\n// only knob. A consumer of \"@reliantlabs/forge-web-runtime/interceptors\" must\n// never have to fall back to the React-typed barrel to complete the job.\n\nexport {\n codeName,\n ConnectClientError,\n FORGE_ERROR_REASON_HEADER,\n isRetryableCode,\n normalizeError,\n} from \"./errors.js\";\n\nexport {\n freshTraceparent,\n injectTraceContext,\n setTraceSampled,\n traceIdFrom,\n TRACEPARENT_RE,\n TRACERESPONSE_HEADER,\n} from \"./trace.js\";\n"]}
|
package/dist/mock-transport.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type DescMessage } from "@bufbuild/protobuf";
|
|
2
|
-
import { type Transport } from "@connectrpc/connect";
|
|
1
|
+
import { type DescMessage, type DescMethod } from "@bufbuild/protobuf";
|
|
2
|
+
import { ConnectError, type Transport } from "@connectrpc/connect";
|
|
3
3
|
/**
|
|
4
4
|
* The shape the generated scenario files satisfy. Structural on purpose: the
|
|
5
5
|
* project's own `Scenario` type (src/mocks/scenario-types.ts) carries a
|
|
@@ -10,8 +10,12 @@ export interface MockScenario {
|
|
|
10
10
|
name: string;
|
|
11
11
|
/** Per-RPC overrides keyed by `${serviceTypeName}/${methodName}`. */
|
|
12
12
|
handlers: Record<string, ((req: never) => unknown) | undefined>;
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* `true`: forward RPCs no handler matches to the real backend (hybrid mode
|
|
15
|
+
* only). `"first"`: forward EVERY RPC, and answer from the handlers and
|
|
16
|
+
* fixtures only when the backend fails — see the dispatch notes above.
|
|
17
|
+
*/
|
|
18
|
+
passthrough?: boolean | "first";
|
|
15
19
|
/** Non-RPC side effects, run once before the first RPC fires. */
|
|
16
20
|
setup?: () => void;
|
|
17
21
|
}
|
|
@@ -91,6 +95,38 @@ export interface CreateMockTransportOptions {
|
|
|
91
95
|
* forwarded here instead of falling through to the fixtures.
|
|
92
96
|
*/
|
|
93
97
|
fallback?: Transport;
|
|
98
|
+
/**
|
|
99
|
+
* Refuse every RPC that is not a read, with PermissionDenied, before any
|
|
100
|
+
* handler, fixture or backend sees it. `true` classifies by the proto's
|
|
101
|
+
* `idempotency_level = NO_SIDE_EFFECTS`; a function classifies itself (a
|
|
102
|
+
* contract that does not annotate idempotency, e.g. by method name).
|
|
103
|
+
*/
|
|
104
|
+
readOnly?: boolean | ((method: DescMethod) => boolean);
|
|
105
|
+
/** The message a refused write carries. Default names the RPC. */
|
|
106
|
+
readOnlyMessage?: string;
|
|
107
|
+
/**
|
|
108
|
+
* With `passthrough: "first"`: the forwarded call's deadline in ms, after
|
|
109
|
+
* which the scenario answers instead. Default: the caller's own timeout.
|
|
110
|
+
* Enforced by the mock transport itself, so a fallback transport that
|
|
111
|
+
* ignores its timeout cannot hold the call open; a server stream must END
|
|
112
|
+
* within it to count as the backend's answer.
|
|
113
|
+
*/
|
|
114
|
+
passthroughTimeoutMs?: number;
|
|
115
|
+
/**
|
|
116
|
+
* With `passthrough: "first"`: called each time the backend failed (or
|
|
117
|
+
* timed out) and the mock answered instead. A caller's own abort is never
|
|
118
|
+
* a fallback: it rejects as it would against the backend.
|
|
119
|
+
*/
|
|
120
|
+
onPassthroughFallback?: (key: string, error: ConnectError) => void;
|
|
121
|
+
}
|
|
122
|
+
/** Options for {@link resolveActiveScenario}. */
|
|
123
|
+
export interface ResolveScenarioOptions {
|
|
124
|
+
/**
|
|
125
|
+
* The scenario to run, chosen by the app (its host, its runtime config)
|
|
126
|
+
* instead of the URL. When the key is present — even as null or "" — the
|
|
127
|
+
* `?scenario=` parameter is NOT read; null/""/unknown select the default.
|
|
128
|
+
*/
|
|
129
|
+
name?: string | null;
|
|
94
130
|
}
|
|
95
131
|
/**
|
|
96
132
|
* Resolve the scenario the app should run under, and run its `setup()` once.
|
|
@@ -103,7 +139,7 @@ export interface CreateMockTransportOptions {
|
|
|
103
139
|
* `setup()` is for non-RPC state (localStorage flags, sessionStorage). It is
|
|
104
140
|
* synchronous and makes no network calls.
|
|
105
141
|
*/
|
|
106
|
-
export declare function resolveActiveScenario(registry: MockScenarioRegistry): MockScenario;
|
|
142
|
+
export declare function resolveActiveScenario(registry: MockScenarioRegistry, options?: ResolveScenarioOptions): MockScenario;
|
|
107
143
|
/**
|
|
108
144
|
* Normalize whatever a streaming scenario handler returns into an
|
|
109
145
|
* AsyncIterable<unknown>. Accepts arrays, iterables, async iterables, and
|
package/dist/mock-transport.js
CHANGED
|
@@ -16,6 +16,17 @@
|
|
|
16
16
|
// NEXT_PUBLIC_MOCK_API=hybrid), unmatched RPCs are forwarded to the
|
|
17
17
|
// real backend. This lets one scenario stub a single endpoint while
|
|
18
18
|
// everything else exercises the live server.
|
|
19
|
+
//
|
|
20
|
+
// `passthrough: "first"` turns that around: EVERY RPC goes to the
|
|
21
|
+
// backend first, and the scenario (then the fixtures) answer only when
|
|
22
|
+
// it fails or exceeds `passthroughTimeoutMs`. That is a demo or a
|
|
23
|
+
// preview on live data that degrades to sample data instead of to an
|
|
24
|
+
// error page — and `onPassthroughFallback` lets the UI say so.
|
|
25
|
+
//
|
|
26
|
+
// 0. Before any of it, `readOnly` refuses every RPC that is not a read
|
|
27
|
+
// (by default: not marked `idempotency_level = NO_SIDE_EFFECTS`) with
|
|
28
|
+
// PermissionDenied, so neither a handler, a fixture nor the backend
|
|
29
|
+
// ever sees a write — a public demo's guarantee.
|
|
19
30
|
// 3. Otherwise RPCs not overridden fall through to the deterministic
|
|
20
31
|
// per-entity fixtures described by the entity descriptors.
|
|
21
32
|
// 4. Anything else rejects with Code.Unimplemented so callers see a
|
|
@@ -32,7 +43,8 @@
|
|
|
32
43
|
// and this engine out entirely, which is why the generated `connect.ts`
|
|
33
44
|
// reaches its own shim through a dynamic `require()` gated on the mock-mode
|
|
34
45
|
// env var.
|
|
35
|
-
import { create, ScalarType, } from "@bufbuild/protobuf";
|
|
46
|
+
import { create, isMessage, ScalarType, } from "@bufbuild/protobuf";
|
|
47
|
+
import { MethodOptions_IdempotencyLevel } from "@bufbuild/protobuf/wkt";
|
|
36
48
|
import { Code, ConnectError, } from "@connectrpc/connect";
|
|
37
49
|
/**
|
|
38
50
|
* Mutable session stores, keyed by descriptor identity.
|
|
@@ -70,10 +82,12 @@ const initialized = new WeakSet();
|
|
|
70
82
|
* `setup()` is for non-RPC state (localStorage flags, sessionStorage). It is
|
|
71
83
|
* synchronous and makes no network calls.
|
|
72
84
|
*/
|
|
73
|
-
export function resolveActiveScenario(registry) {
|
|
74
|
-
const requested =
|
|
75
|
-
?
|
|
76
|
-
:
|
|
85
|
+
export function resolveActiveScenario(registry, options = {}) {
|
|
86
|
+
const requested = "name" in options
|
|
87
|
+
? (options.name ?? null)
|
|
88
|
+
: typeof globalThis !== "undefined" && globalThis.location
|
|
89
|
+
? new URLSearchParams(globalThis.location.search).get("scenario")
|
|
90
|
+
: null;
|
|
77
91
|
// A ternary, not `&&`: with `??` alone the empty-string falsy branch leaks
|
|
78
92
|
// '' into the inferred union, and '' survives `??` (which only replaces
|
|
79
93
|
// null/undefined), so downstream access on .setup / .handlers would fail
|
|
@@ -119,10 +133,29 @@ function makeUnaryResponse(method, message) {
|
|
|
119
133
|
method: method,
|
|
120
134
|
stream: false,
|
|
121
135
|
header: new Headers(),
|
|
122
|
-
message: message,
|
|
136
|
+
message: asResponseMessage(method.output, message),
|
|
123
137
|
trailer: new Headers(),
|
|
124
138
|
};
|
|
125
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* A handler may return the response as a plain init object
|
|
142
|
+
* (`{ items: [...] }`) instead of `create(Schema, {...})`. A real client
|
|
143
|
+
* always receives a full message — every repeated field an array, every
|
|
144
|
+
* scalar its zero value — so a plain object is completed against the
|
|
145
|
+
* method's output schema here; without that, `res.items.map` on a field the
|
|
146
|
+
* handler left out throws in the UI and nowhere near the scenario.
|
|
147
|
+
*/
|
|
148
|
+
function asResponseMessage(output, value) {
|
|
149
|
+
if (!output || value == null || typeof value !== "object")
|
|
150
|
+
return value;
|
|
151
|
+
if (isMessage(value))
|
|
152
|
+
return value;
|
|
153
|
+
return create(output, value);
|
|
154
|
+
}
|
|
155
|
+
/** The default read-only classification: the proto says the method has no side effects. */
|
|
156
|
+
function isRead(method) {
|
|
157
|
+
return method.idempotency === MethodOptions_IdempotencyLevel.NO_SIDE_EFFECTS;
|
|
158
|
+
}
|
|
126
159
|
// ── List semantics ─────────────────────────────────────────────────────
|
|
127
160
|
//
|
|
128
161
|
// What the backend's generated List does, and therefore what the fixture
|
|
@@ -418,9 +451,62 @@ function buildFixtureTable(entities) {
|
|
|
418
451
|
* real transport when running in hybrid mode.
|
|
419
452
|
*/
|
|
420
453
|
export function createMockTransport(options) {
|
|
421
|
-
const { scenario, entities = [], fallback } = options;
|
|
454
|
+
const { scenario, entities = [], fallback, readOnly, passthroughTimeoutMs, onPassthroughFallback, } = options;
|
|
422
455
|
const passthrough = scenario.passthrough === true && fallback != null;
|
|
456
|
+
const first = scenario.passthrough === "first" && fallback != null;
|
|
423
457
|
const fixtures = buildFixtureTable(entities);
|
|
458
|
+
const readsOnly = typeof readOnly === "function" ? readOnly : isRead;
|
|
459
|
+
// 0) The read-only guard: before any dispatch, so a write never reaches a
|
|
460
|
+
// handler, a fixture store or the backend.
|
|
461
|
+
const guard = (method, key) => {
|
|
462
|
+
if (readOnly && !readsOnly(method)) {
|
|
463
|
+
throw new ConnectError(options.readOnlyMessage ?? `mock-transport: ${key} is not a read`, Code.PermissionDenied);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
// "first": the backend's answer, or undefined when the mock should answer.
|
|
467
|
+
//
|
|
468
|
+
// The deadline is enforced HERE, not left to the fallback transport: the
|
|
469
|
+
// attempt gets its own signal (aborted by the caller's signal or by the
|
|
470
|
+
// timer) and is raced against that signal, so a transport that ignores
|
|
471
|
+
// both its timeout and its signal still cannot hold the call open. A
|
|
472
|
+
// caller's own abort is rethrown; only the deadline or a failure falls back.
|
|
473
|
+
const tryBackend = async (key, signal, timeoutMs, call) => {
|
|
474
|
+
const attempt = new AbortController();
|
|
475
|
+
const abortAttempt = () => attempt.abort(signal?.reason);
|
|
476
|
+
if (signal?.aborted)
|
|
477
|
+
abortAttempt();
|
|
478
|
+
signal?.addEventListener("abort", abortAttempt, { once: true });
|
|
479
|
+
const timer = timeoutMs != null && timeoutMs > 0
|
|
480
|
+
? setTimeout(() => attempt.abort(new ConnectError(`mock-transport: ${key} exceeded ${timeoutMs}ms`, Code.DeadlineExceeded)), timeoutMs)
|
|
481
|
+
: undefined;
|
|
482
|
+
const abandoned = new Promise((_, reject) => {
|
|
483
|
+
const fail = () => reject(attempt.signal.reason);
|
|
484
|
+
if (attempt.signal.aborted)
|
|
485
|
+
fail();
|
|
486
|
+
else
|
|
487
|
+
attempt.signal.addEventListener("abort", fail, { once: true });
|
|
488
|
+
});
|
|
489
|
+
// The race observes it; this keeps the release below from surfacing as
|
|
490
|
+
// an unhandled rejection after the race has settled.
|
|
491
|
+
abandoned.catch(() => undefined);
|
|
492
|
+
try {
|
|
493
|
+
return await Promise.race([call(attempt.signal), abandoned]);
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
if (signal?.aborted)
|
|
497
|
+
throw err;
|
|
498
|
+
onPassthroughFallback?.(key, ConnectError.from(err));
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
if (timer !== undefined)
|
|
503
|
+
clearTimeout(timer);
|
|
504
|
+
signal?.removeEventListener("abort", abortAttempt);
|
|
505
|
+
// Release whatever the backend attempt still holds (a pending fetch,
|
|
506
|
+
// an unread stream) once it has been answered or abandoned.
|
|
507
|
+
attempt.abort();
|
|
508
|
+
}
|
|
509
|
+
};
|
|
424
510
|
// Bind the object literal to a Transport-typed variable rather than
|
|
425
511
|
// casting at the return site. A trailing assertion cast does not propagate
|
|
426
512
|
// Connect's Transport signature backwards into the literal's method
|
|
@@ -434,6 +520,14 @@ export function createMockTransport(options) {
|
|
|
434
520
|
// fixture keys are matched against.
|
|
435
521
|
async unary(method, signal, timeoutMs, header, input, contextValues) {
|
|
436
522
|
const key = `${method.parent.typeName}/${method.name}`;
|
|
523
|
+
guard(method, key);
|
|
524
|
+
// 0b) "first": the backend answers unless it fails.
|
|
525
|
+
if (first) {
|
|
526
|
+
const deadline = passthroughTimeoutMs ?? timeoutMs;
|
|
527
|
+
const live = await tryBackend(key, signal, deadline, (attempt) => fallback.unary(method, attempt, deadline, header, input, contextValues));
|
|
528
|
+
if (live)
|
|
529
|
+
return live;
|
|
530
|
+
}
|
|
437
531
|
// 1) Scenario overlay.
|
|
438
532
|
const handler = scenario.handlers[key];
|
|
439
533
|
if (handler) {
|
|
@@ -473,6 +567,30 @@ export function createMockTransport(options) {
|
|
|
473
567
|
// ill-typed (TS2322) because fallback.stream returns StreamResponse<I,O>.
|
|
474
568
|
async stream(method, signal, timeoutMs, header, input, contextValues) {
|
|
475
569
|
const key = `${method.parent.typeName}/${method.name}`;
|
|
570
|
+
guard(method, key);
|
|
571
|
+
// "first" forwards server streams only: a client-streaming request is
|
|
572
|
+
// consumed by the attempt and could not be replayed to the handler.
|
|
573
|
+
//
|
|
574
|
+
// The backend's stream is read to its END, within the deadline, before
|
|
575
|
+
// it counts as an answer, then replayed: a stream that fails or stalls
|
|
576
|
+
// after its headers arrived still falls back to the mock, as a unary
|
|
577
|
+
// call does. So "first" suits finite server streams; a stream that
|
|
578
|
+
// never ends always answers from the mock once the deadline passes.
|
|
579
|
+
if (first && method.methodKind === "server_streaming") {
|
|
580
|
+
const deadline = passthroughTimeoutMs ?? timeoutMs;
|
|
581
|
+
const live = await tryBackend(key, signal, deadline, async (attempt) => {
|
|
582
|
+
const res = await fallback.stream(method, attempt, deadline, header, input, contextValues);
|
|
583
|
+
const messages = [];
|
|
584
|
+
for await (const m of res.message)
|
|
585
|
+
messages.push(m);
|
|
586
|
+
return {
|
|
587
|
+
...res,
|
|
588
|
+
message: toAsyncIterable(messages),
|
|
589
|
+
};
|
|
590
|
+
});
|
|
591
|
+
if (live)
|
|
592
|
+
return live;
|
|
593
|
+
}
|
|
476
594
|
const handler = scenario.handlers[key];
|
|
477
595
|
if (!handler) {
|
|
478
596
|
// Passthrough mirrors the unary path: in hybrid mode, unmatched
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-transport.js","sourceRoot":"","sources":["../src/mock-transport.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,+CAA+C;AAC/C,EAAE;AACF,2DAA2D;AAC3D,EAAE;AACF,yEAAyE;AACzE,yEAAyE;AACzE,sEAAsE;AACtE,4DAA4D;AAC5D,qEAAqE;AACrE,oEAAoE;AACpE,yEAAyE;AACzE,yEAAyE;AACzE,kDAAkD;AAClD,uEAAuE;AACvE,gEAAgE;AAChE,sEAAsE;AACtE,uDAAuD;AACvD,EAAE;AACF,wEAAwE;AACxE,6EAA6E;AAC7E,gEAAgE;AAChE,qEAAqE;AACrE,gEAAgE;AAChE,EAAE;AACF,gFAAgF;AAChF,2EAA2E;AAC3E,wEAAwE;AACxE,4EAA4E;AAC5E,WAAW;AACX,OAAO,EACL,MAAM,EACN,UAAU,GAIX,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,IAAI,EACJ,YAAY,GAGb,MAAM,qBAAqB,CAAC;AAsG7B;;;;;;;;;;GAUG;AACH,MAAM,MAAM,GAAG,IAAI,OAAO,EAA8C,CAAC;AAEzE,SAAS,QAAQ,CAAC,MAA4B;IAC5C,IAAI,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,KAAK,GAAG,IAAI,GAAG,CACb,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC;YAC3B,MAAM,CAAE,GAA+B,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACxD,GAAG;SACJ,CAAC,CACH,CAAC;QACF,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,iDAAiD;AACjD,MAAM,WAAW,GAAG,IAAI,OAAO,EAAgB,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAA8B;IAE9B,MAAM,SAAS,GACb,OAAO,UAAU,KAAK,WAAW,IAAI,UAAU,CAAC,QAAQ;QACtD,CAAC,CAAC,IAAI,eAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC;QACjE,CAAC,CAAC,IAAI,CAAC;IACX,2EAA2E;IAC3E,wEAAwE;IACxE,yEAAyE;IACzE,oBAAoB;IACpB,MAAM,MAAM,GACV,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACpD,QAAQ,CAAC,eAAe,CAAC;IAE3B,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7B,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACxB,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;IACnB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,eAAe,CAAC,KAAc;IACnD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9C,IAAI,QAAQ,IAAI,IAAI;QAAE,OAAO;IAC7B,gBAAgB;IAChB,IACE,OAAQ,QAAiD,CACvD,MAAM,CAAC,aAAa,CACrB,KAAK,UAAU,EAChB,CAAC;QACD,IAAI,KAAK,EAAE,MAAM,CAAC,IAAI,QAAkC;YAAE,MAAM,CAAC,CAAC;QAClE,OAAO;IACT,CAAC;IACD,8BAA8B;IAC9B,IACE,OAAQ,QAA4C,CAAC,MAAM,CAAC,QAAQ,CAAC;QACrE,UAAU,EACV,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,QAA6B;YAAE,MAAM,CAAC,CAAC;QACvD,OAAO;IACT,CAAC;IACD,eAAe;IACf,MAAM,QAAQ,CAAC;AACjB,CAAC;AAED,SAAS,iBAAiB,CACxB,MAAsD,EACtD,OAAU;IAEV,OAAO;QACL,OAAO,EAAE,MAAM,CAAC,MAAe;QAC/B,MAAM,EAAE,MAAe;QACvB,MAAM,EAAE,KAAK;QACb,MAAM,EAAE,IAAI,OAAO,EAAE;QACrB,OAAO,EAAE,OAAgB;QACzB,OAAO,EAAE,IAAI,OAAO,EAAE;KACvB,CAAC;AACJ,CAAC;AAYD,0EAA0E;AAC1E,EAAE;AACF,yEAAyE;AACzE,mEAAmE;AACnE,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,wEAAwE;AACxE,8DAA8D;AAC9D,sEAAsE;AACtE,sEAAsE;AACtE,4EAA4E;AAC5E,6EAA6E;AAC7E,6EAA6E;AAC7E,6EAA6E;AAC7E,0EAA0E;AAC1E,yDAAyD;AACzD,EAAE;AACF,0EAA0E;AAC1E,6EAA6E;AAC7E,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC;IAClC,UAAU;IACV,WAAW;IACX,SAAS;IACT,YAAY;IACZ,QAAQ;CACT,CAAC,CAAC;AAEH,uEAAuE;AACvE,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B,+EAA+E;AAC/E,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAI5B,2EAA2E;AAC3E,SAAS,cAAc,CAAC,IAAiB;IACvC,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACzD,OAAO,KAAK,EAAE,SAAS,KAAK,MAAM,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;QAChE,CAAC,CAAC,KAAK,CAAC,OAAO;QACf,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED,kFAAkF;AAClF,SAAS,YAAY,CAAC,KAAgB;IACpC,IAAI,KAAK,CAAC,SAAS,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,UAAU,CAAC,KAAK,CAAC;AAC3E,CAAC;AAED,SAAS,SAAS,CAAC,CAAU,EAAE,CAAU;IACvC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzB,wEAAwE;IACxE,sDAAsD;IACtD,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,KAAK,CAAC;IACzC,OAAO,CACL,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,CAC5E,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,KAAgB,EAAE,KAAc;IAChD,OAAO,KAAK,CAAC,SAAS,KAAK,MAAM;QAC/B,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,EAAE,IAAI;QACzD,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED,8EAA8E;AAC9E,SAAS,OAAO,CAAC,KAAgB,EAAE,KAAc;IAC/C,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IACpC,yEAAyE;IACzE,IAAI,KAAK,CAAC,SAAS,KAAK,MAAM;QAAE,OAAO,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;IACpE,IAAI,KAAK,CAAC,SAAS,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC/C,IACE,KAAK,CAAC,SAAS,KAAK,SAAS;QAC7B,KAAK,CAAC,OAAO,CAAC,QAAQ,KAAK,2BAA2B,EACtD,CAAC;QACD,MAAM,EAAE,GAAG,KAA2C,CAAC;QACvD,OAAO,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,WAAc,GAAG,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,kEAAkE;AAClE,SAAS,WAAW,CAAC,CAAU,EAAE,CAAU;IACzC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;QACvC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC;IACD,MAAM,CAAC,GAAG,CAAuC,CAAC;IAClD,MAAM,CAAC,GAAG,CAAuC,CAAC;IAClD,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,SAAS,eAAe,CAAC,OAAe;IACtC,OAAO,IAAI,YAAY,CAAC,mBAAmB,OAAO,EAAE,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC;AAC9E,CAAC;AAED,6DAA6D;AAC7D,SAAS,YAAY,CACnB,MAAc,EACd,UAAmB,EACnB,UAAuB;IAEvB,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACpC,MAAM,CAAC,MAAM,GAAG,EAAE,EAAE,SAAS,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1D,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAClC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,SAAS,KAAK,MAAM,CACnD,CAAC;QACF,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,eAAe,CAAC,4BAA4B,MAAM,GAAG,CAAC,CAAC;QAC/D,CAAC;QACD,MAAM,GAAG,GAAG,SAAS,EAAE,WAAW,EAAE,CAAC;QACrC,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,KAAK,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;YACzD,MAAM,eAAe,CAAC,+BAA+B,SAAS,GAAG,CAAC,CAAC;QACrE,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,MAAM,EAAE,CAAC;IAC1E,CAAC,CAAC,CAAC;AACL,CAAC;AAED,0EAA0E;AAC1E,SAAS,UAAU,CAAC,KAAgB,EAAE,CAAS;IAC7C,IAAI,KAAK,CAAC,SAAS,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IAC3C,QAAQ,KAAK,CAAC,MAAM,EAAE,CAAC;QACrB,KAAK,UAAU,CAAC,KAAK,CAAC;QACtB,KAAK,UAAU,CAAC,MAAM,CAAC;QACvB,KAAK,UAAU,CAAC,MAAM,CAAC;QACvB,KAAK,UAAU,CAAC,OAAO,CAAC;QACxB,KAAK,UAAU,CAAC,QAAQ;YACtB,OAAO,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACpD;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,SAAS,CAChB,IAAwB,EACxB,IAAiB,EACjB,KAAc,EACd,aAAsC;IAEtC,MAAM,GAAG,GAAG,CAAC,KAAK,IAAI,EAAE,CAAQ,CAAC;IACjC,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,OAAO,GAAG,IAAsB,CAAC;IAErC,IAAI,UAAU,IAAI,aAAa,EAAE,CAAC;QAChC,KAAK,MAAM,MAAM,IAAI,aAAa,CAAC,MAAM,EAAE,CAAC;YAC1C,IAAI,mBAAmB,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC;gBAAE,SAAS;YACxD,IAAI,MAAM,CAAC,QAAQ,KAAK,iBAAiB,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC;gBACnE,SAAS;YACX,CAAC;YACD,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YACnC,IAAI,IAAI,IAAI,IAAI;gBAAE,SAAS,CAAC,mBAAmB;YAC/C,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAClD,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;gBAAE,SAAS;YAC/C,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GACV,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACxE,IAAI,UAAU,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,CACzC,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,SAAS,KAAK,MAAM;YACtB,CAAC,CAAC,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,MAAM,CAAC,CAC/D,CAAC;QACF,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAC/B,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;YACpB,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;YAC/B,MAAM,IAAI,GAAG,CAAC,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YACjE,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACzE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,IAAI,UAAU,IAAI,OAAO,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,EAAE,GAAG,CAAC,UAAU,KAAK,IAAI,EAAE,UAAU,CAAC,CAAC;QACxE,OAAO,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;YACnC,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC;gBACnC,MAAM,CAAC,GAAG,WAAW,CACnB,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,EAClC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CACnC,CAAC;gBACF,IAAI,CAAC,KAAK,CAAC;oBAAE,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACpC,CAAC;YACD,OAAO,CAAC,CAAC;QACX,CAAC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC;IAE7B,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;IACrE,IAAI,KAAK,EAAE,CAAC;QACV,yEAAyE;QACzE,oDAAoD;QACpD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,MAAM,eAAe,CAAC,oBAAoB,CAAC,CAAC;QACtE,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC;IAC5C,MAAM,QAAQ,GACZ,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC;IACzE,MAAM,GAAG,GAAG,MAAM,GAAG,QAAQ,CAAC;IAE9B,MAAM,IAAI,GAAQ,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;IACpE,MAAM,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,UAAU,CAAC;IACxD,IAAI,UAAU;QAAE,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAChE,IAAI,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,aAAa,IAAI,CAAC,OAAO,IAAI,GAAG,GAAG,KAAK,EAAE,CAAC;QACvE,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,IAAqC,CAAC,CAAC;AAC5E,CAAC;AAED;;;;GAIG;AACH,SAAS,iBAAiB,CACxB,QAAyC;IAEzC,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEhD,KAAK,MAAM,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC9B,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC;QAC1B,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,OAAO,IAAI,GAAG,EAAE,CAAC;QAExD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAChB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;YACzB,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,CAChD,SAAS,CACP,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC,EACrC,IAAI,EACJ,KAAK,EACL,aAAa,CACd,CACF,CAAC;QACJ,CAAC;QAED,IAAI,MAAM,CAAC,GAAG,EAAE,CAAC;YACf,MAAM,EAAE,GAAG,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC;YACxD,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC5B,+DAA+D;gBAC/D,iEAAiE;gBACjE,2CAA2C;gBAC3C,MAAM,GAAG,GAAG,KAA4C,CAAC;gBACzD,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBACtD,IAAI,CAAC,KAAK,EAAE,CAAC;oBACX,8DAA8D;oBAC9D,4DAA4D;oBAC5D,wDAAwD;oBACxD,MAAM,IAAI,YAAY,CACpB,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,YAAY,EAChD,IAAI,CAAC,QAAQ,CACd,CAAC;gBACJ,CAAC;gBACD,OAAO,MAAM,CAAC,cAAc,EAAE;oBAC5B,CAAC,WAAW,CAAC,EAAE,KAAK;iBACY,CAAC,CAAC;YACtC,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;YACzC,MAAM,EAAE,GAAG,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC;YACzE,MAAM,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC;YACzC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC5B,oEAAoE;gBACpE,mEAAmE;gBACnE,gBAAgB;gBAChB,MAAM,GAAG,GAAG,CAAC,KAAK,IAAI,EAAE,CAA4B,CAAC;gBACrD,MAAM,IAAI,GAAG;oBACX,GAAI,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,GAAG,CAA6B;iBAC3D,CAAC;gBACF,OAAO,IAAI,CAAC,SAAS,CAAC;gBACtB,qEAAqE;gBACrE,qEAAqE;gBACrE,oEAAoE;gBACpE,MAAM,EAAE,GACN,OAAO,IAAI,CAAC,EAAE,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,EAAE,CAAC;oBACtC,CAAC,CAAE,IAAI,CAAC,EAAE,CAAY;oBACtB,CAAC,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;gBAC1B,MAAM,OAAO,GAAG,MAAM,CAAC,YAAY,EAAE;oBACnC,GAAG,IAAI;oBACP,CAAC,EAAE,CAAC,EAAE,EAAE;iBACwB,CAAC,CAAC;gBACpC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;gBAC1C,OAAO,MAAM,CAAC,cAAc,EAAE;oBAC5B,CAAC,WAAW,CAAC,EAAE,OAAO;iBACU,CAAC,CAAC;YACtC,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;YACzC,MAAM,EAAE,GAAG,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC;YACzE,MAAM,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC;YACzC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC5B,MAAM,GAAG,GAAG,CAAC,KAAK,IAAI,EAAE,CAA4B,CAAC;gBACrD,qEAAqE;gBACrE,mEAAmE;gBACnE,aAAa;gBACb,MAAM,KAAK,GAAG;oBACZ,GAAI,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,GAAG,CAA6B;iBAC3D,CAAC;gBACF,OAAO,KAAK,CAAC,SAAS,CAAC;gBACvB,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC9C,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;gBAC/B,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBAC/B,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACd,MAAM,IAAI,YAAY,CACpB,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,YAAY,EACjC,IAAI,CAAC,QAAQ,CACd,CAAC;gBACJ,CAAC;gBACD,MAAM,OAAO,GAAG,MAAM,CAAC,YAAY,EAAE;oBACnC,GAAI,QAAoC;oBACxC,GAAG,KAAK;oBACR,CAAC,EAAE,CAAC,EAAE,EAAE;iBACwB,CAAC,CAAC;gBACpC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;gBACvB,OAAO,MAAM,CAAC,cAAc,EAAE;oBAC5B,CAAC,WAAW,CAAC,EAAE,OAAO;iBACU,CAAC,CAAC;YACtC,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC1C,kDAAkD;gBAClD,MAAM,GAAG,GAAG,KAA4C,CAAC;gBACzD,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBAC3C,8DAA8D;gBAC9D,OAAO,EAAE,CAAC;YACZ,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAmC;IAEnC,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,EAAE,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IACtD,MAAM,WAAW,GAAG,QAAQ,CAAC,WAAW,KAAK,IAAI,IAAI,QAAQ,IAAI,IAAI,CAAC;IACtE,MAAM,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAE7C,oEAAoE;IACpE,2EAA2E;IAC3E,oEAAoE;IACpE,yEAAyE;IACzE,uDAAuD;IACvD,MAAM,SAAS,GAAc;QAC3B,4CAA4C;QAC5C,8DAA8D;QAC9D,uEAAuE;QACvE,kEAAkE;QAClE,oCAAoC;QACpC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa;YACjE,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAEvD,uBAAuB;YACvB,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;YACvC,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,KAAc,CAAC,CAAC;gBAC7C,OAAO,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAC3C,CAAC;YAED,iEAAiE;YACjE,iEAAiE;YACjE,0DAA0D;YAC1D,IAAI,WAAW,EAAE,CAAC;gBAChB,OAAO,QAAS,CAAC,KAAK,CACpB,MAAM,EACN,MAAM,EACN,SAAS,EACT,MAAM,EACN,KAAK,EACL,aAAa,CACd,CAAC;YACJ,CAAC;YAED,uEAAuE;YACvE,kEAAkE;YAClE,4DAA4D;YAC5D,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAClC,IAAI,OAAO,EAAE,CAAC;gBACZ,qEAAqE;gBACrE,iDAAiD;gBACjD,MAAM,aAAa,GAAI,MAAkC,CAAC,KAAK,CAAC;gBAChE,OAAO,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC;YAClE,CAAC;YAED,gEAAgE;YAChE,OAAO,OAAO,CAAC,MAAM,CACnB,IAAI,YAAY,CACd,6DAA6D,GAAG,EAAE,EAClE,IAAI,CAAC,aAAa,CACnB,CACF,CAAC;QACJ,CAAC;QAED,qEAAqE;QACrE,yEAAyE;QACzE,mEAAmE;QACnE,yEAAyE;QACzE,uEAAuE;QACvE,yCAAyC;QACzC,EAAE;QACF,kEAAkE;QAClE,oEAAoE;QACpE,+DAA+D;QAC/D,qEAAqE;QACrE,0EAA0E;QAC1E,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa;YAClE,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YACvD,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;YACvC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,gEAAgE;gBAChE,yCAAyC;gBACzC,IAAI,WAAW,EAAE,CAAC;oBAChB,OAAO,QAAS,CAAC,MAAM,CACrB,MAAM,EACN,MAAM,EACN,SAAS,EACT,MAAM,EACN,KAAK,EACL,aAAa,CACd,CAAC;gBACJ,CAAC;gBACD,OAAO,OAAO,CAAC,MAAM,CACnB,IAAI,YAAY,CACd,yDAAyD,GAAG,EAAE,EAC9D,IAAI,CAAC,aAAa,CACnB,CACF,CAAC;YACJ,CAAC;YACD,wEAAwE;YACxE,gEAAgE;YAChE,uDAAuD;YACvD,MAAM,eAAe,GAAc,EAAE,CAAC;YACtC,IAAI,CAAC;gBACH,IAAI,KAAK,EAAE,MAAM,CAAC,IAAI,KAA+B,EAAE,CAAC;oBACtD,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,iEAAiE;gBACjE,oDAAoD;YACtD,CAAC;YACD,MAAM,YAAY,GAChB,eAAe,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC;YACrE,MAAM,MAAM,GAAG,OAAO,CAAC,YAAqB,CAAC,CAAC;YAC9C,OAAO;gBACL,OAAO,EAAE,MAAM,CAAC,MAAe;gBAC/B,MAAM,EAAE,MAAe;gBACvB,MAAM,EAAE,IAAI;gBACZ,MAAM,EAAE,IAAI,OAAO,EAAE;gBACrB,OAAO,EAAE,eAAe,CAAC,MAAM,CAAyB;gBACxD,OAAO,EAAE,IAAI,OAAO,EAAE;aACvB,CAAC;QACJ,CAAC;KACF,CAAC;IACF,OAAO,SAAS,CAAC;AACnB,CAAC","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// The mock transport ENGINE. A generated frontend used to carry this entire\n// pipeline as ~300 lines of Tier-1 scaffold; what varies per project is only\n// the table of entities to dispatch on, so the engine lives here and the\n// project keeps a declarative descriptor list.\n//\n// Dispatch order (unchanged from the scaffolded original):\n//\n// 1. A scenario selected via `?scenario=<name>` (or the auto-generated\n// `default` scenario) gets first crack at every unary RPC. Handlers\n// keyed by `${serviceTypeName}/${methodName}` return typed proto\n// messages and short-circuit the rest of the pipeline.\n// 2. If the active scenario has `passthrough: true` AND a fallback\n// transport was supplied (hybrid mode — VITE_MOCK_API=hybrid /\n// NEXT_PUBLIC_MOCK_API=hybrid), unmatched RPCs are forwarded to the\n// real backend. This lets one scenario stub a single endpoint while\n// everything else exercises the live server.\n// 3. Otherwise RPCs not overridden fall through to the deterministic\n// per-entity fixtures described by the entity descriptors.\n// 4. Anything else rejects with Code.Unimplemented so callers see a\n// clear error instead of a silent empty response.\n//\n// A fixture List answers the way the backend's generated CRUD List does\n// (pkg/crud HandleList), so a filtered table, a paged table and a \"how many\"\n// tile read the same against fixtures as against a server — see\n// `serveList` for the exact contract. Custom (non-CRUD) RPCs are not\n// interpreted at all; they stay hand-written scenario handlers.\n//\n// Imported through the \"@reliantlabs/forge-web-runtime/mock-transport\" subpath,\n// never the barrel: a production bundle must be able to shake the fixtures\n// and this engine out entirely, which is why the generated `connect.ts`\n// reaches its own shim through a dynamic `require()` gated on the mock-mode\n// env var.\nimport {\n create,\n ScalarType,\n type DescField,\n type DescMessage,\n type MessageInitShape,\n} from \"@bufbuild/protobuf\";\nimport {\n Code,\n ConnectError,\n type Transport,\n type UnaryResponse,\n} from \"@connectrpc/connect\";\n\n/**\n * The shape the generated scenario files satisfy. Structural on purpose: the\n * project's own `Scenario` type (src/mocks/scenario-types.ts) carries a\n * richer, per-RPC-typed `handlers` map, and this is the subset the engine\n * dispatches through.\n */\nexport interface MockScenario {\n name: string;\n /** Per-RPC overrides keyed by `${serviceTypeName}/${methodName}`. */\n handlers: Record<string, ((req: never) => unknown) | undefined>;\n /** Forward unmatched RPCs to the real backend (hybrid mode only). */\n passthrough?: boolean;\n /** Non-RPC side effects, run once before the first RPC fires. */\n setup?: () => void;\n}\n\n/** The generated `src/mocks/scenarios` barrel, as the engine consumes it. */\nexport interface MockScenarioRegistry {\n byName(name: string): MockScenario | undefined;\n defaultScenario: MockScenario;\n}\n\n/**\n * One CRUD arm of an entity's dispatch table.\n *\n * The List arm needs nothing beyond these three fields to honour filters,\n * paging and total_count: the REQUEST schema arrives on every call as the\n * Connect method's `input`, and the entity schema is the element type of\n * `itemsField` on `responseSchema`.\n */\nexport interface MockListRpc {\n rpc: string;\n responseSchema: DescMessage;\n /**\n * The camelCase name of the list response's repeated field — the ACTUAL\n * proto field (e.g. `keys`), not the camelCased entity plural.\n */\n itemsField: string;\n}\n\nexport interface MockGetRpc {\n rpc: string;\n responseSchema: DescMessage;\n /** The camelCase field wrapping the entity on the response message. */\n entityField: string;\n}\n\nexport interface MockWriteRpc extends MockGetRpc {\n /**\n * The camelCase field wrapping the entity on the REQUEST message. AIP-134\n * requests nest the entity ({ item: {...}, updateMask }); the generated\n * forms submit it flattened. Both are accepted.\n */\n requestField: string;\n}\n\nexport interface MockDeleteRpc {\n rpc: string;\n}\n\n/**\n * Everything the engine needs to serve one entity's CRUD quintet from\n * fixtures. Generated into `src/lib/mock-transport.ts` from the project's\n * proto descriptors.\n */\nexport interface MockEntityDescriptor {\n /** Fully-qualified proto service name, e.g. \"demo.v1.ClinicService\". */\n service: string;\n /** Lowercase entity name, used verbatim in the NotFound message. */\n label: string;\n /**\n * The camelCase PRIMARY-KEY field. The session store keys records by this\n * field; hardcoding \"id\" breaks every surrogate-PK entity (a\n * `usage_event_id` PK projects to a message with no `id` field).\n */\n pkField: string;\n /** Deterministic seed rows — the same values the dev database is seeded with. */\n fixtures: readonly unknown[];\n /** The entity message schema. Required by the create/update write paths. */\n entitySchema?: DescMessage;\n list?: MockListRpc;\n get?: MockGetRpc;\n create?: MockWriteRpc;\n update?: MockWriteRpc;\n delete?: MockDeleteRpc;\n}\n\nexport interface CreateMockTransportOptions {\n /** The resolved scenario — see {@link resolveActiveScenario}. */\n scenario: MockScenario;\n /** Per-entity fixture dispatch. Omit for a scenario-only project. */\n entities?: readonly MockEntityDescriptor[];\n /**\n * Optional real Connect transport. When the active scenario declares\n * `passthrough: true`, any RPC not matched by a scenario handler is\n * forwarded here instead of falling through to the fixtures.\n */\n fallback?: Transport;\n}\n\n/**\n * Mutable session stores, keyed by descriptor identity.\n *\n * Create/Update/Delete round-trip within one browser session (a created row\n * appears in the next List; a deleted one disappears) and reset to the\n * deterministic fixtures on reload. The scaffolded original got that from\n * module-scope `const store = new Map(...)`; keying off the descriptor\n * object preserves it — a module-level descriptor array hands back the same\n * store on every call, so building the transport twice (hybrid mode, a test\n * swapping transports) does not silently fork the data.\n */\nconst stores = new WeakMap<MockEntityDescriptor, Map<string, unknown>>();\n\nfunction storeFor(entity: MockEntityDescriptor): Map<string, unknown> {\n let store = stores.get(entity);\n if (!store) {\n store = new Map(\n entity.fixtures.map((row) => [\n String((row as Record<string, unknown>)[entity.pkField]),\n row,\n ]),\n );\n stores.set(entity, store);\n }\n return store;\n}\n\n/** Scenarios whose `setup()` has already run. */\nconst initialized = new WeakSet<MockScenario>();\n\n/**\n * Resolve the scenario the app should run under, and run its `setup()` once.\n *\n * Call this at MODULE SCOPE (the generated `src/lib/mock-transport.ts` does).\n * Reading the URL once — rather than on every RPC — means navigating away\n * from `?scenario=` keeps the scenario active until a full reload, matching\n * the agent-driven flow where the URL is the single source of truth.\n *\n * `setup()` is for non-RPC state (localStorage flags, sessionStorage). It is\n * synchronous and makes no network calls.\n */\nexport function resolveActiveScenario(\n registry: MockScenarioRegistry,\n): MockScenario {\n const requested =\n typeof globalThis !== \"undefined\" && globalThis.location\n ? new URLSearchParams(globalThis.location.search).get(\"scenario\")\n : null;\n // A ternary, not `&&`: with `??` alone the empty-string falsy branch leaks\n // '' into the inferred union, and '' survives `??` (which only replaces\n // null/undefined), so downstream access on .setup / .handlers would fail\n // under strict tsc.\n const active =\n (requested ? registry.byName(requested) : undefined) ??\n registry.defaultScenario;\n\n if (!initialized.has(active)) {\n initialized.add(active);\n active.setup?.();\n }\n return active;\n}\n\n/**\n * Normalize whatever a streaming scenario handler returns into an\n * AsyncIterable<unknown>. Accepts arrays, iterables, async iterables, and\n * promises that resolve to any of the above. Single (non-iterable) values\n * are wrapped as a one-element stream — handy for handlers that conceptually\n * yield once.\n */\nexport async function* toAsyncIterable(value: unknown): AsyncIterable<unknown> {\n const resolved = await Promise.resolve(value);\n if (resolved == null) return;\n // AsyncIterable\n if (\n typeof (resolved as { [Symbol.asyncIterator]?: unknown })[\n Symbol.asyncIterator\n ] === \"function\"\n ) {\n for await (const m of resolved as AsyncIterable<unknown>) yield m;\n return;\n }\n // Iterable (including arrays)\n if (\n typeof (resolved as { [Symbol.iterator]?: unknown })[Symbol.iterator] ===\n \"function\"\n ) {\n for (const m of resolved as Iterable<unknown>) yield m;\n return;\n }\n // Single value\n yield resolved;\n}\n\nfunction makeUnaryResponse<T>(\n method: { name: string; parent: { typeName: string } },\n message: T,\n): UnaryResponse<never, never> {\n return {\n service: method.parent as never,\n method: method as never,\n stream: false,\n header: new Headers(),\n message: message as never,\n trailer: new Headers(),\n };\n}\n\n/**\n * A fixture-backed handler for one RPC key. `requestSchema` is the Connect\n * method's input descriptor — absent only when a caller hand-builds a method\n * object without one, in which case List serves paging alone.\n */\ntype FixtureHandler = (\n input: unknown,\n requestSchema: DescMessage | undefined,\n) => unknown;\n\n// ── List semantics ─────────────────────────────────────────────────────\n//\n// What the backend's generated List does, and therefore what the fixture\n// List does (pkg/crud HandleList + the generated Filters closure):\n//\n// - Every `optional` request field that is SET filters by equality on the\n// entity field of the same name. Enums compare by value. A field with\n// IMPLICIT presence (no `optional`) is never a filter — the backend\n// generates none for it, so the mock must not invent one.\n// - `search` matches case-insensitively against the entity's string\n// fields and enum value names (enum columns are stored as names).\n// - `order_by` (snake_case column, comma-separated, optional ASC/DESC per\n// column) plus `descending` sorts; an unknown column is InvalidArgument.\n// - `page_size` defaults to 50 and is clamped to 100. `total_count` is the\n// filtered count BEFORE paging. `next_page_token` is minted only for the\n// default (primary-key) order — an ordered list is single-page on the\n// backend, and a mock that paged it would hide that.\n//\n// Paging, ordering and search arrive through these request fields, by the\n// names forge's CRUD List requests declare. They are never equality filters.\nconst LIST_CONTROL_FIELDS = new Set([\n \"pageSize\",\n \"pageToken\",\n \"orderBy\",\n \"descending\",\n \"search\",\n]);\n\n/** pkg/crud ListOp's defaults: 0 means 50, and no page exceeds 100. */\nconst DEFAULT_PAGE_SIZE = 50;\nconst MAX_PAGE_SIZE = 100;\n\n/** FeatureSet_FieldPresence.EXPLICIT — proto3 `optional`, oneofs, messages. */\nconst EXPLICIT_PRESENCE = 1;\n\ntype Row = Record<string, unknown>;\n\n/** The entity message a List response repeats, read off its descriptor. */\nfunction listItemSchema(list: MockListRpc): DescMessage | undefined {\n const items = list.responseSchema.field[list.itemsField];\n return items?.fieldKind === \"list\" && items.listKind === \"message\"\n ? items.message\n : undefined;\n}\n\n/** Scalars and enums compare by value; messages, bytes, lists and maps do not. */\nfunction isComparable(field: DescField): boolean {\n if (field.fieldKind === \"enum\") return true;\n return field.fieldKind === \"scalar\" && field.scalar !== ScalarType.BYTES;\n}\n\nfunction sameValue(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n // A 64-bit field is a bigint — or a string under jstype=JS_STRING — and\n // the two sides are not guaranteed to agree on which.\n if (a == null || b == null) return false;\n return (\n (typeof a === \"bigint\" || typeof b === \"bigint\") && String(a) === String(b)\n );\n}\n\nfunction enumName(field: DescField, value: unknown): string | undefined {\n return field.fieldKind === \"enum\"\n ? field.enum.values.find((v) => v.number === value)?.name\n : undefined;\n}\n\n/** The value a column sorts by, as Postgres would order the stored column. */\nfunction sortKey(field: DescField, value: unknown): unknown {\n if (value == null) return undefined;\n // Enum columns hold the value NAME, so they sort alphabetically by name.\n if (field.fieldKind === \"enum\") return enumName(field, value) ?? \"\";\n if (field.fieldKind === \"scalar\") return value;\n if (\n field.fieldKind === \"message\" &&\n field.message.typeName === \"google.protobuf.Timestamp\"\n ) {\n const ts = value as { seconds: bigint; nanos: number };\n return BigInt(ts.seconds) * 1_000_000_000n + BigInt(ts.nanos);\n }\n return undefined;\n}\n\n/** Postgres's default: NULL sorts after every value ascending. */\nfunction compareKeys(a: unknown, b: unknown): number {\n if (a === undefined || b === undefined) {\n return a === b ? 0 : a === undefined ? 1 : -1;\n }\n const x = a as string | number | bigint | boolean;\n const y = b as string | number | bigint | boolean;\n return x < y ? -1 : x > y ? 1 : 0;\n}\n\nfunction invalidArgument(message: string): ConnectError {\n return new ConnectError(`mock-transport: ${message}`, Code.InvalidArgument);\n}\n\n/** Parse `order_by` the way orm.ValidateOrderBy reads it. */\nfunction parseOrderBy(\n clause: string,\n descending: boolean,\n itemSchema: DescMessage,\n): { field: DescField; desc: boolean }[] {\n return clause.split(\",\").map((part) => {\n const [column = \"\", direction] = part.trim().split(/\\s+/);\n const field = itemSchema.fields.find(\n (f) => f.name === column || f.localName === column,\n );\n if (!field) {\n throw invalidArgument(`unknown order-by column \"${column}\"`);\n }\n const dir = direction?.toUpperCase();\n if (dir !== undefined && dir !== \"ASC\" && dir !== \"DESC\") {\n throw invalidArgument(`invalid order-by direction \"${direction}\"`);\n }\n return { field, desc: dir === undefined ? descending : dir === \"DESC\" };\n });\n}\n\n/** A count in whatever JS type the response's total_count field takes. */\nfunction countValue(field: DescField, n: number): unknown {\n if (field.fieldKind !== \"scalar\") return n;\n switch (field.scalar) {\n case ScalarType.INT64:\n case ScalarType.UINT64:\n case ScalarType.SINT64:\n case ScalarType.FIXED64:\n case ScalarType.SFIXED64:\n return field.longAsString ? String(n) : BigInt(n);\n default:\n return n;\n }\n}\n\n/**\n * Answer one List call from the entity's rows. See the List semantics note\n * above for the contract; the engine's tests pin each clause.\n */\nfunction serveList(\n rows: readonly unknown[],\n list: MockListRpc,\n input: unknown,\n requestSchema: DescMessage | undefined,\n): unknown {\n const req = (input ?? {}) as Row;\n const itemSchema = listItemSchema(list);\n let matched = rows as readonly Row[];\n\n if (itemSchema && requestSchema) {\n for (const filter of requestSchema.fields) {\n if (LIST_CONTROL_FIELDS.has(filter.localName)) continue;\n if (filter.presence !== EXPLICIT_PRESENCE || !isComparable(filter)) {\n continue;\n }\n const want = req[filter.localName];\n if (want == null) continue; // unset: no filter\n const target = itemSchema.field[filter.localName];\n if (!target || !isComparable(target)) continue;\n matched = matched.filter((row) => sameValue(row[filter.localName], want));\n }\n }\n\n const search =\n typeof req.search === \"string\" ? req.search.trim().toLowerCase() : \"\";\n if (itemSchema && search) {\n const searchable = itemSchema.fields.filter(\n (f) =>\n f.fieldKind === \"enum\" ||\n (f.fieldKind === \"scalar\" && f.scalar === ScalarType.STRING),\n );\n matched = matched.filter((row) =>\n searchable.some((f) => {\n const value = row[f.localName];\n const text = f.fieldKind === \"enum\" ? enumName(f, value) : value;\n return typeof text === \"string\" && text.toLowerCase().includes(search);\n }),\n );\n }\n\n const orderBy = typeof req.orderBy === \"string\" ? req.orderBy.trim() : \"\";\n if (itemSchema && orderBy) {\n const keys = parseOrderBy(orderBy, req.descending === true, itemSchema);\n matched = [...matched].sort((a, b) => {\n for (const { field, desc } of keys) {\n const c = compareKeys(\n sortKey(field, a[field.localName]),\n sortKey(field, b[field.localName]),\n );\n if (c !== 0) return desc ? -c : c;\n }\n return 0;\n });\n }\n\n const total = matched.length;\n\n let offset = 0;\n const token = typeof req.pageToken === \"string\" ? req.pageToken : \"\";\n if (token) {\n // The mock's own cursor: an offset into the filtered rows. Opaque to the\n // client, exactly like the backend's keyset cursor.\n if (!/^\\d+$/.test(token)) throw invalidArgument(\"invalid page token\");\n offset = Number(token);\n }\n const requested = Number(req.pageSize ?? 0);\n const pageSize =\n requested > 0 ? Math.min(requested, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE;\n const end = offset + pageSize;\n\n const init: Row = { [list.itemsField]: matched.slice(offset, end) };\n const totalField = list.responseSchema.field.totalCount;\n if (totalField) init.totalCount = countValue(totalField, total);\n if (list.responseSchema.field.nextPageToken && !orderBy && end < total) {\n init.nextPageToken = String(end);\n }\n return create(list.responseSchema, init as MessageInitShape<DescMessage>);\n}\n\n/**\n * Compile the entity descriptors into a flat `${service}/${rpc}` → handler\n * map. Building it once per transport keeps per-RPC dispatch to a single\n * lookup, exactly like the generated `switch` it replaces.\n */\nfunction buildFixtureTable(\n entities: readonly MockEntityDescriptor[],\n): Map<string, FixtureHandler> {\n const table = new Map<string, FixtureHandler>();\n\n for (const entity of entities) {\n const pk = entity.pkField;\n const key = (rpc: string) => `${entity.service}/${rpc}`;\n\n if (entity.list) {\n const list = entity.list;\n table.set(key(list.rpc), (input, requestSchema) =>\n serveList(\n Array.from(storeFor(entity).values()),\n list,\n input,\n requestSchema,\n ),\n );\n }\n\n if (entity.get) {\n const { rpc, responseSchema, entityField } = entity.get;\n table.set(key(rpc), (input) => {\n // Read the key off the SAME field the store is keyed by, not a\n // hardcoded `id` — a surrogate-PK entity would otherwise look up\n // String(undefined) and miss every record.\n const req = input as Record<string, unknown> | undefined;\n const found = storeFor(entity).get(String(req?.[pk]));\n if (!found) {\n // A miss is NotFound — exactly what the real backend returns.\n // (Falling back to the first fixture made every detail page\n // \"work\" against the wrong record and hid bad-id bugs.)\n throw new ConnectError(\n `${entity.label} ${String(req?.[pk])} not found`,\n Code.NotFound,\n );\n }\n return create(responseSchema, {\n [entityField]: found,\n } as MessageInitShape<DescMessage>);\n });\n }\n\n if (entity.create && entity.entitySchema) {\n const { rpc, responseSchema, entityField, requestField } = entity.create;\n const entitySchema = entity.entitySchema;\n table.set(key(rpc), (input) => {\n // Create requests carry the entity fields flattened (that's how the\n // generated form submits); a nested { <entity>: {...} } payload is\n // accepted too.\n const req = (input ?? {}) as Record<string, unknown>;\n const init = {\n ...((req[requestField] ?? req) as Record<string, unknown>),\n };\n delete init.$typeName;\n // Mint the primary key on the PK field the store is keyed by — for a\n // surrogate-PK entity this is NOT `id`, and writing a generated `id`\n // would leave the store key undefined so the row never round-trips.\n const id =\n typeof init[pk] === \"string\" && init[pk]\n ? (init[pk] as string)\n : crypto.randomUUID();\n const created = create(entitySchema, {\n ...init,\n [pk]: id,\n } as MessageInitShape<DescMessage>);\n storeFor(entity).set(String(id), created);\n return create(responseSchema, {\n [entityField]: created,\n } as MessageInitShape<DescMessage>);\n });\n }\n\n if (entity.update && entity.entitySchema) {\n const { rpc, responseSchema, entityField, requestField } = entity.update;\n const entitySchema = entity.entitySchema;\n table.set(key(rpc), (input) => {\n const req = (input ?? {}) as Record<string, unknown>;\n // AIP-134 requests wrap the entity ({ <entity>: {...}, updateMask })\n // — the id lives inside the wrapper; flat requests carry it at the\n // top level.\n const patch = {\n ...((req[requestField] ?? req) as Record<string, unknown>),\n };\n delete patch.$typeName;\n const id = String(patch[pk] ?? req[pk] ?? \"\");\n const store = storeFor(entity);\n const existing = store.get(id);\n if (!existing) {\n throw new ConnectError(\n `${entity.label} ${id} not found`,\n Code.NotFound,\n );\n }\n const updated = create(entitySchema, {\n ...(existing as Record<string, unknown>),\n ...patch,\n [pk]: id,\n } as MessageInitShape<DescMessage>);\n store.set(id, updated);\n return create(responseSchema, {\n [entityField]: updated,\n } as MessageInitShape<DescMessage>);\n });\n }\n\n if (entity.delete) {\n table.set(key(entity.delete.rpc), (input) => {\n // Delete by the SAME field the store is keyed by.\n const req = input as Record<string, unknown> | undefined;\n storeFor(entity).delete(String(req?.[pk]));\n // Delete returns an empty response — the standard CRUD shape.\n return {};\n });\n }\n }\n\n return table;\n}\n\n/**\n * Create the mock transport.\n *\n * The generated `src/lib/mock-transport.ts` supplies the resolved scenario\n * and the project's entity descriptors; `connect.ts` supplies the optional\n * real transport when running in hybrid mode.\n */\nexport function createMockTransport(\n options: CreateMockTransportOptions,\n): Transport {\n const { scenario, entities = [], fallback } = options;\n const passthrough = scenario.passthrough === true && fallback != null;\n const fixtures = buildFixtureTable(entities);\n\n // Bind the object literal to a Transport-typed variable rather than\n // casting at the return site. A trailing assertion cast does not propagate\n // Connect's Transport signature backwards into the literal's method\n // bodies, so under `strict` tsc every callback parameter on unary/stream\n // errors with TS7006 (\"implicitly has an 'any' type\").\n const transport: Transport = {\n // Connect v2's Transport.unary signature is\n // (method, signal, timeoutMs, header, input, contextValues)\n // — method first, no separate service arg. `method.parent.typeName` is\n // the fully-qualified service name; that is what handler keys and\n // fixture keys are matched against.\n async unary(method, signal, timeoutMs, header, input, contextValues) {\n const key = `${method.parent.typeName}/${method.name}`;\n\n // 1) Scenario overlay.\n const handler = scenario.handlers[key];\n if (handler) {\n const result = await handler(input as never);\n return makeUnaryResponse(method, result);\n }\n\n // 2) Passthrough: route to the real backend instead of fixtures.\n // Skips fixtures entirely — in hybrid mode the whole point is\n // \"use real data for anything not explicitly stubbed\".\n if (passthrough) {\n return fallback!.unary(\n method,\n signal,\n timeoutMs,\n header,\n input,\n contextValues,\n );\n }\n\n // 3) Base fixture dispatch, backed by the mutable per-entity stores so\n // writes round-trip within the session. Misses are REAL errors\n // (Code.NotFound), never a silent wrong-record fallback.\n const fixture = fixtures.get(key);\n if (fixture) {\n // `method.input` is typed as always present, but a hand-built method\n // object (a test, a custom harness) may omit it.\n const requestSchema = (method as { input?: DescMessage }).input;\n return makeUnaryResponse(method, fixture(input, requestSchema));\n }\n\n // 4) Nothing matched. Surface a clear error instead of hanging.\n return Promise.reject(\n new ConnectError(\n `mock-transport: no scenario handler or entity fixture for ${key}`,\n Code.Unimplemented,\n ),\n );\n },\n\n // Streaming: a scenario handler can return an array, iterable, async\n // iterable, or a Promise resolving to any of those. The transport adapts\n // it into the AsyncIterable<Response> shape Connect expects. If no\n // handler matches, reject with Unimplemented — base-fixture streaming is\n // intentionally not served (there is no canonical \"list of N messages\"\n // shape for an arbitrary streaming RPC).\n //\n // No explicit return-type annotation: the outer `const transport:\n // Transport` binding lets tsc infer the per-callback signature from\n // Connect's generic Transport.stream<I, O>. Annotating this as\n // Promise<StreamResponse<never, never>> makes the passthrough branch\n // ill-typed (TS2322) because fallback.stream returns StreamResponse<I,O>.\n async stream(method, signal, timeoutMs, header, input, contextValues) {\n const key = `${method.parent.typeName}/${method.name}`;\n const handler = scenario.handlers[key];\n if (!handler) {\n // Passthrough mirrors the unary path: in hybrid mode, unmatched\n // streaming RPCs go to the real backend.\n if (passthrough) {\n return fallback!.stream(\n method,\n signal,\n timeoutMs,\n header,\n input,\n contextValues,\n );\n }\n return Promise.reject(\n new ConnectError(\n `mock-transport: no scenario handler for streaming RPC ${key}`,\n Code.Unimplemented,\n ),\n );\n }\n // Read the request stream eagerly into an array so the scenario handler\n // sees the full client-streaming payload. For server-stream and\n // unary-into-stream-out RPCs this is a single message.\n const requestMessages: unknown[] = [];\n try {\n for await (const m of input as AsyncIterable<unknown>) {\n requestMessages.push(m);\n }\n } catch {\n // Synthetic clients sometimes pass a single value rather than an\n // iterable; treat that as an empty request payload.\n }\n const handlerInput =\n requestMessages.length <= 1 ? requestMessages[0] : requestMessages;\n const result = handler(handlerInput as never);\n return {\n service: method.parent as never,\n method: method as never,\n stream: true,\n header: new Headers(),\n message: toAsyncIterable(result) as AsyncIterable<never>,\n trailer: new Headers(),\n };\n },\n };\n return transport;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"mock-transport.js","sourceRoot":"","sources":["../src/mock-transport.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// The mock transport ENGINE. A generated frontend used to carry this entire\n// pipeline as ~300 lines of Tier-1 scaffold; what varies per project is only\n// the table of entities to dispatch on, so the engine lives here and the\n// project keeps a declarative descriptor list.\n//\n// Dispatch order (unchanged from the scaffolded original):\n//\n// 1. A scenario selected via `?scenario=<name>` (or the auto-generated\n// `default` scenario) gets first crack at every unary RPC. Handlers\n// keyed by `${serviceTypeName}/${methodName}` return typed proto\n// messages and short-circuit the rest of the pipeline.\n// 2. If the active scenario has `passthrough: true` AND a fallback\n// transport was supplied (hybrid mode — VITE_MOCK_API=hybrid /\n// NEXT_PUBLIC_MOCK_API=hybrid), unmatched RPCs are forwarded to the\n// real backend. This lets one scenario stub a single endpoint while\n// everything else exercises the live server.\n//\n// `passthrough: \"first\"` turns that around: EVERY RPC goes to the\n// backend first, and the scenario (then the fixtures) answer only when\n// it fails or exceeds `passthroughTimeoutMs`. That is a demo or a\n// preview on live data that degrades to sample data instead of to an\n// error page — and `onPassthroughFallback` lets the UI say so.\n//\n// 0. Before any of it, `readOnly` refuses every RPC that is not a read\n// (by default: not marked `idempotency_level = NO_SIDE_EFFECTS`) with\n// PermissionDenied, so neither a handler, a fixture nor the backend\n// ever sees a write — a public demo's guarantee.\n// 3. Otherwise RPCs not overridden fall through to the deterministic\n// per-entity fixtures described by the entity descriptors.\n// 4. Anything else rejects with Code.Unimplemented so callers see a\n// clear error instead of a silent empty response.\n//\n// A fixture List answers the way the backend's generated CRUD List does\n// (pkg/crud HandleList), so a filtered table, a paged table and a \"how many\"\n// tile read the same against fixtures as against a server — see\n// `serveList` for the exact contract. Custom (non-CRUD) RPCs are not\n// interpreted at all; they stay hand-written scenario handlers.\n//\n// Imported through the \"@reliantlabs/forge-web-runtime/mock-transport\" subpath,\n// never the barrel: a production bundle must be able to shake the fixtures\n// and this engine out entirely, which is why the generated `connect.ts`\n// reaches its own shim through a dynamic `require()` gated on the mock-mode\n// env var.\nimport {\n create,\n isMessage,\n ScalarType,\n type DescField,\n type DescMessage,\n type DescMethod,\n type MessageInitShape,\n} from \"@bufbuild/protobuf\";\nimport { MethodOptions_IdempotencyLevel } from \"@bufbuild/protobuf/wkt\";\nimport {\n Code,\n ConnectError,\n type Transport,\n type UnaryResponse,\n} from \"@connectrpc/connect\";\n\n/**\n * The shape the generated scenario files satisfy. Structural on purpose: the\n * project's own `Scenario` type (src/mocks/scenario-types.ts) carries a\n * richer, per-RPC-typed `handlers` map, and this is the subset the engine\n * dispatches through.\n */\nexport interface MockScenario {\n name: string;\n /** Per-RPC overrides keyed by `${serviceTypeName}/${methodName}`. */\n handlers: Record<string, ((req: never) => unknown) | undefined>;\n /**\n * `true`: forward RPCs no handler matches to the real backend (hybrid mode\n * only). `\"first\"`: forward EVERY RPC, and answer from the handlers and\n * fixtures only when the backend fails — see the dispatch notes above.\n */\n passthrough?: boolean | \"first\";\n /** Non-RPC side effects, run once before the first RPC fires. */\n setup?: () => void;\n}\n\n/** The generated `src/mocks/scenarios` barrel, as the engine consumes it. */\nexport interface MockScenarioRegistry {\n byName(name: string): MockScenario | undefined;\n defaultScenario: MockScenario;\n}\n\n/**\n * One CRUD arm of an entity's dispatch table.\n *\n * The List arm needs nothing beyond these three fields to honour filters,\n * paging and total_count: the REQUEST schema arrives on every call as the\n * Connect method's `input`, and the entity schema is the element type of\n * `itemsField` on `responseSchema`.\n */\nexport interface MockListRpc {\n rpc: string;\n responseSchema: DescMessage;\n /**\n * The camelCase name of the list response's repeated field — the ACTUAL\n * proto field (e.g. `keys`), not the camelCased entity plural.\n */\n itemsField: string;\n}\n\nexport interface MockGetRpc {\n rpc: string;\n responseSchema: DescMessage;\n /** The camelCase field wrapping the entity on the response message. */\n entityField: string;\n}\n\nexport interface MockWriteRpc extends MockGetRpc {\n /**\n * The camelCase field wrapping the entity on the REQUEST message. AIP-134\n * requests nest the entity ({ item: {...}, updateMask }); the generated\n * forms submit it flattened. Both are accepted.\n */\n requestField: string;\n}\n\nexport interface MockDeleteRpc {\n rpc: string;\n}\n\n/**\n * Everything the engine needs to serve one entity's CRUD quintet from\n * fixtures. Generated into `src/lib/mock-transport.ts` from the project's\n * proto descriptors.\n */\nexport interface MockEntityDescriptor {\n /** Fully-qualified proto service name, e.g. \"demo.v1.ClinicService\". */\n service: string;\n /** Lowercase entity name, used verbatim in the NotFound message. */\n label: string;\n /**\n * The camelCase PRIMARY-KEY field. The session store keys records by this\n * field; hardcoding \"id\" breaks every surrogate-PK entity (a\n * `usage_event_id` PK projects to a message with no `id` field).\n */\n pkField: string;\n /** Deterministic seed rows — the same values the dev database is seeded with. */\n fixtures: readonly unknown[];\n /** The entity message schema. Required by the create/update write paths. */\n entitySchema?: DescMessage;\n list?: MockListRpc;\n get?: MockGetRpc;\n create?: MockWriteRpc;\n update?: MockWriteRpc;\n delete?: MockDeleteRpc;\n}\n\nexport interface CreateMockTransportOptions {\n /** The resolved scenario — see {@link resolveActiveScenario}. */\n scenario: MockScenario;\n /** Per-entity fixture dispatch. Omit for a scenario-only project. */\n entities?: readonly MockEntityDescriptor[];\n /**\n * Optional real Connect transport. When the active scenario declares\n * `passthrough: true`, any RPC not matched by a scenario handler is\n * forwarded here instead of falling through to the fixtures.\n */\n fallback?: Transport;\n /**\n * Refuse every RPC that is not a read, with PermissionDenied, before any\n * handler, fixture or backend sees it. `true` classifies by the proto's\n * `idempotency_level = NO_SIDE_EFFECTS`; a function classifies itself (a\n * contract that does not annotate idempotency, e.g. by method name).\n */\n readOnly?: boolean | ((method: DescMethod) => boolean);\n /** The message a refused write carries. Default names the RPC. */\n readOnlyMessage?: string;\n /**\n * With `passthrough: \"first\"`: the forwarded call's deadline in ms, after\n * which the scenario answers instead. Default: the caller's own timeout.\n * Enforced by the mock transport itself, so a fallback transport that\n * ignores its timeout cannot hold the call open; a server stream must END\n * within it to count as the backend's answer.\n */\n passthroughTimeoutMs?: number;\n /**\n * With `passthrough: \"first\"`: called each time the backend failed (or\n * timed out) and the mock answered instead. A caller's own abort is never\n * a fallback: it rejects as it would against the backend.\n */\n onPassthroughFallback?: (key: string, error: ConnectError) => void;\n}\n\n/** Options for {@link resolveActiveScenario}. */\nexport interface ResolveScenarioOptions {\n /**\n * The scenario to run, chosen by the app (its host, its runtime config)\n * instead of the URL. When the key is present — even as null or \"\" — the\n * `?scenario=` parameter is NOT read; null/\"\"/unknown select the default.\n */\n name?: string | null;\n}\n\n/**\n * Mutable session stores, keyed by descriptor identity.\n *\n * Create/Update/Delete round-trip within one browser session (a created row\n * appears in the next List; a deleted one disappears) and reset to the\n * deterministic fixtures on reload. The scaffolded original got that from\n * module-scope `const store = new Map(...)`; keying off the descriptor\n * object preserves it — a module-level descriptor array hands back the same\n * store on every call, so building the transport twice (hybrid mode, a test\n * swapping transports) does not silently fork the data.\n */\nconst stores = new WeakMap<MockEntityDescriptor, Map<string, unknown>>();\n\nfunction storeFor(entity: MockEntityDescriptor): Map<string, unknown> {\n let store = stores.get(entity);\n if (!store) {\n store = new Map(\n entity.fixtures.map((row) => [\n String((row as Record<string, unknown>)[entity.pkField]),\n row,\n ]),\n );\n stores.set(entity, store);\n }\n return store;\n}\n\n/** Scenarios whose `setup()` has already run. */\nconst initialized = new WeakSet<MockScenario>();\n\n/**\n * Resolve the scenario the app should run under, and run its `setup()` once.\n *\n * Call this at MODULE SCOPE (the generated `src/lib/mock-transport.ts` does).\n * Reading the URL once — rather than on every RPC — means navigating away\n * from `?scenario=` keeps the scenario active until a full reload, matching\n * the agent-driven flow where the URL is the single source of truth.\n *\n * `setup()` is for non-RPC state (localStorage flags, sessionStorage). It is\n * synchronous and makes no network calls.\n */\nexport function resolveActiveScenario(\n registry: MockScenarioRegistry,\n options: ResolveScenarioOptions = {},\n): MockScenario {\n const requested =\n \"name\" in options\n ? (options.name ?? null)\n : typeof globalThis !== \"undefined\" && globalThis.location\n ? new URLSearchParams(globalThis.location.search).get(\"scenario\")\n : null;\n // A ternary, not `&&`: with `??` alone the empty-string falsy branch leaks\n // '' into the inferred union, and '' survives `??` (which only replaces\n // null/undefined), so downstream access on .setup / .handlers would fail\n // under strict tsc.\n const active =\n (requested ? registry.byName(requested) : undefined) ??\n registry.defaultScenario;\n\n if (!initialized.has(active)) {\n initialized.add(active);\n active.setup?.();\n }\n return active;\n}\n\n/**\n * Normalize whatever a streaming scenario handler returns into an\n * AsyncIterable<unknown>. Accepts arrays, iterables, async iterables, and\n * promises that resolve to any of the above. Single (non-iterable) values\n * are wrapped as a one-element stream — handy for handlers that conceptually\n * yield once.\n */\nexport async function* toAsyncIterable(value: unknown): AsyncIterable<unknown> {\n const resolved = await Promise.resolve(value);\n if (resolved == null) return;\n // AsyncIterable\n if (\n typeof (resolved as { [Symbol.asyncIterator]?: unknown })[\n Symbol.asyncIterator\n ] === \"function\"\n ) {\n for await (const m of resolved as AsyncIterable<unknown>) yield m;\n return;\n }\n // Iterable (including arrays)\n if (\n typeof (resolved as { [Symbol.iterator]?: unknown })[Symbol.iterator] ===\n \"function\"\n ) {\n for (const m of resolved as Iterable<unknown>) yield m;\n return;\n }\n // Single value\n yield resolved;\n}\n\nfunction makeUnaryResponse<T>(\n method: { name: string; parent: { typeName: string }; output?: DescMessage },\n message: T,\n): UnaryResponse<never, never> {\n return {\n service: method.parent as never,\n method: method as never,\n stream: false,\n header: new Headers(),\n message: asResponseMessage(method.output, message) as never,\n trailer: new Headers(),\n };\n}\n\n/**\n * A handler may return the response as a plain init object\n * (`{ items: [...] }`) instead of `create(Schema, {...})`. A real client\n * always receives a full message — every repeated field an array, every\n * scalar its zero value — so a plain object is completed against the\n * method's output schema here; without that, `res.items.map` on a field the\n * handler left out throws in the UI and nowhere near the scenario.\n */\nfunction asResponseMessage(\n output: DescMessage | undefined,\n value: unknown,\n): unknown {\n if (!output || value == null || typeof value !== \"object\") return value;\n if (isMessage(value)) return value;\n return create(output, value as MessageInitShape<DescMessage>);\n}\n\n/** The default read-only classification: the proto says the method has no side effects. */\nfunction isRead(method: DescMethod): boolean {\n return method.idempotency === MethodOptions_IdempotencyLevel.NO_SIDE_EFFECTS;\n}\n\n/**\n * A fixture-backed handler for one RPC key. `requestSchema` is the Connect\n * method's input descriptor — absent only when a caller hand-builds a method\n * object without one, in which case List serves paging alone.\n */\ntype FixtureHandler = (\n input: unknown,\n requestSchema: DescMessage | undefined,\n) => unknown;\n\n// ── List semantics ─────────────────────────────────────────────────────\n//\n// What the backend's generated List does, and therefore what the fixture\n// List does (pkg/crud HandleList + the generated Filters closure):\n//\n// - Every `optional` request field that is SET filters by equality on the\n// entity field of the same name. Enums compare by value. A field with\n// IMPLICIT presence (no `optional`) is never a filter — the backend\n// generates none for it, so the mock must not invent one.\n// - `search` matches case-insensitively against the entity's string\n// fields and enum value names (enum columns are stored as names).\n// - `order_by` (snake_case column, comma-separated, optional ASC/DESC per\n// column) plus `descending` sorts; an unknown column is InvalidArgument.\n// - `page_size` defaults to 50 and is clamped to 100. `total_count` is the\n// filtered count BEFORE paging. `next_page_token` is minted only for the\n// default (primary-key) order — an ordered list is single-page on the\n// backend, and a mock that paged it would hide that.\n//\n// Paging, ordering and search arrive through these request fields, by the\n// names forge's CRUD List requests declare. They are never equality filters.\nconst LIST_CONTROL_FIELDS = new Set([\n \"pageSize\",\n \"pageToken\",\n \"orderBy\",\n \"descending\",\n \"search\",\n]);\n\n/** pkg/crud ListOp's defaults: 0 means 50, and no page exceeds 100. */\nconst DEFAULT_PAGE_SIZE = 50;\nconst MAX_PAGE_SIZE = 100;\n\n/** FeatureSet_FieldPresence.EXPLICIT — proto3 `optional`, oneofs, messages. */\nconst EXPLICIT_PRESENCE = 1;\n\ntype Row = Record<string, unknown>;\n\n/** The entity message a List response repeats, read off its descriptor. */\nfunction listItemSchema(list: MockListRpc): DescMessage | undefined {\n const items = list.responseSchema.field[list.itemsField];\n return items?.fieldKind === \"list\" && items.listKind === \"message\"\n ? items.message\n : undefined;\n}\n\n/** Scalars and enums compare by value; messages, bytes, lists and maps do not. */\nfunction isComparable(field: DescField): boolean {\n if (field.fieldKind === \"enum\") return true;\n return field.fieldKind === \"scalar\" && field.scalar !== ScalarType.BYTES;\n}\n\nfunction sameValue(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n // A 64-bit field is a bigint — or a string under jstype=JS_STRING — and\n // the two sides are not guaranteed to agree on which.\n if (a == null || b == null) return false;\n return (\n (typeof a === \"bigint\" || typeof b === \"bigint\") && String(a) === String(b)\n );\n}\n\nfunction enumName(field: DescField, value: unknown): string | undefined {\n return field.fieldKind === \"enum\"\n ? field.enum.values.find((v) => v.number === value)?.name\n : undefined;\n}\n\n/** The value a column sorts by, as Postgres would order the stored column. */\nfunction sortKey(field: DescField, value: unknown): unknown {\n if (value == null) return undefined;\n // Enum columns hold the value NAME, so they sort alphabetically by name.\n if (field.fieldKind === \"enum\") return enumName(field, value) ?? \"\";\n if (field.fieldKind === \"scalar\") return value;\n if (\n field.fieldKind === \"message\" &&\n field.message.typeName === \"google.protobuf.Timestamp\"\n ) {\n const ts = value as { seconds: bigint; nanos: number };\n return BigInt(ts.seconds) * 1_000_000_000n + BigInt(ts.nanos);\n }\n return undefined;\n}\n\n/** Postgres's default: NULL sorts after every value ascending. */\nfunction compareKeys(a: unknown, b: unknown): number {\n if (a === undefined || b === undefined) {\n return a === b ? 0 : a === undefined ? 1 : -1;\n }\n const x = a as string | number | bigint | boolean;\n const y = b as string | number | bigint | boolean;\n return x < y ? -1 : x > y ? 1 : 0;\n}\n\nfunction invalidArgument(message: string): ConnectError {\n return new ConnectError(`mock-transport: ${message}`, Code.InvalidArgument);\n}\n\n/** Parse `order_by` the way orm.ValidateOrderBy reads it. */\nfunction parseOrderBy(\n clause: string,\n descending: boolean,\n itemSchema: DescMessage,\n): { field: DescField; desc: boolean }[] {\n return clause.split(\",\").map((part) => {\n const [column = \"\", direction] = part.trim().split(/\\s+/);\n const field = itemSchema.fields.find(\n (f) => f.name === column || f.localName === column,\n );\n if (!field) {\n throw invalidArgument(`unknown order-by column \"${column}\"`);\n }\n const dir = direction?.toUpperCase();\n if (dir !== undefined && dir !== \"ASC\" && dir !== \"DESC\") {\n throw invalidArgument(`invalid order-by direction \"${direction}\"`);\n }\n return { field, desc: dir === undefined ? descending : dir === \"DESC\" };\n });\n}\n\n/** A count in whatever JS type the response's total_count field takes. */\nfunction countValue(field: DescField, n: number): unknown {\n if (field.fieldKind !== \"scalar\") return n;\n switch (field.scalar) {\n case ScalarType.INT64:\n case ScalarType.UINT64:\n case ScalarType.SINT64:\n case ScalarType.FIXED64:\n case ScalarType.SFIXED64:\n return field.longAsString ? String(n) : BigInt(n);\n default:\n return n;\n }\n}\n\n/**\n * Answer one List call from the entity's rows. See the List semantics note\n * above for the contract; the engine's tests pin each clause.\n */\nfunction serveList(\n rows: readonly unknown[],\n list: MockListRpc,\n input: unknown,\n requestSchema: DescMessage | undefined,\n): unknown {\n const req = (input ?? {}) as Row;\n const itemSchema = listItemSchema(list);\n let matched = rows as readonly Row[];\n\n if (itemSchema && requestSchema) {\n for (const filter of requestSchema.fields) {\n if (LIST_CONTROL_FIELDS.has(filter.localName)) continue;\n if (filter.presence !== EXPLICIT_PRESENCE || !isComparable(filter)) {\n continue;\n }\n const want = req[filter.localName];\n if (want == null) continue; // unset: no filter\n const target = itemSchema.field[filter.localName];\n if (!target || !isComparable(target)) continue;\n matched = matched.filter((row) => sameValue(row[filter.localName], want));\n }\n }\n\n const search =\n typeof req.search === \"string\" ? req.search.trim().toLowerCase() : \"\";\n if (itemSchema && search) {\n const searchable = itemSchema.fields.filter(\n (f) =>\n f.fieldKind === \"enum\" ||\n (f.fieldKind === \"scalar\" && f.scalar === ScalarType.STRING),\n );\n matched = matched.filter((row) =>\n searchable.some((f) => {\n const value = row[f.localName];\n const text = f.fieldKind === \"enum\" ? enumName(f, value) : value;\n return typeof text === \"string\" && text.toLowerCase().includes(search);\n }),\n );\n }\n\n const orderBy = typeof req.orderBy === \"string\" ? req.orderBy.trim() : \"\";\n if (itemSchema && orderBy) {\n const keys = parseOrderBy(orderBy, req.descending === true, itemSchema);\n matched = [...matched].sort((a, b) => {\n for (const { field, desc } of keys) {\n const c = compareKeys(\n sortKey(field, a[field.localName]),\n sortKey(field, b[field.localName]),\n );\n if (c !== 0) return desc ? -c : c;\n }\n return 0;\n });\n }\n\n const total = matched.length;\n\n let offset = 0;\n const token = typeof req.pageToken === \"string\" ? req.pageToken : \"\";\n if (token) {\n // The mock's own cursor: an offset into the filtered rows. Opaque to the\n // client, exactly like the backend's keyset cursor.\n if (!/^\\d+$/.test(token)) throw invalidArgument(\"invalid page token\");\n offset = Number(token);\n }\n const requested = Number(req.pageSize ?? 0);\n const pageSize =\n requested > 0 ? Math.min(requested, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE;\n const end = offset + pageSize;\n\n const init: Row = { [list.itemsField]: matched.slice(offset, end) };\n const totalField = list.responseSchema.field.totalCount;\n if (totalField) init.totalCount = countValue(totalField, total);\n if (list.responseSchema.field.nextPageToken && !orderBy && end < total) {\n init.nextPageToken = String(end);\n }\n return create(list.responseSchema, init as MessageInitShape<DescMessage>);\n}\n\n/**\n * Compile the entity descriptors into a flat `${service}/${rpc}` → handler\n * map. Building it once per transport keeps per-RPC dispatch to a single\n * lookup, exactly like the generated `switch` it replaces.\n */\nfunction buildFixtureTable(\n entities: readonly MockEntityDescriptor[],\n): Map<string, FixtureHandler> {\n const table = new Map<string, FixtureHandler>();\n\n for (const entity of entities) {\n const pk = entity.pkField;\n const key = (rpc: string) => `${entity.service}/${rpc}`;\n\n if (entity.list) {\n const list = entity.list;\n table.set(key(list.rpc), (input, requestSchema) =>\n serveList(\n Array.from(storeFor(entity).values()),\n list,\n input,\n requestSchema,\n ),\n );\n }\n\n if (entity.get) {\n const { rpc, responseSchema, entityField } = entity.get;\n table.set(key(rpc), (input) => {\n // Read the key off the SAME field the store is keyed by, not a\n // hardcoded `id` — a surrogate-PK entity would otherwise look up\n // String(undefined) and miss every record.\n const req = input as Record<string, unknown> | undefined;\n const found = storeFor(entity).get(String(req?.[pk]));\n if (!found) {\n // A miss is NotFound — exactly what the real backend returns.\n // (Falling back to the first fixture made every detail page\n // \"work\" against the wrong record and hid bad-id bugs.)\n throw new ConnectError(\n `${entity.label} ${String(req?.[pk])} not found`,\n Code.NotFound,\n );\n }\n return create(responseSchema, {\n [entityField]: found,\n } as MessageInitShape<DescMessage>);\n });\n }\n\n if (entity.create && entity.entitySchema) {\n const { rpc, responseSchema, entityField, requestField } = entity.create;\n const entitySchema = entity.entitySchema;\n table.set(key(rpc), (input) => {\n // Create requests carry the entity fields flattened (that's how the\n // generated form submits); a nested { <entity>: {...} } payload is\n // accepted too.\n const req = (input ?? {}) as Record<string, unknown>;\n const init = {\n ...((req[requestField] ?? req) as Record<string, unknown>),\n };\n delete init.$typeName;\n // Mint the primary key on the PK field the store is keyed by — for a\n // surrogate-PK entity this is NOT `id`, and writing a generated `id`\n // would leave the store key undefined so the row never round-trips.\n const id =\n typeof init[pk] === \"string\" && init[pk]\n ? (init[pk] as string)\n : crypto.randomUUID();\n const created = create(entitySchema, {\n ...init,\n [pk]: id,\n } as MessageInitShape<DescMessage>);\n storeFor(entity).set(String(id), created);\n return create(responseSchema, {\n [entityField]: created,\n } as MessageInitShape<DescMessage>);\n });\n }\n\n if (entity.update && entity.entitySchema) {\n const { rpc, responseSchema, entityField, requestField } = entity.update;\n const entitySchema = entity.entitySchema;\n table.set(key(rpc), (input) => {\n const req = (input ?? {}) as Record<string, unknown>;\n // AIP-134 requests wrap the entity ({ <entity>: {...}, updateMask })\n // — the id lives inside the wrapper; flat requests carry it at the\n // top level.\n const patch = {\n ...((req[requestField] ?? req) as Record<string, unknown>),\n };\n delete patch.$typeName;\n const id = String(patch[pk] ?? req[pk] ?? \"\");\n const store = storeFor(entity);\n const existing = store.get(id);\n if (!existing) {\n throw new ConnectError(\n `${entity.label} ${id} not found`,\n Code.NotFound,\n );\n }\n const updated = create(entitySchema, {\n ...(existing as Record<string, unknown>),\n ...patch,\n [pk]: id,\n } as MessageInitShape<DescMessage>);\n store.set(id, updated);\n return create(responseSchema, {\n [entityField]: updated,\n } as MessageInitShape<DescMessage>);\n });\n }\n\n if (entity.delete) {\n table.set(key(entity.delete.rpc), (input) => {\n // Delete by the SAME field the store is keyed by.\n const req = input as Record<string, unknown> | undefined;\n storeFor(entity).delete(String(req?.[pk]));\n // Delete returns an empty response — the standard CRUD shape.\n return {};\n });\n }\n }\n\n return table;\n}\n\n/**\n * Create the mock transport.\n *\n * The generated `src/lib/mock-transport.ts` supplies the resolved scenario\n * and the project's entity descriptors; `connect.ts` supplies the optional\n * real transport when running in hybrid mode.\n */\nexport function createMockTransport(\n options: CreateMockTransportOptions,\n): Transport {\n const {\n scenario,\n entities = [],\n fallback,\n readOnly,\n passthroughTimeoutMs,\n onPassthroughFallback,\n } = options;\n const passthrough = scenario.passthrough === true && fallback != null;\n const first = scenario.passthrough === \"first\" && fallback != null;\n const fixtures = buildFixtureTable(entities);\n const readsOnly = typeof readOnly === \"function\" ? readOnly : isRead;\n\n // 0) The read-only guard: before any dispatch, so a write never reaches a\n // handler, a fixture store or the backend.\n const guard = (method: DescMethod, key: string) => {\n if (readOnly && !readsOnly(method)) {\n throw new ConnectError(\n options.readOnlyMessage ?? `mock-transport: ${key} is not a read`,\n Code.PermissionDenied,\n );\n }\n };\n // \"first\": the backend's answer, or undefined when the mock should answer.\n //\n // The deadline is enforced HERE, not left to the fallback transport: the\n // attempt gets its own signal (aborted by the caller's signal or by the\n // timer) and is raced against that signal, so a transport that ignores\n // both its timeout and its signal still cannot hold the call open. A\n // caller's own abort is rethrown; only the deadline or a failure falls back.\n const tryBackend = async <R>(\n key: string,\n signal: AbortSignal | undefined,\n timeoutMs: number | undefined,\n call: (signal: AbortSignal) => Promise<R>,\n ): Promise<R | undefined> => {\n const attempt = new AbortController();\n const abortAttempt = () => attempt.abort(signal?.reason);\n if (signal?.aborted) abortAttempt();\n signal?.addEventListener(\"abort\", abortAttempt, { once: true });\n const timer =\n timeoutMs != null && timeoutMs > 0\n ? setTimeout(\n () =>\n attempt.abort(\n new ConnectError(\n `mock-transport: ${key} exceeded ${timeoutMs}ms`,\n Code.DeadlineExceeded,\n ),\n ),\n timeoutMs,\n )\n : undefined;\n const abandoned = new Promise<never>((_, reject) => {\n const fail = () => reject(attempt.signal.reason);\n if (attempt.signal.aborted) fail();\n else attempt.signal.addEventListener(\"abort\", fail, { once: true });\n });\n // The race observes it; this keeps the release below from surfacing as\n // an unhandled rejection after the race has settled.\n abandoned.catch(() => undefined);\n try {\n return await Promise.race([call(attempt.signal), abandoned]);\n } catch (err) {\n if (signal?.aborted) throw err;\n onPassthroughFallback?.(key, ConnectError.from(err));\n return undefined;\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n signal?.removeEventListener(\"abort\", abortAttempt);\n // Release whatever the backend attempt still holds (a pending fetch,\n // an unread stream) once it has been answered or abandoned.\n attempt.abort();\n }\n };\n\n // Bind the object literal to a Transport-typed variable rather than\n // casting at the return site. A trailing assertion cast does not propagate\n // Connect's Transport signature backwards into the literal's method\n // bodies, so under `strict` tsc every callback parameter on unary/stream\n // errors with TS7006 (\"implicitly has an 'any' type\").\n const transport: Transport = {\n // Connect v2's Transport.unary signature is\n // (method, signal, timeoutMs, header, input, contextValues)\n // — method first, no separate service arg. `method.parent.typeName` is\n // the fully-qualified service name; that is what handler keys and\n // fixture keys are matched against.\n async unary(method, signal, timeoutMs, header, input, contextValues) {\n const key = `${method.parent.typeName}/${method.name}`;\n guard(method, key);\n\n // 0b) \"first\": the backend answers unless it fails.\n if (first) {\n const deadline = passthroughTimeoutMs ?? timeoutMs;\n const live = await tryBackend(key, signal, deadline, (attempt) =>\n fallback!.unary(\n method,\n attempt,\n deadline,\n header,\n input,\n contextValues,\n ),\n );\n if (live) return live;\n }\n\n // 1) Scenario overlay.\n const handler = scenario.handlers[key];\n if (handler) {\n const result = await handler(input as never);\n return makeUnaryResponse(method, result);\n }\n\n // 2) Passthrough: route to the real backend instead of fixtures.\n // Skips fixtures entirely — in hybrid mode the whole point is\n // \"use real data for anything not explicitly stubbed\".\n if (passthrough) {\n return fallback!.unary(\n method,\n signal,\n timeoutMs,\n header,\n input,\n contextValues,\n );\n }\n\n // 3) Base fixture dispatch, backed by the mutable per-entity stores so\n // writes round-trip within the session. Misses are REAL errors\n // (Code.NotFound), never a silent wrong-record fallback.\n const fixture = fixtures.get(key);\n if (fixture) {\n // `method.input` is typed as always present, but a hand-built method\n // object (a test, a custom harness) may omit it.\n const requestSchema = (method as { input?: DescMessage }).input;\n return makeUnaryResponse(method, fixture(input, requestSchema));\n }\n\n // 4) Nothing matched. Surface a clear error instead of hanging.\n return Promise.reject(\n new ConnectError(\n `mock-transport: no scenario handler or entity fixture for ${key}`,\n Code.Unimplemented,\n ),\n );\n },\n\n // Streaming: a scenario handler can return an array, iterable, async\n // iterable, or a Promise resolving to any of those. The transport adapts\n // it into the AsyncIterable<Response> shape Connect expects. If no\n // handler matches, reject with Unimplemented — base-fixture streaming is\n // intentionally not served (there is no canonical \"list of N messages\"\n // shape for an arbitrary streaming RPC).\n //\n // No explicit return-type annotation: the outer `const transport:\n // Transport` binding lets tsc infer the per-callback signature from\n // Connect's generic Transport.stream<I, O>. Annotating this as\n // Promise<StreamResponse<never, never>> makes the passthrough branch\n // ill-typed (TS2322) because fallback.stream returns StreamResponse<I,O>.\n async stream(method, signal, timeoutMs, header, input, contextValues) {\n const key = `${method.parent.typeName}/${method.name}`;\n guard(method, key);\n // \"first\" forwards server streams only: a client-streaming request is\n // consumed by the attempt and could not be replayed to the handler.\n //\n // The backend's stream is read to its END, within the deadline, before\n // it counts as an answer, then replayed: a stream that fails or stalls\n // after its headers arrived still falls back to the mock, as a unary\n // call does. So \"first\" suits finite server streams; a stream that\n // never ends always answers from the mock once the deadline passes.\n if (first && method.methodKind === \"server_streaming\") {\n const deadline = passthroughTimeoutMs ?? timeoutMs;\n const live = await tryBackend(\n key,\n signal,\n deadline,\n async (attempt) => {\n const res = await fallback!.stream(\n method,\n attempt,\n deadline,\n header,\n input,\n contextValues,\n );\n const messages: unknown[] = [];\n for await (const m of res.message) messages.push(m);\n return {\n ...res,\n message: toAsyncIterable(messages) as typeof res.message,\n };\n },\n );\n if (live) return live;\n }\n const handler = scenario.handlers[key];\n if (!handler) {\n // Passthrough mirrors the unary path: in hybrid mode, unmatched\n // streaming RPCs go to the real backend.\n if (passthrough) {\n return fallback!.stream(\n method,\n signal,\n timeoutMs,\n header,\n input,\n contextValues,\n );\n }\n return Promise.reject(\n new ConnectError(\n `mock-transport: no scenario handler for streaming RPC ${key}`,\n Code.Unimplemented,\n ),\n );\n }\n // Read the request stream eagerly into an array so the scenario handler\n // sees the full client-streaming payload. For server-stream and\n // unary-into-stream-out RPCs this is a single message.\n const requestMessages: unknown[] = [];\n try {\n for await (const m of input as AsyncIterable<unknown>) {\n requestMessages.push(m);\n }\n } catch {\n // Synthetic clients sometimes pass a single value rather than an\n // iterable; treat that as an empty request payload.\n }\n const handlerInput =\n requestMessages.length <= 1 ? requestMessages[0] : requestMessages;\n const result = handler(handlerInput as never);\n return {\n service: method.parent as never,\n method: method as never,\n stream: true,\n header: new Headers(),\n message: toAsyncIterable(result) as AsyncIterable<never>,\n trailer: new Headers(),\n };\n },\n };\n return transport;\n}\n"]}
|
package/dist/node.js
CHANGED
|
@@ -149,6 +149,16 @@ export function buildNodeResource(config, runtime, version = sdkVersion()) {
|
|
|
149
149
|
out["telemetry.sdk.version"] = version;
|
|
150
150
|
if (config.serviceVersion)
|
|
151
151
|
out["service.version"] = config.serviceVersion;
|
|
152
|
+
if (electron) {
|
|
153
|
+
// A desktop app's main process: the same app.desktop.* its renderer
|
|
154
|
+
// reports from the preload bridge (client-context.ts), so main-process
|
|
155
|
+
// errors name the app, Electron and Chromium versions they ran on.
|
|
156
|
+
if (config.serviceVersion)
|
|
157
|
+
out["app.desktop.version"] = config.serviceVersion;
|
|
158
|
+
out["app.desktop.electron.version"] = electron;
|
|
159
|
+
if (runtime.versions.chrome)
|
|
160
|
+
out["app.desktop.chromium.version"] = runtime.versions.chrome;
|
|
161
|
+
}
|
|
152
162
|
if (config.serviceNamespace)
|
|
153
163
|
out["service.namespace"] = config.serviceNamespace;
|
|
154
164
|
if (config.frontend)
|