@reliantlabs/forge-web-runtime 0.3.0 → 0.3.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 +25 -25
- package/dist/devlog.d.ts +50 -0
- package/dist/devlog.js +449 -0
- package/dist/devlog.js.map +1 -0
- package/dist/error-boundary.js +2 -1
- package/dist/error-boundary.js.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors.js.map +1 -1
- package/dist/mock-transport.js +1 -1
- package/dist/mock-transport.js.map +1 -1
- package/dist/resource.js +2 -2
- package/dist/resource.js.map +1 -1
- package/dist/route-guard.js.map +1 -1
- package/dist/service-hooks.js +9 -0
- package/dist/service-hooks.js.map +1 -1
- package/dist/telemetry.js.map +1 -1
- package/package.json +53 -25
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-boundary.js","sourceRoot":"","sources":["../src/error-boundary.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,sEAAsE;AACtE,EAAE;AACF,2EAA2E;AAC3E,sEAAsE;AACtE,uEAAuE;AACvE,6EAA6E;AAC7E,uEAAuE;AACvE,OAAO,EACL,SAAS,GAIV,MAAM,OAAO,CAAC;AAuBf,MAAM,OAAO,oBAAqB,SAAQ,SAAuB;IAC/D,YAAY,KAAY;QACtB,KAAK,CAAC,KAAK,CAAC,CAAC;QACb,IAAI,CAAC,KAAK,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC/B,CAAC;IAED,MAAM,CAAC,wBAAwB,CAAC,KAAY;QAC1C,OAAO,EAAE,KAAK,EAAE,CAAC;IACnB,CAAC;IAED,iBAAiB,CAAC,KAAY,EAAE,IAAe;QAC7C,sEAAsE;QACtE,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAC/D,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,kBAAkB,CAAC,IAAW;QAC5B,
|
|
1
|
+
{"version":3,"file":"error-boundary.js","sourceRoot":"","sources":["../src/error-boundary.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// A React error boundary with a designed fallback. Next.js file-convention\n// boundaries (app/error.tsx, app/global-error.tsx) catch route render\n// crashes; this component boundary lets you ALSO isolate a subtree — a\n// dashboard widget, a <Resource> table — so one crashing panel degrades to a\n// small fallback instead of taking the whole route (or the app) white.\nimport {\n Component,\n type ErrorInfo,\n type ReactElement,\n type ReactNode,\n} from \"react\";\n\ninterface FallbackProps {\n error: Error;\n reset: () => void;\n}\n\ninterface Props {\n children: ReactNode;\n /** Custom fallback UI. Defaults to DefaultErrorFallback. */\n fallback?: (props: FallbackProps) => ReactNode;\n /** Reporter hook — wire to telemetry (initClientTelemetry) or Sentry. */\n onError?: (error: Error, info: ErrorInfo) => void;\n /** When any value in this array changes, the boundary resets. */\n resetKeys?: unknown[];\n /** Compact single-line fallback (for small subtrees). */\n compact?: boolean;\n}\n\ninterface State {\n error: Error | null;\n}\n\nexport class RuntimeErrorBoundary extends Component<Props, State> {\n constructor(props: Props) {\n super(props);\n this.state = { error: null };\n }\n\n static getDerivedStateFromError(error: Error): State {\n return { error };\n }\n\n componentDidCatch(error: Error, info: ErrorInfo): void {\n // Always log; the app can additionally forward to a sink via onError.\n console.error(\"[runtime] error boundary caught:\", error, info);\n this.props.onError?.(error, info);\n }\n\n componentDidUpdate(prev: Props): void {\n if (\n this.state.error &&\n !shallowEqual(prev.resetKeys, this.props.resetKeys)\n ) {\n this.reset();\n }\n }\n\n reset = (): void => {\n this.setState({ error: null });\n };\n\n render(): ReactNode {\n const { error } = this.state;\n if (!error) {\n return this.props.children;\n }\n if (this.props.fallback) {\n return this.props.fallback({ error, reset: this.reset });\n }\n return (\n <DefaultErrorFallback\n error={error}\n reset={this.reset}\n compact={this.props.compact}\n />\n );\n }\n}\n\nfunction shallowEqual(a?: unknown[], b?: unknown[]): boolean {\n if (a === b) {\n return true;\n }\n if (!a || !b || a.length !== b.length) {\n return false;\n }\n return a.every((v, i) => Object.is(v, b[i]));\n}\n\n// The `: ReactElement` here and on every other exported component is\n// load-bearing, not decoration — see README, \"One .d.ts for React 18 and 19\".\n// Inferring it makes the published declaration name a type whose SPELLING\n// differs between the two @types/react majors this package supports.\nexport function DefaultErrorFallback({\n error,\n reset,\n compact,\n}: FallbackProps & { compact?: boolean }): ReactElement {\n if (compact) {\n return (\n <div\n role=\"alert\"\n className=\"flex items-center justify-between gap-3 rounded-md border border-danger-border bg-danger-surface px-3 py-2 text-sm text-danger-ink\"\n >\n <span className=\"truncate\">Couldn't load this section.</span>\n <button\n type=\"button\"\n onClick={reset}\n className=\"shrink-0 font-medium text-danger underline hover:text-danger-hover\"\n >\n Retry\n </button>\n </div>\n );\n }\n return (\n <div\n role=\"alert\"\n className=\"flex min-h-[12rem] flex-col items-center justify-center gap-3 rounded-lg border border-danger-border bg-danger-surface p-8 text-center\"\n >\n <h2 className=\"text-lg font-semibold text-danger-ink\">\n Something went wrong\n </h2>\n <p className=\"max-w-md text-sm text-danger-ink\">\n This section ran into an unexpected error. You can try again — the rest\n of the app is still working.\n </p>\n <p className=\"max-w-md font-mono text-xs text-danger\">{error.message}</p>\n <button\n type=\"button\"\n onClick={reset}\n className=\"mt-1 rounded-md bg-danger px-4 py-2 text-sm font-medium text-on-danger hover:bg-danger-hover\"\n >\n Try again\n </button>\n </div>\n );\n}\n"]}
|
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Typed client errors. Connect surfaces failures as ConnectError with a\n// numeric Code; the rest of the app should reason about a stable, typed\n// shape instead of sniffing status codes at every call site. normalizeError\n// maps any thrown value into a ConnectClientError carrying the canonical\n// code name, an HTTP-ish status, a retryability verdict, and a\n// human-presentable message.\nimport { Code, ConnectError } from \"@connectrpc/connect\";\n\n/**\n * FORGE_ERROR_REASON_HEADER is the Connect trailer/metadata key carrying a\n * stable, machine-readable reason code (snake_case) the backend sets for\n * every error. UI/routing keys off `error.reason` instead of matching\n * human-readable message strings — the message text is display-only and\n * may change. The backend owns the setter; the frontend only reads it.\n */\nexport const FORGE_ERROR_REASON_HEADER = \"x-forge-error-reason\";\n\n/**\n * stripServerFraming removes the transport framing that must never surface\n * to a user: the leading \"[code] \" gRPC-code prefix connect-es puts on\n * `ConnectError.message`. What survives is the message the server wrote,\n * which is exactly what a user should read.\n *\n * This is the SINGLE implementation — a project never wants a different\n * answer here, and a copy frozen into a scaffold could never be corrected\n * once shipped. Idempotent: framing that has already been stripped strips\n * to itself.\n */\nexport function stripServerFraming(message: string): string {\n return message.replace(/^\\[[a-z_]+\\]\\s*/i, \"\").trim();\n}\n\n/** The copy shown when an error carries no presentable message of its own. */\nconst DEFAULT_USER_MESSAGE = \"Something went wrong. Please try again.\";\n\n/**\n * userMessage — turn any thrown value into copy fit for end users.\n *\n * `ConnectError.message` prefixes the gRPC code (\"[not_found] no such task\");\n * `rawMessage` is the server's human-readable text without that framing, and\n * stripServerFraming drops the prefix from whichever of the two is available.\n * Use this everywhere an error is SHOWN (banners, toasts) instead of\n * `err.message`.\n *\n * Pass `fallback` to override the default copy for a surface that wants its\n * own wording.\n */\nexport function userMessage(err: unknown, fallback: string = DEFAULT_USER_MESSAGE): string {\n if (err instanceof ConnectError) {\n return stripServerFraming(err.rawMessage || err.message) || fallback;\n }\n if (err instanceof Error) {\n return stripServerFraming(err.message) || fallback;\n }\n return err == null ? fallback : stripServerFraming(String(err)) || fallback;\n}\n\n/** Connect codes worth a transparent transport-level retry. */\nconst RETRYABLE = new Set<Code>([\n Code.Unavailable,\n Code.DeadlineExceeded,\n Code.Aborted,\n Code.ResourceExhausted,\n]);\n\n/** Rough Connect-code → HTTP-status mapping for display/telemetry only. */\nconst HTTP_STATUS: Partial<Record<Code, number>> = {\n [Code.InvalidArgument]: 400,\n [Code.Unauthenticated]: 401,\n [Code.PermissionDenied]: 403,\n [Code.NotFound]: 404,\n [Code.AlreadyExists]: 409,\n [Code.ResourceExhausted]: 429,\n [Code.Unimplemented]: 501,\n [Code.Unavailable]: 503,\n [Code.DeadlineExceeded]: 504,\n [Code.Internal]: 500,\n};\n\n/**\n * isConnectClientErrorShape — the STRUCTURAL test behind\n * {@link ConnectClientError}'s `instanceof`.\n *\n * A class object is per module instance, not per package. npm nests a\n * transitive dependency on any version conflict, and a bundler can pull one\n * package in as both ESM and CJS, so a tree holding two copies of this\n * package holds two distinct ConnectClientError classes. Matching on shape\n * instead of identity is what makes the two agree: the shape is the\n * contract, the constructor is not.\n */\nfunction isConnectClientErrorShape(v: unknown): v is ConnectClientError {\n if (!(v instanceof Error)) return false;\n if (Object.getPrototypeOf(v) === ConnectClientError.prototype) return true;\n const e = v as Partial<ConnectClientError>;\n return (\n v.name === \"ConnectClientError\" &&\n typeof e.code === \"string\" &&\n typeof e.status === \"number\" &&\n typeof e.retryable === \"boolean\" &&\n (typeof e.reason === \"string\" || e.reason === null)\n );\n}\n\n/**\n * ConnectClientError is the single typed error shape the app catches. It is\n * a plain Error subclass so it works with React Query, error boundaries, and\n * `instanceof` alike.\n */\nexport class ConnectClientError extends Error {\n /** Canonical Connect code name, e.g. \"unavailable\", \"not_found\". */\n readonly code: string;\n /** Numeric Connect code (Code enum), or undefined for non-Connect errors. */\n readonly connectCode?: Code;\n /** Approximate HTTP status, for telemetry and generic UI. */\n readonly status: number;\n /** True when a transparent retry is reasonable (transient failures). */\n readonly retryable: boolean;\n /**\n * Stable machine-readable reason code from the backend\n * (x-forge-error-reason metadata), or null. Key UI/routing off THIS,\n * never off `message` — message text is display-only.\n */\n readonly reason: string | null;\n\n constructor(args: {\n message: string;\n code: string;\n connectCode?: Code;\n status: number;\n retryable: boolean;\n reason?: string | null;\n cause?: unknown;\n }) {\n super(args.message, { cause: args.cause });\n this.name = \"ConnectClientError\";\n this.code = args.code;\n this.connectCode = args.connectCode;\n this.status = args.status;\n this.retryable = args.retryable;\n this.reason = args.reason ?? null;\n }\n\n /**\n * `instanceof` matches on SHAPE, not on class identity — so every check\n * spelled `err instanceof ConnectClientError`, here and in app code, keeps\n * working when two copies of this package end up in one bundle (see\n * {@link isConnectClientErrorShape}). Without it, normalizeError's\n * pass-through guard misses an already-normalized error from the other\n * copy; that error's `name` is \"ConnectClientError\" rather than\n * \"ConnectError\", so it misses the ConnectError branch too and is re-wrapped\n * through the generic Error branch — silently downgrading every error in the\n * app to code \"unknown\", status 500, no connectCode and no reason.\n * @connectrpc/connect does the same on ConnectError, for the same reason.\n */\n static [Symbol.hasInstance](v: unknown): boolean {\n return isConnectClientErrorShape(v);\n }\n}\n\n/** isRetryable reports whether a Connect code merits a transport retry. */\nexport function isRetryableCode(code: Code): boolean {\n return RETRYABLE.has(code);\n}\n\n/**\n * normalizeError coerces any thrown value into a ConnectClientError. Already-\n * normalized errors pass through unchanged so the interceptor chain never\n * double-wraps.\n *\n * Both `instanceof` checks below match on SHAPE rather than class identity —\n * ours via ConnectClientError's Symbol.hasInstance, ConnectError's via the one\n * @connectrpc/connect ships — so neither branch is missed when a second copy\n * of either package lands in the bundle.\n */\nexport function normalizeError(err: unknown): ConnectClientError {\n if (err instanceof ConnectClientError) {\n return err;\n }\n\n if (err instanceof ConnectError) {\n return new ConnectClientError({\n // Strip the \"[code] \" prefix so the message is user-presentable at\n // every layer.\n message: stripServerFraming(err.rawMessage || err.message),\n code: Code[err.code]?.toLowerCase() ?? \"unknown\",\n connectCode: err.code,\n status: HTTP_STATUS[err.code] ?? 500,\n retryable: isRetryableCode(err.code),\n // Stable machine reason from backend metadata (backend owns the set).\n reason: err.metadata.get(FORGE_ERROR_REASON_HEADER) || null,\n cause: err,\n });\n }\n\n if (err instanceof Error) {\n // A network fault (fetch rejected: offline, DNS, CORS preflight, TLS)\n // surfaces as a TypeError — that, and only that, is a safe transparent\n // retry. A generic Error is NOT blanket-retried (blanket-retrying every\n // bare Error widened the blast radius to non-idempotent failures).\n const networkFault = err instanceof TypeError;\n return new ConnectClientError({\n message: stripServerFraming(err.message),\n code: networkFault ? \"unavailable\" : \"unknown\",\n status: networkFault ? 503 : 500,\n retryable: networkFault,\n reason: null,\n cause: err,\n });\n }\n\n return new ConnectClientError({\n message: typeof err === \"string\" ? stripServerFraming(err) : \"Unknown error\",\n code: \"unknown\",\n status: 500,\n retryable: false,\n reason: null,\n cause: err,\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Typed client errors. Connect surfaces failures as ConnectError with a\n// numeric Code; the rest of the app should reason about a stable, typed\n// shape instead of sniffing status codes at every call site. normalizeError\n// maps any thrown value into a ConnectClientError carrying the canonical\n// code name, an HTTP-ish status, a retryability verdict, and a\n// human-presentable message.\nimport { Code, ConnectError } from \"@connectrpc/connect\";\n\n/**\n * FORGE_ERROR_REASON_HEADER is the Connect trailer/metadata key carrying a\n * stable, machine-readable reason code (snake_case) the backend sets for\n * every error. UI/routing keys off `error.reason` instead of matching\n * human-readable message strings — the message text is display-only and\n * may change. The backend owns the setter; the frontend only reads it.\n */\nexport const FORGE_ERROR_REASON_HEADER = \"x-forge-error-reason\";\n\n/**\n * stripServerFraming removes the transport framing that must never surface\n * to a user: the leading \"[code] \" gRPC-code prefix connect-es puts on\n * `ConnectError.message`. What survives is the message the server wrote,\n * which is exactly what a user should read.\n *\n * This is the SINGLE implementation — a project never wants a different\n * answer here, and a copy frozen into a scaffold could never be corrected\n * once shipped. Idempotent: framing that has already been stripped strips\n * to itself.\n */\nexport function stripServerFraming(message: string): string {\n return message.replace(/^\\[[a-z_]+\\]\\s*/i, \"\").trim();\n}\n\n/** The copy shown when an error carries no presentable message of its own. */\nconst DEFAULT_USER_MESSAGE = \"Something went wrong. Please try again.\";\n\n/**\n * userMessage — turn any thrown value into copy fit for end users.\n *\n * `ConnectError.message` prefixes the gRPC code (\"[not_found] no such task\");\n * `rawMessage` is the server's human-readable text without that framing, and\n * stripServerFraming drops the prefix from whichever of the two is available.\n * Use this everywhere an error is SHOWN (banners, toasts) instead of\n * `err.message`.\n *\n * Pass `fallback` to override the default copy for a surface that wants its\n * own wording.\n */\nexport function userMessage(\n err: unknown,\n fallback: string = DEFAULT_USER_MESSAGE,\n): string {\n if (err instanceof ConnectError) {\n return stripServerFraming(err.rawMessage || err.message) || fallback;\n }\n if (err instanceof Error) {\n return stripServerFraming(err.message) || fallback;\n }\n return err == null ? fallback : stripServerFraming(String(err)) || fallback;\n}\n\n/** Connect codes worth a transparent transport-level retry. */\nconst RETRYABLE = new Set<Code>([\n Code.Unavailable,\n Code.DeadlineExceeded,\n Code.Aborted,\n Code.ResourceExhausted,\n]);\n\n/** Rough Connect-code → HTTP-status mapping for display/telemetry only. */\nconst HTTP_STATUS: Partial<Record<Code, number>> = {\n [Code.InvalidArgument]: 400,\n [Code.Unauthenticated]: 401,\n [Code.PermissionDenied]: 403,\n [Code.NotFound]: 404,\n [Code.AlreadyExists]: 409,\n [Code.ResourceExhausted]: 429,\n [Code.Unimplemented]: 501,\n [Code.Unavailable]: 503,\n [Code.DeadlineExceeded]: 504,\n [Code.Internal]: 500,\n};\n\n/**\n * isConnectClientErrorShape — the STRUCTURAL test behind\n * {@link ConnectClientError}'s `instanceof`.\n *\n * A class object is per module instance, not per package. npm nests a\n * transitive dependency on any version conflict, and a bundler can pull one\n * package in as both ESM and CJS, so a tree holding two copies of this\n * package holds two distinct ConnectClientError classes. Matching on shape\n * instead of identity is what makes the two agree: the shape is the\n * contract, the constructor is not.\n */\nfunction isConnectClientErrorShape(v: unknown): v is ConnectClientError {\n if (!(v instanceof Error)) return false;\n if (Object.getPrototypeOf(v) === ConnectClientError.prototype) return true;\n const e = v as Partial<ConnectClientError>;\n return (\n v.name === \"ConnectClientError\" &&\n typeof e.code === \"string\" &&\n typeof e.status === \"number\" &&\n typeof e.retryable === \"boolean\" &&\n (typeof e.reason === \"string\" || e.reason === null)\n );\n}\n\n/**\n * ConnectClientError is the single typed error shape the app catches. It is\n * a plain Error subclass so it works with React Query, error boundaries, and\n * `instanceof` alike.\n */\nexport class ConnectClientError extends Error {\n /** Canonical Connect code name, e.g. \"unavailable\", \"not_found\". */\n readonly code: string;\n /** Numeric Connect code (Code enum), or undefined for non-Connect errors. */\n readonly connectCode?: Code;\n /** Approximate HTTP status, for telemetry and generic UI. */\n readonly status: number;\n /** True when a transparent retry is reasonable (transient failures). */\n readonly retryable: boolean;\n /**\n * Stable machine-readable reason code from the backend\n * (x-forge-error-reason metadata), or null. Key UI/routing off THIS,\n * never off `message` — message text is display-only.\n */\n readonly reason: string | null;\n\n constructor(args: {\n message: string;\n code: string;\n connectCode?: Code;\n status: number;\n retryable: boolean;\n reason?: string | null;\n cause?: unknown;\n }) {\n super(args.message, { cause: args.cause });\n this.name = \"ConnectClientError\";\n this.code = args.code;\n this.connectCode = args.connectCode;\n this.status = args.status;\n this.retryable = args.retryable;\n this.reason = args.reason ?? null;\n }\n\n /**\n * `instanceof` matches on SHAPE, not on class identity — so every check\n * spelled `err instanceof ConnectClientError`, here and in app code, keeps\n * working when two copies of this package end up in one bundle (see\n * {@link isConnectClientErrorShape}). Without it, normalizeError's\n * pass-through guard misses an already-normalized error from the other\n * copy; that error's `name` is \"ConnectClientError\" rather than\n * \"ConnectError\", so it misses the ConnectError branch too and is re-wrapped\n * through the generic Error branch — silently downgrading every error in the\n * app to code \"unknown\", status 500, no connectCode and no reason.\n * @connectrpc/connect does the same on ConnectError, for the same reason.\n */\n static [Symbol.hasInstance](v: unknown): boolean {\n return isConnectClientErrorShape(v);\n }\n}\n\n/** isRetryable reports whether a Connect code merits a transport retry. */\nexport function isRetryableCode(code: Code): boolean {\n return RETRYABLE.has(code);\n}\n\n/**\n * normalizeError coerces any thrown value into a ConnectClientError. Already-\n * normalized errors pass through unchanged so the interceptor chain never\n * double-wraps.\n *\n * Both `instanceof` checks below match on SHAPE rather than class identity —\n * ours via ConnectClientError's Symbol.hasInstance, ConnectError's via the one\n * @connectrpc/connect ships — so neither branch is missed when a second copy\n * of either package lands in the bundle.\n */\nexport function normalizeError(err: unknown): ConnectClientError {\n if (err instanceof ConnectClientError) {\n return err;\n }\n\n if (err instanceof ConnectError) {\n return new ConnectClientError({\n // Strip the \"[code] \" prefix so the message is user-presentable at\n // every layer.\n message: stripServerFraming(err.rawMessage || err.message),\n code: Code[err.code]?.toLowerCase() ?? \"unknown\",\n connectCode: err.code,\n status: HTTP_STATUS[err.code] ?? 500,\n retryable: isRetryableCode(err.code),\n // Stable machine reason from backend metadata (backend owns the set).\n reason: err.metadata.get(FORGE_ERROR_REASON_HEADER) || null,\n cause: err,\n });\n }\n\n if (err instanceof Error) {\n // A network fault (fetch rejected: offline, DNS, CORS preflight, TLS)\n // surfaces as a TypeError — that, and only that, is a safe transparent\n // retry. A generic Error is NOT blanket-retried (blanket-retrying every\n // bare Error widened the blast radius to non-idempotent failures).\n const networkFault = err instanceof TypeError;\n return new ConnectClientError({\n message: stripServerFraming(err.message),\n code: networkFault ? \"unavailable\" : \"unknown\",\n status: networkFault ? 503 : 500,\n retryable: networkFault,\n reason: null,\n cause: err,\n });\n }\n\n return new ConnectClientError({\n message:\n typeof err === \"string\" ? stripServerFraming(err) : \"Unknown error\",\n code: \"unknown\",\n status: 500,\n retryable: false,\n reason: null,\n cause: err,\n });\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -29,3 +29,11 @@ export { RouteGuard, type RouteGuardProps } from "./route-guard.js";
|
|
|
29
29
|
export { Resource, type ResourceColumn, type ResourceProps, type ResourceStatus, } from "./resource.js";
|
|
30
30
|
/** Client RUM (opt-in export). */
|
|
31
31
|
export { initClientTelemetry, type TelemetryConfig, type TelemetryEvent, } from "./telemetry.js";
|
|
32
|
+
/**
|
|
33
|
+
* Dev-only browser log forwarding: mirrors console output and uncaught errors
|
|
34
|
+
* to the dev server, which prints them into
|
|
35
|
+
* `.forge/logs/<env>/frontend_<name>.log`. No-ops unless `dev: true`, and the
|
|
36
|
+
* receiving endpoint exists only in a dev server — so a production bundle
|
|
37
|
+
* neither posts nor has anywhere to post to.
|
|
38
|
+
*/
|
|
39
|
+
export { DEV_LOG_ENDPOINT, devLoggingInstalled, installDevLogging, uninstallDevLogging, type DevLoggingOptions, type DevLogPayload, } from "./devlog.js";
|
package/dist/index.js
CHANGED
|
@@ -47,4 +47,12 @@ export { RouteGuard } from "./route-guard.js";
|
|
|
47
47
|
export { Resource, } from "./resource.js";
|
|
48
48
|
/** Client RUM (opt-in export). */
|
|
49
49
|
export { initClientTelemetry, } from "./telemetry.js";
|
|
50
|
+
/**
|
|
51
|
+
* Dev-only browser log forwarding: mirrors console output and uncaught errors
|
|
52
|
+
* to the dev server, which prints them into
|
|
53
|
+
* `.forge/logs/<env>/frontend_<name>.log`. No-ops unless `dev: true`, and the
|
|
54
|
+
* receiving endpoint exists only in a dev server — so a production bundle
|
|
55
|
+
* neither posts nor has anywhere to post to.
|
|
56
|
+
*/
|
|
57
|
+
export { DEV_LOG_ENDPOINT, devLoggingInstalled, installDevLogging, uninstallDevLogging, } from "./devlog.js";
|
|
50
58
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,yEAAyE;AACzE,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,4EAA4E;AAC5E,8EAA8E;AAC9E,gEAAgE;AAChE,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,4CAA4C;AAE5C,yEAAyE;AACzE,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,sEAAsE;AAEtE,mCAAmC;AACnC,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,GAEjB,MAAM,mBAAmB,CAAC;AAE3B;;;GAGG;AACH,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,OAAO,EACL,cAAc,EACd,YAAY,EACZ,iBAAiB,GAElB,MAAM,eAAe,CAAC;AAEvB,qCAAqC;AACrC,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,qFAAqF;AACrF,OAAO,EACL,SAAS,EACT,eAAe,EACf,UAAU,GAIX,MAAM,cAAc,CAAC;AAEtB,gFAAgF;AAChF,OAAO,EAAE,YAAY,EAA0B,MAAM,gBAAgB,CAAC;AAEtE,+EAA+E;AAC/E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,yEAAyE;AACzE,OAAO,EACL,gBAAgB,GAMjB,MAAM,iBAAiB,CAAC;AAEzB,gEAAgE;AAChE,OAAO,EAAE,UAAU,EAAwB,MAAM,kBAAkB,CAAC;AAEpE,uFAAuF;AACvF,OAAO,EACL,QAAQ,GAIT,MAAM,eAAe,CAAC;AAEvB,kCAAkC;AAClC,OAAO,EACL,mBAAmB,GAGpB,MAAM,gBAAgB,CAAC","sourcesContent":["// @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Public barrel. Import batteries from \"@reliantlabs/forge-web-runtime\".\n//\n// Everything here is MECHANISM: the transport interceptor stack, the session,\n// the error boundary, the toast queue, the <Resource> tristate ladder, W3C\n// trace propagation, client RUM. Nothing here reaches into an application —\n// app-shaped values (auth state, event subscriptions, toast markup) arrive as\n// props, so an app can restyle or replace its own files freely.\n//\n// Presentation that a project is meant to own — the component library under\n// src/components/ui, globals.css, nav, layout — deliberately stays in the\n// scaffold and is NOT part of this package.\n\n// The per-group docs below are JSDoc, not `//`, on purpose: TypeScript's\n// declaration emit preserves JSDoc and DROPS line comments, and dist/ is now\n// the whole of what this package publishes. `forge project libraries` reads\n// this inventory out of the published barrel, and a consumer's editor shows\n// it on hover — both would get symbols with no explanation otherwise.\n\n/** Transport interceptor stack. */\nexport {\n authInterceptor,\n buildRuntimeInterceptors,\n errorNormalizeInterceptor,\n headerInterceptor,\n retryInterceptor,\n traceInterceptor,\n type RuntimeInterceptorConfig,\n} from \"./interceptors.js\";\n\n/**\n * Typed client errors, and the backend-framing strippers every surface that\n * SHOWS an error goes through.\n */\nexport {\n ConnectClientError,\n FORGE_ERROR_REASON_HEADER,\n isRetryableCode,\n normalizeError,\n stripServerFraming,\n userMessage,\n} from \"./errors.js\";\n\n/**\n * Mount-prefix helpers, for the URLs Next.js's router never sees — absolute\n * URLs handed to external systems that round-trip back into the app.\n *\n * Barrel-exported rather than behind a subpath: three string functions with\n * no imports, so there is no dependency footprint for a subpath to fence off.\n */\nexport {\n createBasePath,\n joinBasePath,\n normalizeBasePath,\n type BasePathHelpers,\n} from \"./basepath.js\";\n\n/** W3C trace-context propagation. */\nexport {\n freshTraceparent,\n injectTraceContext,\n setTraceSampled,\n TRACEPARENT_RE,\n} from \"./trace.js\";\n\n/** Session: the signed-in user, derived from the app's auth state and JWT claims. */\nexport {\n decodeJwt,\n SessionProvider,\n useSession,\n type JwtClaims,\n type Session,\n type SessionAuth,\n} from \"./session.js\";\n\n/** The app-shell provider set: session + error boundary + toast host in one. */\nexport { RuntimeShell, type RuntimeShellProps } from \"./providers.js\";\n\n/** Error boundary with a designed crash fallback, for a route or a subtree. */\nexport {\n DefaultErrorFallback,\n RuntimeErrorBoundary,\n} from \"./error-boundary.js\";\n\n/** The toast queue. The app supplies the event source and the markup. */\nexport {\n RuntimeToastHost,\n type RuntimeToast,\n type RuntimeToastHostProps,\n type RuntimeToastPayload,\n type RuntimeToastSink,\n type RuntimeToastVariant,\n} from \"./toast-host.js\";\n\n/** Route guarding: render a page only to a signed-in caller. */\nexport { RouteGuard, type RouteGuardProps } from \"./route-guard.js\";\n\n/** Generic data-table container: the tristate ladder, filter and cursor pagination. */\nexport {\n Resource,\n type ResourceColumn,\n type ResourceProps,\n type ResourceStatus,\n} from \"./resource.js\";\n\n/** Client RUM (opt-in export). */\nexport {\n initClientTelemetry,\n type TelemetryConfig,\n type TelemetryEvent,\n} from \"./telemetry.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,yEAAyE;AACzE,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,4EAA4E;AAC5E,8EAA8E;AAC9E,gEAAgE;AAChE,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,4CAA4C;AAE5C,yEAAyE;AACzE,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,sEAAsE;AAEtE,mCAAmC;AACnC,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,GAEjB,MAAM,mBAAmB,CAAC;AAE3B;;;GAGG;AACH,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,OAAO,EACL,cAAc,EACd,YAAY,EACZ,iBAAiB,GAElB,MAAM,eAAe,CAAC;AAEvB,qCAAqC;AACrC,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,qFAAqF;AACrF,OAAO,EACL,SAAS,EACT,eAAe,EACf,UAAU,GAIX,MAAM,cAAc,CAAC;AAEtB,gFAAgF;AAChF,OAAO,EAAE,YAAY,EAA0B,MAAM,gBAAgB,CAAC;AAEtE,+EAA+E;AAC/E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,yEAAyE;AACzE,OAAO,EACL,gBAAgB,GAMjB,MAAM,iBAAiB,CAAC;AAEzB,gEAAgE;AAChE,OAAO,EAAE,UAAU,EAAwB,MAAM,kBAAkB,CAAC;AAEpE,uFAAuF;AACvF,OAAO,EACL,QAAQ,GAIT,MAAM,eAAe,CAAC;AAEvB,kCAAkC;AAClC,OAAO,EACL,mBAAmB,GAGpB,MAAM,gBAAgB,CAAC;AAExB;;;;;;GAMG;AACH,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,GAGpB,MAAM,aAAa,CAAC","sourcesContent":["// @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Public barrel. Import batteries from \"@reliantlabs/forge-web-runtime\".\n//\n// Everything here is MECHANISM: the transport interceptor stack, the session,\n// the error boundary, the toast queue, the <Resource> tristate ladder, W3C\n// trace propagation, client RUM. Nothing here reaches into an application —\n// app-shaped values (auth state, event subscriptions, toast markup) arrive as\n// props, so an app can restyle or replace its own files freely.\n//\n// Presentation that a project is meant to own — the component library under\n// src/components/ui, globals.css, nav, layout — deliberately stays in the\n// scaffold and is NOT part of this package.\n\n// The per-group docs below are JSDoc, not `//`, on purpose: TypeScript's\n// declaration emit preserves JSDoc and DROPS line comments, and dist/ is now\n// the whole of what this package publishes. `forge project libraries` reads\n// this inventory out of the published barrel, and a consumer's editor shows\n// it on hover — both would get symbols with no explanation otherwise.\n\n/** Transport interceptor stack. */\nexport {\n authInterceptor,\n buildRuntimeInterceptors,\n errorNormalizeInterceptor,\n headerInterceptor,\n retryInterceptor,\n traceInterceptor,\n type RuntimeInterceptorConfig,\n} from \"./interceptors.js\";\n\n/**\n * Typed client errors, and the backend-framing strippers every surface that\n * SHOWS an error goes through.\n */\nexport {\n ConnectClientError,\n FORGE_ERROR_REASON_HEADER,\n isRetryableCode,\n normalizeError,\n stripServerFraming,\n userMessage,\n} from \"./errors.js\";\n\n/**\n * Mount-prefix helpers, for the URLs Next.js's router never sees — absolute\n * URLs handed to external systems that round-trip back into the app.\n *\n * Barrel-exported rather than behind a subpath: three string functions with\n * no imports, so there is no dependency footprint for a subpath to fence off.\n */\nexport {\n createBasePath,\n joinBasePath,\n normalizeBasePath,\n type BasePathHelpers,\n} from \"./basepath.js\";\n\n/** W3C trace-context propagation. */\nexport {\n freshTraceparent,\n injectTraceContext,\n setTraceSampled,\n TRACEPARENT_RE,\n} from \"./trace.js\";\n\n/** Session: the signed-in user, derived from the app's auth state and JWT claims. */\nexport {\n decodeJwt,\n SessionProvider,\n useSession,\n type JwtClaims,\n type Session,\n type SessionAuth,\n} from \"./session.js\";\n\n/** The app-shell provider set: session + error boundary + toast host in one. */\nexport { RuntimeShell, type RuntimeShellProps } from \"./providers.js\";\n\n/** Error boundary with a designed crash fallback, for a route or a subtree. */\nexport {\n DefaultErrorFallback,\n RuntimeErrorBoundary,\n} from \"./error-boundary.js\";\n\n/** The toast queue. The app supplies the event source and the markup. */\nexport {\n RuntimeToastHost,\n type RuntimeToast,\n type RuntimeToastHostProps,\n type RuntimeToastPayload,\n type RuntimeToastSink,\n type RuntimeToastVariant,\n} from \"./toast-host.js\";\n\n/** Route guarding: render a page only to a signed-in caller. */\nexport { RouteGuard, type RouteGuardProps } from \"./route-guard.js\";\n\n/** Generic data-table container: the tristate ladder, filter and cursor pagination. */\nexport {\n Resource,\n type ResourceColumn,\n type ResourceProps,\n type ResourceStatus,\n} from \"./resource.js\";\n\n/** Client RUM (opt-in export). */\nexport {\n initClientTelemetry,\n type TelemetryConfig,\n type TelemetryEvent,\n} from \"./telemetry.js\";\n\n/**\n * Dev-only browser log forwarding: mirrors console output and uncaught errors\n * to the dev server, which prints them into\n * `.forge/logs/<env>/frontend_<name>.log`. No-ops unless `dev: true`, and the\n * receiving endpoint exists only in a dev server — so a production bundle\n * neither posts nor has anywhere to post to.\n */\nexport {\n DEV_LOG_ENDPOINT,\n devLoggingInstalled,\n installDevLogging,\n uninstallDevLogging,\n type DevLoggingOptions,\n type DevLogPayload,\n} from \"./devlog.js\";\n"]}
|
package/dist/interceptors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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\";\n\nimport { ConnectClientError, normalizeError } from \"./errors.js\";\nimport { injectTraceContext } from \"./trace.js\";\n\nimport type { Interceptor, UnaryRequest, StreamRequest } 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 */\nexport function traceInterceptor(): Interceptor {\n return (next) => async (req) => {\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 const normalized = normalizeError(err);\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 ConnectClientError,\n FORGE_ERROR_REASON_HEADER,\n isRetryableCode,\n normalizeError,\n} from \"./errors.js\";\n\nexport {\n freshTraceparent,\n injectTraceContext,\n setTraceSampled,\n TRACEPARENT_RE,\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\";\n\nimport { ConnectClientError, normalizeError } from \"./errors.js\";\nimport { injectTraceContext } 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 */\nexport function traceInterceptor(): Interceptor {\n return (next) => async (req) => {\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 const normalized = normalizeError(err);\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 ConnectClientError,\n FORGE_ERROR_REASON_HEADER,\n isRetryableCode,\n normalizeError,\n} from \"./errors.js\";\n\nexport {\n freshTraceparent,\n injectTraceContext,\n setTraceSampled,\n TRACEPARENT_RE,\n} from \"./trace.js\";\n"]}
|
package/dist/mock-transport.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// and this engine out entirely, which is why the generated `connect.ts`
|
|
27
27
|
// reaches its own shim through a dynamic `require()` gated on the mock-mode
|
|
28
28
|
// env var.
|
|
29
|
-
import { create } from "@bufbuild/protobuf";
|
|
29
|
+
import { create, } from "@bufbuild/protobuf";
|
|
30
30
|
import { Code, ConnectError, } from "@connectrpc/connect";
|
|
31
31
|
/**
|
|
32
32
|
* Mutable session stores, keyed by descriptor identity.
|
|
@@ -1 +1 @@
|
|
|
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// 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// 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 { create, type DescMessage, type MessageInitShape } 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/** One CRUD arm of an entity's dispatch table. */\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(\n value: unknown,\n): 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/** A fixture-backed handler for one RPC key. */\ntype FixtureHandler = (input: unknown) => unknown;\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 { rpc, responseSchema, itemsField } = entity.list;\n table.set(key(rpc), () =>\n create(responseSchema, {\n [itemsField]: Array.from(storeFor(entity).values()),\n } as MessageInitShape<DescMessage>),\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 return makeUnaryResponse(method, fixture(input));\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// 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// 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 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/** One CRUD arm of an entity's dispatch table. */\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/** A fixture-backed handler for one RPC key. */\ntype FixtureHandler = (input: unknown) => unknown;\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 { rpc, responseSchema, itemsField } = entity.list;\n table.set(key(rpc), () =>\n create(responseSchema, {\n [itemsField]: Array.from(storeFor(entity).values()),\n } as MessageInitShape<DescMessage>),\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 return makeUnaryResponse(method, fixture(input));\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"]}
|
package/dist/resource.js
CHANGED
|
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
// { status, data, error } and hand it here. Pagination is
|
|
12
12
|
// cursor-based (page_token / next_page_token in Connect list RPCs): the page
|
|
13
13
|
// owns the cursor stack and passes onNextPage/onPrevPage.
|
|
14
|
-
import { useEffect, useState
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
15
|
export function Resource({ status, data, columns, rowKey, title, actions, error, onRetry, onRowClick, emptyTitle = "Nothing here yet", emptyMessage = "No records match.", filter, onFilterChange, filterPlaceholder = "Search…", filterDebounceMs = 300, onNextPage, onPrevPage, hasNextPage, hasPrevPage, isFetching, }) {
|
|
16
16
|
const [query, setQuery] = useState(filter ?? "");
|
|
17
17
|
// Keep local input in sync when the parent resets the filter externally.
|
|
@@ -29,7 +29,7 @@ export function Resource({ status, data, columns, rowKey, title, actions, error,
|
|
|
29
29
|
const showFooter = Boolean(onNextPage || onPrevPage);
|
|
30
30
|
const colCount = columns.length;
|
|
31
31
|
const captionText = title ?? "Results";
|
|
32
|
-
return (_jsxs("section", { className: "flex flex-col gap-4", children: [(title || actions) && (_jsxs("header", { className: "flex items-center justify-between gap-4", children: [title ? (_jsx("h1", { className: "text-xl font-semibold text-ink", children: title })) : (_jsx("span", {})), actions ? _jsx("div", { className: "flex items-center gap-2", children: actions }) : null] })), onFilterChange ? (_jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: filterPlaceholder, "aria-label": title ? `Search ${title.toLowerCase()}` : "Search", className: "w-full max-w-sm rounded-md border border-border bg-surface px-3 py-2 text-sm text-ink shadow-sm focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent" })) : null, _jsx("div", { className: "overflow-x-auto rounded-lg border border-border bg-surface", children: _jsxs("table", { className: "min-w-full divide-y divide-border text-sm", children: [_jsx("caption", { className: "sr-only", children: captionText }), _jsx("thead", { className: "bg-surface-muted", children: _jsx("tr", { children: columns.map((col, i) => (_jsx("th", { scope: "col", className: `px-4 py-2.5 text-left font-medium text-ink-muted ${col.className ?? ""}`, children: col.header }, i))) }) }), _jsx("tbody", { className: "divide-y divide-border", children: status === "loading" ? (_jsx(SkeletonRows, { rows: 5, cols: colCount })) : status === "error" ? (_jsx("tr", { children: _jsx("td", { colSpan: colCount, className: "px-4 py-10", children: _jsx(ErrorState, { error: error, onRetry: onRetry }) }) })) : !data || data.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: colCount, className: "px-4 py-12", children: _jsx(EmptyState, { title: emptyTitle, message: emptyMessage }) }) })) : (data.map((row) => (_jsx("tr", { onClick: onRowClick ? () => onRowClick(row) : undefined,
|
|
32
|
+
return (_jsxs("section", { className: "flex flex-col gap-4", children: [(title || actions) && (_jsxs("header", { className: "flex items-center justify-between gap-4", children: [title ? (_jsx("h1", { className: "text-xl font-semibold text-ink", children: title })) : (_jsx("span", {})), actions ? (_jsx("div", { className: "flex items-center gap-2", children: actions })) : null] })), onFilterChange ? (_jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: filterPlaceholder, "aria-label": title ? `Search ${title.toLowerCase()}` : "Search", className: "w-full max-w-sm rounded-md border border-border bg-surface px-3 py-2 text-sm text-ink shadow-sm focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent" })) : null, _jsx("div", { className: "overflow-x-auto rounded-lg border border-border bg-surface", children: _jsxs("table", { className: "min-w-full divide-y divide-border text-sm", children: [_jsx("caption", { className: "sr-only", children: captionText }), _jsx("thead", { className: "bg-surface-muted", children: _jsx("tr", { children: columns.map((col, i) => (_jsx("th", { scope: "col", className: `px-4 py-2.5 text-left font-medium text-ink-muted ${col.className ?? ""}`, children: col.header }, i))) }) }), _jsx("tbody", { className: "divide-y divide-border", children: status === "loading" ? (_jsx(SkeletonRows, { rows: 5, cols: colCount })) : status === "error" ? (_jsx("tr", { children: _jsx("td", { colSpan: colCount, className: "px-4 py-10", children: _jsx(ErrorState, { error: error, onRetry: onRetry }) }) })) : !data || data.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: colCount, className: "px-4 py-12", children: _jsx(EmptyState, { title: emptyTitle, message: emptyMessage }) }) })) : (data.map((row) => (_jsx("tr", { onClick: onRowClick ? () => onRowClick(row) : undefined,
|
|
33
33
|
// Keyboard-operable when the row navigates: Enter/Space
|
|
34
34
|
// activate it, and it takes focus in tab order.
|
|
35
35
|
role: onRowClick ? "button" : undefined, tabIndex: onRowClick ? 0 : undefined, onKeyDown: onRowClick
|
package/dist/resource.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// <Resource> — the owned-once data-table container. It encapsulates the\n// loading / error / empty / data tristate ladder, a server-side (debounced)\n// filter, and cursor pagination, so list pages stop re-hand-rolling the\n// ladder and stop client-side-filtering a single page cap.\n//\n// Pairs with the app's useQueryResource hook: map a query result to\n// { status, data, error } and hand it here. Pagination is\n// cursor-based (page_token / next_page_token in Connect list RPCs): the page\n// owns the cursor stack and passes onNextPage/onPrevPage.\nimport {\n useEffect,\n useState,\n type ReactElement,\n type ReactNode,\n} from \"react\";\n\nexport interface ResourceColumn<T> {\n header: string;\n cell: (row: T) => ReactNode;\n /** Extra classes for the cell + header (alignment, width). */\n className?: string;\n}\n\nexport type ResourceStatus = \"loading\" | \"error\" | \"success\";\n\nexport interface ResourceProps<T> {\n status: ResourceStatus;\n data: T[] | undefined;\n columns: ResourceColumn<T>[];\n rowKey: (row: T) => string;\n\n title?: string;\n /** Header-right slot, e.g. a \"New\" button. */\n actions?: ReactNode;\n error?: Error | null;\n onRetry?: () => void;\n onRowClick?: (row: T) => void;\n\n emptyTitle?: string;\n emptyMessage?: string;\n\n // Server-side filter (debounced). Omit onFilterChange to hide the search box.\n filter?: string;\n onFilterChange?: (value: string) => void;\n filterPlaceholder?: string;\n filterDebounceMs?: number;\n\n // Cursor pagination. Omit the handlers to hide the footer.\n onNextPage?: () => void;\n onPrevPage?: () => void;\n hasNextPage?: boolean;\n hasPrevPage?: boolean;\n /** True during a background refetch (page change / filter change). */\n isFetching?: boolean;\n}\n\nexport function Resource<T>({\n status,\n data,\n columns,\n rowKey,\n title,\n actions,\n error,\n onRetry,\n onRowClick,\n emptyTitle = \"Nothing here yet\",\n emptyMessage = \"No records match.\",\n filter,\n onFilterChange,\n filterPlaceholder = \"Search…\",\n filterDebounceMs = 300,\n onNextPage,\n onPrevPage,\n hasNextPage,\n hasPrevPage,\n isFetching,\n}: ResourceProps<T>): ReactElement {\n const [query, setQuery] = useState(filter ?? \"\");\n\n // Keep local input in sync when the parent resets the filter externally.\n useEffect(() => {\n setQuery(filter ?? \"\");\n }, [filter]);\n\n // Debounce filter changes so each keystroke doesn't fire an RPC.\n useEffect(() => {\n if (!onFilterChange || query === (filter ?? \"\")) {\n return;\n }\n const handle = setTimeout(() => onFilterChange(query), filterDebounceMs);\n return () => clearTimeout(handle);\n }, [query, filter, onFilterChange, filterDebounceMs]);\n\n const showFooter = Boolean(onNextPage || onPrevPage);\n const colCount = columns.length;\n const captionText = title ?? \"Results\";\n\n return (\n <section className=\"flex flex-col gap-4\">\n {(title || actions) && (\n <header className=\"flex items-center justify-between gap-4\">\n {title ? (\n <h1 className=\"text-xl font-semibold text-ink\">{title}</h1>\n ) : (\n <span />\n )}\n {actions ? <div className=\"flex items-center gap-2\">{actions}</div> : null}\n </header>\n )}\n\n {onFilterChange ? (\n <input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={filterPlaceholder}\n aria-label={title ? `Search ${title.toLowerCase()}` : \"Search\"}\n className=\"w-full max-w-sm rounded-md border border-border bg-surface px-3 py-2 text-sm text-ink shadow-sm focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent\"\n />\n ) : null}\n\n <div className=\"overflow-x-auto rounded-lg border border-border bg-surface\">\n <table className=\"min-w-full divide-y divide-border text-sm\">\n <caption className=\"sr-only\">{captionText}</caption>\n <thead className=\"bg-surface-muted\">\n <tr>\n {columns.map((col, i) => (\n <th\n key={i}\n scope=\"col\"\n className={`px-4 py-2.5 text-left font-medium text-ink-muted ${col.className ?? \"\"}`}\n >\n {col.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {status === \"loading\" ? (\n <SkeletonRows rows={5} cols={colCount} />\n ) : status === \"error\" ? (\n <tr>\n <td colSpan={colCount} className=\"px-4 py-10\">\n <ErrorState error={error} onRetry={onRetry} />\n </td>\n </tr>\n ) : !data || data.length === 0 ? (\n <tr>\n <td colSpan={colCount} className=\"px-4 py-12\">\n <EmptyState title={emptyTitle} message={emptyMessage} />\n </td>\n </tr>\n ) : (\n data.map((row) => (\n <tr\n key={rowKey(row)}\n onClick={onRowClick ? () => onRowClick(row) : undefined}\n // Keyboard-operable when the row navigates: Enter/Space\n // activate it, and it takes focus in tab order.\n role={onRowClick ? \"button\" : undefined}\n tabIndex={onRowClick ? 0 : undefined}\n onKeyDown={\n onRowClick\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onRowClick(row);\n }\n }\n : undefined\n }\n className={\n onRowClick\n ? \"cursor-pointer transition-colors hover:bg-surface-muted focus:bg-surface-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent\"\n : undefined\n }\n >\n {columns.map((col, i) => (\n <td\n key={i}\n className={`px-4 py-2.5 text-ink ${col.className ?? \"\"}`}\n >\n {col.cell(row)}\n </td>\n ))}\n </tr>\n ))\n )}\n </tbody>\n </table>\n </div>\n\n {showFooter ? (\n <footer className=\"flex items-center justify-end gap-2\">\n {isFetching ? (\n <span className=\"mr-auto text-xs text-ink-subtle\">Loading…</span>\n ) : null}\n <button\n type=\"button\"\n onClick={onPrevPage}\n disabled={!hasPrevPage}\n className=\"rounded-md border border-border px-3 py-1.5 text-sm font-medium text-ink-muted hover:bg-surface-muted disabled:cursor-not-allowed disabled:opacity-40\"\n >\n Previous\n </button>\n <button\n type=\"button\"\n onClick={onNextPage}\n disabled={!hasNextPage}\n className=\"rounded-md border border-border px-3 py-1.5 text-sm font-medium text-ink-muted hover:bg-surface-muted disabled:cursor-not-allowed disabled:opacity-40\"\n >\n Next\n </button>\n </footer>\n ) : null}\n </section>\n );\n}\n\n// SkeletonRows is the loading rung of the tristate ladder: a shimmer the\n// width of the table while the first page is in flight.\n//\n// It is ONE row with a colSpan cell, matching the error and empty rungs\n// above — not `rows` × `cols` real table cells. That shape is what makes it\n// accessible, and the reasoning is worth keeping:\n//\n// - The shimmer is pure decoration. It carries no data, so it is wrapped\n// in aria-hidden and the cell carries one sr-only \"Loading…\" instead.\n// Assistive tech hears the state once, not once per placeholder.\n// - A grid of empty <td>s announced as a table of blank cells, and read\n// as a real 5-row result set by anything counting rows. The placeholder\n// geometry is now plain <div>s inside the single cell, so the table has\n// exactly the structure it claims to have at every moment.\n// - Empty cells also tripped jsx-a11y/control-has-associated-label under\n// the eslint config forge scaffolds. That was the symptom that surfaced\n// this; the row-shape mismatch above is the actual defect.\n//\n// aria-hidden goes on the inner <div>, never on the <td> or <tr>: jsx-a11y\n// treats table elements as focusable, so hiding one is\n// no-aria-hidden-on-focusable — correctly, since a hidden focusable node is\n// a focus trap for screen-reader users.\nfunction SkeletonRows({ rows, cols }: { rows: number; cols: number }) {\n return (\n <tr>\n <td colSpan={cols} className=\"p-0\">\n <span className=\"sr-only\">Loading…</span>\n <div aria-hidden=\"true\" className=\"divide-y divide-border\">\n {Array.from({ length: rows }).map((_, r) => (\n <div key={r} className=\"flex items-center gap-4 px-4 py-3\">\n {Array.from({ length: cols }).map((__, c) => (\n <div\n key={c}\n className=\"h-4 flex-1 animate-pulse rounded bg-surface-muted\"\n />\n ))}\n </div>\n ))}\n </div>\n </td>\n </tr>\n );\n}\n\nfunction ErrorState({\n error,\n onRetry,\n}: {\n error?: Error | null;\n onRetry?: () => void;\n}) {\n return (\n <div role=\"alert\" className=\"flex flex-col items-center gap-2 text-center\">\n <p className=\"text-sm font-medium text-danger-ink\">Couldn't load data</p>\n {error?.message ? (\n <p className=\"max-w-md font-mono text-xs text-danger\">{error.message}</p>\n ) : null}\n {onRetry ? (\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"mt-1 rounded-md bg-danger px-3 py-1.5 text-sm font-medium text-on-danger hover:bg-danger-hover\"\n >\n Try again\n </button>\n ) : null}\n </div>\n );\n}\n\nfunction EmptyState({ title, message }: { title: string; message: string }) {\n return (\n <div className=\"flex flex-col items-center gap-1 text-center\">\n <p className=\"text-sm font-medium text-ink\">{title}</p>\n <p className=\"text-sm text-ink-muted\">{message}</p>\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// <Resource> — the owned-once data-table container. It encapsulates the\n// loading / error / empty / data tristate ladder, a server-side (debounced)\n// filter, and cursor pagination, so list pages stop re-hand-rolling the\n// ladder and stop client-side-filtering a single page cap.\n//\n// Pairs with the app's useQueryResource hook: map a query result to\n// { status, data, error } and hand it here. Pagination is\n// cursor-based (page_token / next_page_token in Connect list RPCs): the page\n// owns the cursor stack and passes onNextPage/onPrevPage.\nimport { useEffect, useState, type ReactElement, type ReactNode } from \"react\";\n\nexport interface ResourceColumn<T> {\n header: string;\n cell: (row: T) => ReactNode;\n /** Extra classes for the cell + header (alignment, width). */\n className?: string;\n}\n\nexport type ResourceStatus = \"loading\" | \"error\" | \"success\";\n\nexport interface ResourceProps<T> {\n status: ResourceStatus;\n data: T[] | undefined;\n columns: ResourceColumn<T>[];\n rowKey: (row: T) => string;\n\n title?: string;\n /** Header-right slot, e.g. a \"New\" button. */\n actions?: ReactNode;\n error?: Error | null;\n onRetry?: () => void;\n onRowClick?: (row: T) => void;\n\n emptyTitle?: string;\n emptyMessage?: string;\n\n // Server-side filter (debounced). Omit onFilterChange to hide the search box.\n filter?: string;\n onFilterChange?: (value: string) => void;\n filterPlaceholder?: string;\n filterDebounceMs?: number;\n\n // Cursor pagination. Omit the handlers to hide the footer.\n onNextPage?: () => void;\n onPrevPage?: () => void;\n hasNextPage?: boolean;\n hasPrevPage?: boolean;\n /** True during a background refetch (page change / filter change). */\n isFetching?: boolean;\n}\n\nexport function Resource<T>({\n status,\n data,\n columns,\n rowKey,\n title,\n actions,\n error,\n onRetry,\n onRowClick,\n emptyTitle = \"Nothing here yet\",\n emptyMessage = \"No records match.\",\n filter,\n onFilterChange,\n filterPlaceholder = \"Search…\",\n filterDebounceMs = 300,\n onNextPage,\n onPrevPage,\n hasNextPage,\n hasPrevPage,\n isFetching,\n}: ResourceProps<T>): ReactElement {\n const [query, setQuery] = useState(filter ?? \"\");\n\n // Keep local input in sync when the parent resets the filter externally.\n useEffect(() => {\n setQuery(filter ?? \"\");\n }, [filter]);\n\n // Debounce filter changes so each keystroke doesn't fire an RPC.\n useEffect(() => {\n if (!onFilterChange || query === (filter ?? \"\")) {\n return;\n }\n const handle = setTimeout(() => onFilterChange(query), filterDebounceMs);\n return () => clearTimeout(handle);\n }, [query, filter, onFilterChange, filterDebounceMs]);\n\n const showFooter = Boolean(onNextPage || onPrevPage);\n const colCount = columns.length;\n const captionText = title ?? \"Results\";\n\n return (\n <section className=\"flex flex-col gap-4\">\n {(title || actions) && (\n <header className=\"flex items-center justify-between gap-4\">\n {title ? (\n <h1 className=\"text-xl font-semibold text-ink\">{title}</h1>\n ) : (\n <span />\n )}\n {actions ? (\n <div className=\"flex items-center gap-2\">{actions}</div>\n ) : null}\n </header>\n )}\n\n {onFilterChange ? (\n <input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={filterPlaceholder}\n aria-label={title ? `Search ${title.toLowerCase()}` : \"Search\"}\n className=\"w-full max-w-sm rounded-md border border-border bg-surface px-3 py-2 text-sm text-ink shadow-sm focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent\"\n />\n ) : null}\n\n <div className=\"overflow-x-auto rounded-lg border border-border bg-surface\">\n <table className=\"min-w-full divide-y divide-border text-sm\">\n <caption className=\"sr-only\">{captionText}</caption>\n <thead className=\"bg-surface-muted\">\n <tr>\n {columns.map((col, i) => (\n <th\n key={i}\n scope=\"col\"\n className={`px-4 py-2.5 text-left font-medium text-ink-muted ${\n col.className ?? \"\"\n }`}\n >\n {col.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {status === \"loading\" ? (\n <SkeletonRows rows={5} cols={colCount} />\n ) : status === \"error\" ? (\n <tr>\n <td colSpan={colCount} className=\"px-4 py-10\">\n <ErrorState error={error} onRetry={onRetry} />\n </td>\n </tr>\n ) : !data || data.length === 0 ? (\n <tr>\n <td colSpan={colCount} className=\"px-4 py-12\">\n <EmptyState title={emptyTitle} message={emptyMessage} />\n </td>\n </tr>\n ) : (\n data.map((row) => (\n <tr\n key={rowKey(row)}\n onClick={onRowClick ? () => onRowClick(row) : undefined}\n // Keyboard-operable when the row navigates: Enter/Space\n // activate it, and it takes focus in tab order.\n role={onRowClick ? \"button\" : undefined}\n tabIndex={onRowClick ? 0 : undefined}\n onKeyDown={\n onRowClick\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onRowClick(row);\n }\n }\n : undefined\n }\n className={\n onRowClick\n ? \"cursor-pointer transition-colors hover:bg-surface-muted focus:bg-surface-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent\"\n : undefined\n }\n >\n {columns.map((col, i) => (\n <td\n key={i}\n className={`px-4 py-2.5 text-ink ${col.className ?? \"\"}`}\n >\n {col.cell(row)}\n </td>\n ))}\n </tr>\n ))\n )}\n </tbody>\n </table>\n </div>\n\n {showFooter ? (\n <footer className=\"flex items-center justify-end gap-2\">\n {isFetching ? (\n <span className=\"mr-auto text-xs text-ink-subtle\">Loading…</span>\n ) : null}\n <button\n type=\"button\"\n onClick={onPrevPage}\n disabled={!hasPrevPage}\n className=\"rounded-md border border-border px-3 py-1.5 text-sm font-medium text-ink-muted hover:bg-surface-muted disabled:cursor-not-allowed disabled:opacity-40\"\n >\n Previous\n </button>\n <button\n type=\"button\"\n onClick={onNextPage}\n disabled={!hasNextPage}\n className=\"rounded-md border border-border px-3 py-1.5 text-sm font-medium text-ink-muted hover:bg-surface-muted disabled:cursor-not-allowed disabled:opacity-40\"\n >\n Next\n </button>\n </footer>\n ) : null}\n </section>\n );\n}\n\n// SkeletonRows is the loading rung of the tristate ladder: a shimmer the\n// width of the table while the first page is in flight.\n//\n// It is ONE row with a colSpan cell, matching the error and empty rungs\n// above — not `rows` × `cols` real table cells. That shape is what makes it\n// accessible, and the reasoning is worth keeping:\n//\n// - The shimmer is pure decoration. It carries no data, so it is wrapped\n// in aria-hidden and the cell carries one sr-only \"Loading…\" instead.\n// Assistive tech hears the state once, not once per placeholder.\n// - A grid of empty <td>s announced as a table of blank cells, and read\n// as a real 5-row result set by anything counting rows. The placeholder\n// geometry is now plain <div>s inside the single cell, so the table has\n// exactly the structure it claims to have at every moment.\n// - Empty cells also tripped jsx-a11y/control-has-associated-label under\n// the eslint config forge scaffolds. That was the symptom that surfaced\n// this; the row-shape mismatch above is the actual defect.\n//\n// aria-hidden goes on the inner <div>, never on the <td> or <tr>: jsx-a11y\n// treats table elements as focusable, so hiding one is\n// no-aria-hidden-on-focusable — correctly, since a hidden focusable node is\n// a focus trap for screen-reader users.\nfunction SkeletonRows({ rows, cols }: { rows: number; cols: number }) {\n return (\n <tr>\n <td colSpan={cols} className=\"p-0\">\n <span className=\"sr-only\">Loading…</span>\n <div aria-hidden=\"true\" className=\"divide-y divide-border\">\n {Array.from({ length: rows }).map((_, r) => (\n <div key={r} className=\"flex items-center gap-4 px-4 py-3\">\n {Array.from({ length: cols }).map((__, c) => (\n <div\n key={c}\n className=\"h-4 flex-1 animate-pulse rounded bg-surface-muted\"\n />\n ))}\n </div>\n ))}\n </div>\n </td>\n </tr>\n );\n}\n\nfunction ErrorState({\n error,\n onRetry,\n}: {\n error?: Error | null;\n onRetry?: () => void;\n}) {\n return (\n <div role=\"alert\" className=\"flex flex-col items-center gap-2 text-center\">\n <p className=\"text-sm font-medium text-danger-ink\">\n Couldn't load data\n </p>\n {error?.message ? (\n <p className=\"max-w-md font-mono text-xs text-danger\">\n {error.message}\n </p>\n ) : null}\n {onRetry ? (\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"mt-1 rounded-md bg-danger px-3 py-1.5 text-sm font-medium text-on-danger hover:bg-danger-hover\"\n >\n Try again\n </button>\n ) : null}\n </div>\n );\n}\n\nfunction EmptyState({ title, message }: { title: string; message: string }) {\n return (\n <div className=\"flex flex-col items-center gap-1 text-center\">\n <p className=\"text-sm font-medium text-ink\">{title}</p>\n <p className=\"text-sm text-ink-muted\">{message}</p>\n </div>\n );\n}\n"]}
|
package/dist/route-guard.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-guard.js","sourceRoot":"","sources":["../src/route-guard.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,sEAAsE;AACtE,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,+EAA+E;AAC/E,+EAA+E;AAC/E,sBAAsB;AACtB,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAY1C,MAAM,UAAU,UAAU,CAAC,EACzB,QAAQ,EACR,OAAO,EACP,QAAQ,GACQ;IAChB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAE7B,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QACtB,OAAO,4BAAG,OAAO,IAAI,IAAI,GAAI,CAAC;IAChC,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC;QAC7B,OAAO,CACL,4BACG,QAAQ,IAAI,CACX,eAAK,SAAS,EAAC,2EAA2E,aACxF,YAAG,SAAS,EAAC,8BAA8B,8BAAkB,EAC7D,YAAG,SAAS,EAAC,wBAAwB,
|
|
1
|
+
{"version":3,"file":"route-guard.js","sourceRoot":"","sources":["../src/route-guard.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,sEAAsE;AACtE,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,+EAA+E;AAC/E,+EAA+E;AAC/E,sBAAsB;AACtB,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAY1C,MAAM,UAAU,UAAU,CAAC,EACzB,QAAQ,EACR,OAAO,EACP,QAAQ,GACQ;IAChB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAE7B,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QACtB,OAAO,4BAAG,OAAO,IAAI,IAAI,GAAI,CAAC;IAChC,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC;QAC7B,OAAO,CACL,4BACG,QAAQ,IAAI,CACX,eAAK,SAAS,EAAC,2EAA2E,aACxF,YAAG,SAAS,EAAC,8BAA8B,8BAAkB,EAC7D,YAAG,SAAS,EAAC,wBAAwB,2CAA+B,IAChE,CACP,GACA,CACJ,CAAC;IACJ,CAAC;IACD,OAAO,4BAAG,QAAQ,GAAI,CAAC;AACzB,CAAC","sourcesContent":["\"use client\";\n\n// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Sign-in gating for a page or section, driven by the session forge already\n// derives from the JWT. This gates RENDERING only — the backend remains the\n// source of truth; a determined client can always call the RPC, and it will be\n// rejected server-side. Wrap a page/section in <RouteGuard> to show it only to\n// a signed-in caller.\nimport { useSession } from \"./session.js\";\n\nimport type { ReactElement, ReactNode } from \"react\";\n\nexport interface RouteGuardProps {\n children: ReactNode;\n /** Shown while the session is still loading. */\n loading?: ReactNode;\n /** Shown when nobody is signed in. Defaults to a simple notice. */\n fallback?: ReactNode;\n}\n\nexport function RouteGuard({\n children,\n loading,\n fallback,\n}: RouteGuardProps): ReactElement {\n const session = useSession();\n\n if (session.isLoading) {\n return <>{loading ?? null}</>;\n }\n if (!session.isAuthenticated) {\n return (\n <>\n {fallback ?? (\n <div className=\"flex min-h-[12rem] flex-col items-center justify-center gap-1 text-center\">\n <p className=\"text-sm font-medium text-ink\">Not signed in</p>\n <p className=\"text-sm text-ink-muted\">Sign in to view this page.</p>\n </div>\n )}\n </>\n );\n }\n return <>{children}</>;\n}\n"]}
|
package/dist/service-hooks.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.
|
|
2
3
|
//
|
|
4
|
+
// Every export below is a React hook or builds one, so this module belongs in
|
|
5
|
+
// the CLIENT graph. Without the directive the Next.js App Router compiles it
|
|
6
|
+
// into the SERVER graph, where it binds a SECOND @tanstack/react-query
|
|
7
|
+
// instance and every generated hook throws "No QueryClient set" at runtime —
|
|
8
|
+
// invisible to tsc, eslint AND `next build`. published-surface.test.ts asserts
|
|
9
|
+
// the directive survives into dist/. It is NOT applied package-wide: the
|
|
10
|
+
// ./interceptors subpath exists to be importable without React.
|
|
11
|
+
//
|
|
3
12
|
// The React Query wrapper machinery behind forge's generated per-service hook
|
|
4
13
|
// files (`src/hooks/<svc>-service-hooks_gen.ts`).
|
|
5
14
|
//
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-hooks.js","sourceRoot":"","sources":["../src/service-hooks.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,8EAA8E;AAC9E,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,qEAAqE;AACrE,qEAAqE;AACrE,+EAA+E;AAC/E,4EAA4E;AAC5E,8EAA8E;AAC9E,sEAAsE;AACtE,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,sEAAsE;AACtE,wEAAwE;AACxE,6EAA6E;AAC7E,4EAA4E;AAC5E,mCAAmC;AACnC,EAAE;AACF,wEAAwE;AACxE,8EAA8E;AAC9E,uDAAuD;AACvD,yEAAyE;AACzE,8EAA8E;AAC9E,8EAA8E;AAC9E,OAAO,EACL,MAAM,EACN,MAAM,GAGP,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,WAAW,EACX,QAAQ,EACR,cAAc,GAKf,MAAM,uBAAuB,CAAC;AAW/B;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CACxB,MAAc,EACd,GAA6B;IAE7B,OAAO,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;AAC7C,CAAC;AAoBD;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,OAAO;QACL,GAAG,EAAE,CAAC,OAAO,CAAU;QACvB,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,CAAU;QAClD,KAAK,EACH,CACE,MAAc,EACd,MAAc,EACd,WAAoB,EACpB,EAAE,CACJ,CAAC,GAA6B,EAAmB,EAAE,CACjD,WAAW,KAAK,SAAS;YACvB,CAAC,CAAE,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAW;YACvD,CAAC,CAAE,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAW;KAC3E,CAAC;AACJ,CAAC;AAeD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAC7B,MAA0D,EAC1D,IAA0D;IAE1D,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CACtB,QAAQ,CAAC;QACP,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC;QACrB,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC;QACxB,GAAG,OAAO;KACX,CAAC,CAAC;AACP,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,kBAAkB,CAChC,aAA8B,EAC9B,IAA0D;IAE1D,OAAO,CAAC,OAAO,EAAE,EAAE;QACjB,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;QACrC,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,IAAI,EAAE,CAAC;QAC7C,OAAO,WAAW,CAAC;YACjB,UAAU,EAAE,CAAC,GAA6B,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC;YACxD,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,EAAE;gBACrB,WAAW,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC;gBAC3D,SAAS,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;YACvB,CAAC;YACD,GAAG,IAAI;SACR,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// The React Query wrapper machinery behind forge's generated per-service hook\n// files (`src/hooks/<svc>-service-hooks_gen.ts`).\n//\n// WHAT WAS HERE BEFORE. Each generated file re-emitted the same three things\n// per RPC: a query-key entry, a useQuery or useMutation wrapper, and — for\n// mutations — the entity-scoped invalidateQueries plus the onSuccess\n// composition, with the reasoning for each re-inlined as comments. A\n// five-service project rendered five copies of machinery that differed only in\n// RPC names and message types. This module IS that machinery, written once.\n// The generated file keeps exactly what varies per RPC: the name, the request\n// schema, the response type, query-vs-mutation, and the entity scope.\n//\n// WHY A SUBPATH AND NOT THE BARREL. This module imports @tanstack/react-query.\n// Every browser frontend forge scaffolds depends on it — but the barrel is\n// also what a consumer imports for interceptors and errors alone, and\n// anchoring React Query there would make a non-React consumer resolve a\n// package it never installed. Same reasoning as ./mock-transport and ./otel:\n// an engine with its own dependency footprint gets its own entry point, and\n// React Query is an OPTIONAL peer.\n//\n// WHAT IT DELIBERATELY DOES NOT DO. It does not emit error toasts — the\n// app-wide MutationCache in src/lib/query-client.ts is the single error-toast\n// chokepoint, and pages that render errors inline pass\n// `meta: { silenceErrorToast: true }`. It never defaults success-side UX\n// either: \"CreateOrder succeeded\" leaks RPC names into the UI, so the toast /\n// redirect / inline confirmation stays the call site's via options.onSuccess.\nimport {\n create,\n toJson,\n type DescMessage,\n type MessageInitShape,\n} from \"@bufbuild/protobuf\";\nimport {\n useMutation,\n useQuery,\n useQueryClient,\n type UseMutationOptions,\n type UseMutationResult,\n type UseQueryOptions,\n type UseQueryResult,\n} from \"@tanstack/react-query\";\n\nimport type { ConnectClientError } from \"./errors.js\";\n\n/**\n * A generated query key. The tuple is [service, entity?, method,\n * protojson(req)] for a method key and [service] / [service, entity] for the\n * broader scopes.\n */\nexport type ServiceQueryKey = readonly unknown[];\n\n/**\n * requestKey projects a request init into protojson for use inside a query\n * key. Two reasons this is not the raw request object:\n *\n * - React Query's default hasher is JSON.stringify, which THROWS on bigint\n * fields (any proto int64) — the raw message crashes the cache.\n * - protojson normalizes representations, so { id: 1 } and { id: \"1\" } land\n * in ONE cache entry instead of silently splitting the cache.\n *\n * Exported because a hand-written hook that wants to share a generated hook's\n * cache entry has to hash its request the identical way.\n */\nexport function requestKey<Schema extends DescMessage>(\n schema: Schema,\n req: MessageInitShape<Schema>,\n): unknown {\n return toJson(schema, create(schema, req));\n}\n\n/** The three key-scope builders a generated key factory is assembled from. */\nexport interface ServiceKeyBuilders {\n /** Every query on this service: [service]. */\n readonly all: readonly [string];\n /** Every query for one CRUD entity: [service, entity]. Mutations invalidate this. */\n entity(name: string): readonly [string, string];\n /**\n * One query: [service, entity?, method, protojson(req)]. Returns the\n * per-request key FUNCTION, which is what both the query hook and any\n * call-site prefetch/setQueryData use.\n */\n query<Schema extends DescMessage>(\n method: string,\n schema: Schema,\n entityScope?: string,\n ): (req: MessageInitShape<Schema>) => ServiceQueryKey;\n}\n\n/**\n * serviceKeys returns the builders for one service's query-key factory.\n *\n * Cache-key semantics are a compatibility contract, not an implementation\n * detail: an existing app's\n * `invalidateQueries({ queryKey: orderServiceKeys.order })` has to keep\n * matching the keys its queries were registered under across a version bump.\n * So the tuple order and contents produced here are fixed, and identical to\n * what forge previously inlined into every generated hook file.\n */\nexport function serviceKeys(service: string): ServiceKeyBuilders {\n return {\n all: [service] as const,\n entity: (name: string) => [service, name] as const,\n query:\n <Schema extends DescMessage>(\n method: string,\n schema: Schema,\n entityScope?: string,\n ) =>\n (req: MessageInitShape<Schema>): ServiceQueryKey =>\n entityScope === undefined\n ? ([service, method, requestKey(schema, req)] as const)\n : ([service, entityScope, method, requestKey(schema, req)] as const),\n };\n}\n\n/** A generated query hook: `useGetOrder(req, options?)`. */\nexport type ServiceQueryHook<Schema extends DescMessage, Response> = (\n req: MessageInitShape<Schema>,\n options?: Partial<UseQueryOptions<Response, ConnectClientError>>,\n) => UseQueryResult<Response, ConnectClientError>;\n\n/** A generated mutation hook: `useCreateOrder(options?)`. */\nexport type ServiceMutationHook<Schema extends DescMessage, Response> = (\n options?: Partial<\n UseMutationOptions<Response, ConnectClientError, MessageInitShape<Schema>>\n >,\n) => UseMutationResult<Response, ConnectClientError, MessageInitShape<Schema>>;\n\n/**\n * createQueryHook builds one useQuery wrapper.\n *\n * The signature it returns is the one call sites already use — `(req,\n * options?)`, with the error typed as ConnectClientError rather than React\n * Query's default `Error`. That type is declared on the OPTIONS parameter\n * because React Query takes the result's error type from the same TError it\n * reads there, so `query.error` is a ConnectClientError at every call site\n * with nothing to pass. Typing it as `Error` hid `.reason`, `.code`, `.status`\n * and `.retryable`, and left `err.message.includes(...)` as the only\n * expression that compiled — the one thing the error contract forbids.\n */\nexport function createQueryHook<Schema extends DescMessage, Response>(\n keyFor: (req: MessageInitShape<Schema>) => ServiceQueryKey,\n call: (req: MessageInitShape<Schema>) => Promise<Response>,\n): ServiceQueryHook<Schema, Response> {\n return (req, options) =>\n useQuery({\n queryKey: keyFor(req),\n queryFn: () => call(req),\n ...options,\n });\n}\n\n/**\n * createMutationHook builds one useMutation wrapper with scoped invalidation.\n *\n * invalidateKey is the entity scope when the RPC maps to a CRUD entity, and\n * the whole-service scope otherwise — a bespoke mutation may touch anything,\n * so over-invalidating is the safe default there.\n *\n * The invalidation is composed BEFORE the caller's onSuccess and is never\n * replaced by it: onSuccess is destructured out and re-invoked after the\n * invalidate, while the rest of the options object is spread separately. A\n * caller passing onSuccess therefore ADDS behaviour, rather than silently\n * disabling the cache refresh — which is exactly what a plain `...options`\n * spread over a default onSuccess would do.\n */\nexport function createMutationHook<Schema extends DescMessage, Response>(\n invalidateKey: ServiceQueryKey,\n call: (req: MessageInitShape<Schema>) => Promise<Response>,\n): ServiceMutationHook<Schema, Response> {\n return (options) => {\n const queryClient = useQueryClient();\n const { onSuccess, ...rest } = options ?? {};\n return useMutation({\n mutationFn: (req: MessageInitShape<Schema>) => call(req),\n onSuccess: (...args) => {\n queryClient.invalidateQueries({ queryKey: invalidateKey });\n onSuccess?.(...args);\n },\n ...rest,\n });\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"service-hooks.js","sourceRoot":"","sources":["../src/service-hooks.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,sEAAsE;AACtE,EAAE;AACF,8EAA8E;AAC9E,6EAA6E;AAC7E,uEAAuE;AACvE,6EAA6E;AAC7E,+EAA+E;AAC/E,yEAAyE;AACzE,gEAAgE;AAChE,EAAE;AACF,8EAA8E;AAC9E,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,qEAAqE;AACrE,qEAAqE;AACrE,+EAA+E;AAC/E,4EAA4E;AAC5E,8EAA8E;AAC9E,sEAAsE;AACtE,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,sEAAsE;AACtE,wEAAwE;AACxE,6EAA6E;AAC7E,4EAA4E;AAC5E,mCAAmC;AACnC,EAAE;AACF,wEAAwE;AACxE,8EAA8E;AAC9E,uDAAuD;AACvD,yEAAyE;AACzE,8EAA8E;AAC9E,8EAA8E;AAC9E,OAAO,EACL,MAAM,EACN,MAAM,GAGP,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,WAAW,EACX,QAAQ,EACR,cAAc,GAKf,MAAM,uBAAuB,CAAC;AAW/B;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CACxB,MAAc,EACd,GAA6B;IAE7B,OAAO,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;AAC7C,CAAC;AAoBD;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,OAAO;QACL,GAAG,EAAE,CAAC,OAAO,CAAU;QACvB,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,CAAU;QAClD,KAAK,EACH,CACE,MAAc,EACd,MAAc,EACd,WAAoB,EACpB,EAAE,CACJ,CAAC,GAA6B,EAAmB,EAAE,CACjD,WAAW,KAAK,SAAS;YACvB,CAAC,CAAE,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAW;YACvD,CAAC,CAAE,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAW;KAC3E,CAAC;AACJ,CAAC;AAeD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAC7B,MAA0D,EAC1D,IAA0D;IAE1D,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CACtB,QAAQ,CAAC;QACP,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC;QACrB,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC;QACxB,GAAG,OAAO;KACX,CAAC,CAAC;AACP,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,kBAAkB,CAChC,aAA8B,EAC9B,IAA0D;IAE1D,OAAO,CAAC,OAAO,EAAE,EAAE;QACjB,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;QACrC,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,IAAI,EAAE,CAAC;QAC7C,OAAO,WAAW,CAAC;YACjB,UAAU,EAAE,CAAC,GAA6B,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC;YACxD,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,EAAE;gBACrB,WAAW,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC;gBAC3D,SAAS,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;YACvB,CAAC;YACD,GAAG,IAAI;SACR,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\n// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Every export below is a React hook or builds one, so this module belongs in\n// the CLIENT graph. Without the directive the Next.js App Router compiles it\n// into the SERVER graph, where it binds a SECOND @tanstack/react-query\n// instance and every generated hook throws \"No QueryClient set\" at runtime —\n// invisible to tsc, eslint AND `next build`. published-surface.test.ts asserts\n// the directive survives into dist/. It is NOT applied package-wide: the\n// ./interceptors subpath exists to be importable without React.\n//\n// The React Query wrapper machinery behind forge's generated per-service hook\n// files (`src/hooks/<svc>-service-hooks_gen.ts`).\n//\n// WHAT WAS HERE BEFORE. Each generated file re-emitted the same three things\n// per RPC: a query-key entry, a useQuery or useMutation wrapper, and — for\n// mutations — the entity-scoped invalidateQueries plus the onSuccess\n// composition, with the reasoning for each re-inlined as comments. A\n// five-service project rendered five copies of machinery that differed only in\n// RPC names and message types. This module IS that machinery, written once.\n// The generated file keeps exactly what varies per RPC: the name, the request\n// schema, the response type, query-vs-mutation, and the entity scope.\n//\n// WHY A SUBPATH AND NOT THE BARREL. This module imports @tanstack/react-query.\n// Every browser frontend forge scaffolds depends on it — but the barrel is\n// also what a consumer imports for interceptors and errors alone, and\n// anchoring React Query there would make a non-React consumer resolve a\n// package it never installed. Same reasoning as ./mock-transport and ./otel:\n// an engine with its own dependency footprint gets its own entry point, and\n// React Query is an OPTIONAL peer.\n//\n// WHAT IT DELIBERATELY DOES NOT DO. It does not emit error toasts — the\n// app-wide MutationCache in src/lib/query-client.ts is the single error-toast\n// chokepoint, and pages that render errors inline pass\n// `meta: { silenceErrorToast: true }`. It never defaults success-side UX\n// either: \"CreateOrder succeeded\" leaks RPC names into the UI, so the toast /\n// redirect / inline confirmation stays the call site's via options.onSuccess.\nimport {\n create,\n toJson,\n type DescMessage,\n type MessageInitShape,\n} from \"@bufbuild/protobuf\";\nimport {\n useMutation,\n useQuery,\n useQueryClient,\n type UseMutationOptions,\n type UseMutationResult,\n type UseQueryOptions,\n type UseQueryResult,\n} from \"@tanstack/react-query\";\n\nimport type { ConnectClientError } from \"./errors.js\";\n\n/**\n * A generated query key. The tuple is [service, entity?, method,\n * protojson(req)] for a method key and [service] / [service, entity] for the\n * broader scopes.\n */\nexport type ServiceQueryKey = readonly unknown[];\n\n/**\n * requestKey projects a request init into protojson for use inside a query\n * key. Two reasons this is not the raw request object:\n *\n * - React Query's default hasher is JSON.stringify, which THROWS on bigint\n * fields (any proto int64) — the raw message crashes the cache.\n * - protojson normalizes representations, so { id: 1 } and { id: \"1\" } land\n * in ONE cache entry instead of silently splitting the cache.\n *\n * Exported because a hand-written hook that wants to share a generated hook's\n * cache entry has to hash its request the identical way.\n */\nexport function requestKey<Schema extends DescMessage>(\n schema: Schema,\n req: MessageInitShape<Schema>,\n): unknown {\n return toJson(schema, create(schema, req));\n}\n\n/** The three key-scope builders a generated key factory is assembled from. */\nexport interface ServiceKeyBuilders {\n /** Every query on this service: [service]. */\n readonly all: readonly [string];\n /** Every query for one CRUD entity: [service, entity]. Mutations invalidate this. */\n entity(name: string): readonly [string, string];\n /**\n * One query: [service, entity?, method, protojson(req)]. Returns the\n * per-request key FUNCTION, which is what both the query hook and any\n * call-site prefetch/setQueryData use.\n */\n query<Schema extends DescMessage>(\n method: string,\n schema: Schema,\n entityScope?: string,\n ): (req: MessageInitShape<Schema>) => ServiceQueryKey;\n}\n\n/**\n * serviceKeys returns the builders for one service's query-key factory.\n *\n * Cache-key semantics are a compatibility contract, not an implementation\n * detail: an existing app's\n * `invalidateQueries({ queryKey: orderServiceKeys.order })` has to keep\n * matching the keys its queries were registered under across a version bump.\n * So the tuple order and contents produced here are fixed, and identical to\n * what forge previously inlined into every generated hook file.\n */\nexport function serviceKeys(service: string): ServiceKeyBuilders {\n return {\n all: [service] as const,\n entity: (name: string) => [service, name] as const,\n query:\n <Schema extends DescMessage>(\n method: string,\n schema: Schema,\n entityScope?: string,\n ) =>\n (req: MessageInitShape<Schema>): ServiceQueryKey =>\n entityScope === undefined\n ? ([service, method, requestKey(schema, req)] as const)\n : ([service, entityScope, method, requestKey(schema, req)] as const),\n };\n}\n\n/** A generated query hook: `useGetOrder(req, options?)`. */\nexport type ServiceQueryHook<Schema extends DescMessage, Response> = (\n req: MessageInitShape<Schema>,\n options?: Partial<UseQueryOptions<Response, ConnectClientError>>,\n) => UseQueryResult<Response, ConnectClientError>;\n\n/** A generated mutation hook: `useCreateOrder(options?)`. */\nexport type ServiceMutationHook<Schema extends DescMessage, Response> = (\n options?: Partial<\n UseMutationOptions<Response, ConnectClientError, MessageInitShape<Schema>>\n >,\n) => UseMutationResult<Response, ConnectClientError, MessageInitShape<Schema>>;\n\n/**\n * createQueryHook builds one useQuery wrapper.\n *\n * The signature it returns is the one call sites already use — `(req,\n * options?)`, with the error typed as ConnectClientError rather than React\n * Query's default `Error`. That type is declared on the OPTIONS parameter\n * because React Query takes the result's error type from the same TError it\n * reads there, so `query.error` is a ConnectClientError at every call site\n * with nothing to pass. Typing it as `Error` hid `.reason`, `.code`, `.status`\n * and `.retryable`, and left `err.message.includes(...)` as the only\n * expression that compiled — the one thing the error contract forbids.\n */\nexport function createQueryHook<Schema extends DescMessage, Response>(\n keyFor: (req: MessageInitShape<Schema>) => ServiceQueryKey,\n call: (req: MessageInitShape<Schema>) => Promise<Response>,\n): ServiceQueryHook<Schema, Response> {\n return (req, options) =>\n useQuery({\n queryKey: keyFor(req),\n queryFn: () => call(req),\n ...options,\n });\n}\n\n/**\n * createMutationHook builds one useMutation wrapper with scoped invalidation.\n *\n * invalidateKey is the entity scope when the RPC maps to a CRUD entity, and\n * the whole-service scope otherwise — a bespoke mutation may touch anything,\n * so over-invalidating is the safe default there.\n *\n * The invalidation is composed BEFORE the caller's onSuccess and is never\n * replaced by it: onSuccess is destructured out and re-invoked after the\n * invalidate, while the rest of the options object is spread separately. A\n * caller passing onSuccess therefore ADDS behaviour, rather than silently\n * disabling the cache refresh — which is exactly what a plain `...options`\n * spread over a default onSuccess would do.\n */\nexport function createMutationHook<Schema extends DescMessage, Response>(\n invalidateKey: ServiceQueryKey,\n call: (req: MessageInitShape<Schema>) => Promise<Response>,\n): ServiceMutationHook<Schema, Response> {\n return (options) => {\n const queryClient = useQueryClient();\n const { onSuccess, ...rest } = options ?? {};\n return useMutation({\n mutationFn: (req: MessageInitShape<Schema>) => call(req),\n onSuccess: (...args) => {\n queryClient.invalidateQueries({ queryKey: invalidateKey });\n onSuccess?.(...args);\n },\n ...rest,\n });\n };\n}\n"]}
|