@reliantlabs/forge-web-runtime 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -13,6 +13,15 @@ export { ConnectClientError, FORGE_ERROR_REASON_HEADER, isRetryableCode, normali
13
13
  * no imports, so there is no dependency footprint for a subpath to fence off.
14
14
  */
15
15
  export { createBasePath, joinBasePath, normalizeBasePath, type BasePathHelpers, } from "./basepath.js";
16
+ /**
17
+ * Money and basis points as integers: format minor units through Intl, and
18
+ * parse form input back to minor units without rounding.
19
+ *
20
+ * The web counterpart of forge/pkg/money — an int64 count of the currency's
21
+ * minor unit plus an ISO 4217 code, exact past 2^53. Barrel-exported for the
22
+ * same reason as the base-path helpers: it imports nothing.
23
+ */
24
+ export { basisPointsToInput, currencyMinorDigits, formatBasisPoints, formatMinorUnits, minorUnitsToInput, parseBasisPoints, parseMinorUnits, type BasisPointsOptions, type MoneyOptions, type ParseBasisPointsOptions, type ParseMoneyOptions, } from "./money.js";
16
25
  /** W3C trace-context propagation. */
17
26
  export { freshTraceparent, injectTraceContext, setTraceSampled, TRACEPARENT_RE, } from "./trace.js";
18
27
  /** Session: the signed-in user, derived from the app's auth state and JWT claims. */
@@ -27,8 +36,12 @@ export { RuntimeToastHost, type RuntimeToast, type RuntimeToastHostProps, type R
27
36
  export { RouteGuard, type RouteGuardProps } from "./route-guard.js";
28
37
  /** Generic data-table container: the tristate ladder, filter and cursor pagination. */
29
38
  export { Resource, type ResourceColumn, type ResourceProps, type ResourceStatus, } from "./resource.js";
30
- /** Client RUM (opt-in export). */
31
- export { initClientTelemetry, type TelemetryConfig, type TelemetryEvent, } from "./telemetry.js";
39
+ /**
40
+ * Record a caught error with the browser telemetry SDK, when one is running.
41
+ * A no-op otherwise, so it is safe in any catch block. The SDK itself arrives
42
+ * through the "/telemetry" subpath, never the barrel.
43
+ */
44
+ export { reportException, setExceptionReporter, type ExceptionAttributes, } from "./error-reporter.js";
32
45
  /**
33
46
  * Dev-only browser log forwarding: mirrors console output and uncaught errors
34
47
  * to the dev server, which prints them into
package/dist/index.js CHANGED
@@ -31,6 +31,15 @@ export { ConnectClientError, FORGE_ERROR_REASON_HEADER, isRetryableCode, normali
31
31
  * no imports, so there is no dependency footprint for a subpath to fence off.
32
32
  */
33
33
  export { createBasePath, joinBasePath, normalizeBasePath, } from "./basepath.js";
34
+ /**
35
+ * Money and basis points as integers: format minor units through Intl, and
36
+ * parse form input back to minor units without rounding.
37
+ *
38
+ * The web counterpart of forge/pkg/money — an int64 count of the currency's
39
+ * minor unit plus an ISO 4217 code, exact past 2^53. Barrel-exported for the
40
+ * same reason as the base-path helpers: it imports nothing.
41
+ */
42
+ export { basisPointsToInput, currencyMinorDigits, formatBasisPoints, formatMinorUnits, minorUnitsToInput, parseBasisPoints, parseMinorUnits, } from "./money.js";
34
43
  /** W3C trace-context propagation. */
35
44
  export { freshTraceparent, injectTraceContext, setTraceSampled, TRACEPARENT_RE, } from "./trace.js";
36
45
  /** Session: the signed-in user, derived from the app's auth state and JWT claims. */
@@ -45,8 +54,12 @@ export { RuntimeToastHost, } from "./toast-host.js";
45
54
  export { RouteGuard } from "./route-guard.js";
46
55
  /** Generic data-table container: the tristate ladder, filter and cursor pagination. */
47
56
  export { Resource, } from "./resource.js";
48
- /** Client RUM (opt-in export). */
49
- export { initClientTelemetry, } from "./telemetry.js";
57
+ /**
58
+ * Record a caught error with the browser telemetry SDK, when one is running.
59
+ * A no-op otherwise, so it is safe in any catch block. The SDK itself arrives
60
+ * through the "/telemetry" subpath, never the barrel.
61
+ */
62
+ export { reportException, setExceptionReporter, } from "./error-reporter.js";
50
63
  /**
51
64
  * Dev-only browser log forwarding: mirrors console output and uncaught errors
52
65
  * to the dev server, which prints them into
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;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"]}
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;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAKhB,MAAM,YAAY,CAAC;AAEpB,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;;;;GAIG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,GAErB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;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/**\n * Money and basis points as integers: format minor units through Intl, and\n * parse form input back to minor units without rounding.\n *\n * The web counterpart of forge/pkg/money — an int64 count of the currency's\n * minor unit plus an ISO 4217 code, exact past 2^53. Barrel-exported for the\n * same reason as the base-path helpers: it imports nothing.\n */\nexport {\n basisPointsToInput,\n currencyMinorDigits,\n formatBasisPoints,\n formatMinorUnits,\n minorUnitsToInput,\n parseBasisPoints,\n parseMinorUnits,\n type BasisPointsOptions,\n type MoneyOptions,\n type ParseBasisPointsOptions,\n type ParseMoneyOptions,\n} from \"./money.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/**\n * Record a caught error with the browser telemetry SDK, when one is running.\n * A no-op otherwise, so it is safe in any catch block. The SDK itself arrives\n * through the \"/telemetry\" subpath, never the barrel.\n */\nexport {\n reportException,\n setExceptionReporter,\n type ExceptionAttributes,\n} from \"./error-reporter.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"]}
@@ -20,7 +20,14 @@ export interface MockScenarioRegistry {
20
20
  byName(name: string): MockScenario | undefined;
21
21
  defaultScenario: MockScenario;
22
22
  }
23
- /** One CRUD arm of an entity's dispatch table. */
23
+ /**
24
+ * One CRUD arm of an entity's dispatch table.
25
+ *
26
+ * The List arm needs nothing beyond these three fields to honour filters,
27
+ * paging and total_count: the REQUEST schema arrives on every call as the
28
+ * Connect method's `input`, and the entity schema is the element type of
29
+ * `itemsField` on `responseSchema`.
30
+ */
24
31
  export interface MockListRpc {
25
32
  rpc: string;
26
33
  responseSchema: DescMessage;
@@ -21,12 +21,18 @@
21
21
  // 4. Anything else rejects with Code.Unimplemented so callers see a
22
22
  // clear error instead of a silent empty response.
23
23
  //
24
+ // A fixture List answers the way the backend's generated CRUD List does
25
+ // (pkg/crud HandleList), so a filtered table, a paged table and a "how many"
26
+ // tile read the same against fixtures as against a server — see
27
+ // `serveList` for the exact contract. Custom (non-CRUD) RPCs are not
28
+ // interpreted at all; they stay hand-written scenario handlers.
29
+ //
24
30
  // Imported through the "@reliantlabs/forge-web-runtime/mock-transport" subpath,
25
31
  // never the barrel: a production bundle must be able to shake the fixtures
26
32
  // and this engine out entirely, which is why the generated `connect.ts`
27
33
  // reaches its own shim through a dynamic `require()` gated on the mock-mode
28
34
  // env var.
29
- import { create, } from "@bufbuild/protobuf";
35
+ import { create, ScalarType, } from "@bufbuild/protobuf";
30
36
  import { Code, ConnectError, } from "@connectrpc/connect";
31
37
  /**
32
38
  * Mutable session stores, keyed by descriptor identity.
@@ -117,6 +123,191 @@ function makeUnaryResponse(method, message) {
117
123
  trailer: new Headers(),
118
124
  };
119
125
  }
126
+ // ── List semantics ─────────────────────────────────────────────────────
127
+ //
128
+ // What the backend's generated List does, and therefore what the fixture
129
+ // List does (pkg/crud HandleList + the generated Filters closure):
130
+ //
131
+ // - Every `optional` request field that is SET filters by equality on the
132
+ // entity field of the same name. Enums compare by value. A field with
133
+ // IMPLICIT presence (no `optional`) is never a filter — the backend
134
+ // generates none for it, so the mock must not invent one.
135
+ // - `search` matches case-insensitively against the entity's string
136
+ // fields and enum value names (enum columns are stored as names).
137
+ // - `order_by` (snake_case column, comma-separated, optional ASC/DESC per
138
+ // column) plus `descending` sorts; an unknown column is InvalidArgument.
139
+ // - `page_size` defaults to 50 and is clamped to 100. `total_count` is the
140
+ // filtered count BEFORE paging. `next_page_token` is minted only for the
141
+ // default (primary-key) order — an ordered list is single-page on the
142
+ // backend, and a mock that paged it would hide that.
143
+ //
144
+ // Paging, ordering and search arrive through these request fields, by the
145
+ // names forge's CRUD List requests declare. They are never equality filters.
146
+ const LIST_CONTROL_FIELDS = new Set([
147
+ "pageSize",
148
+ "pageToken",
149
+ "orderBy",
150
+ "descending",
151
+ "search",
152
+ ]);
153
+ /** pkg/crud ListOp's defaults: 0 means 50, and no page exceeds 100. */
154
+ const DEFAULT_PAGE_SIZE = 50;
155
+ const MAX_PAGE_SIZE = 100;
156
+ /** FeatureSet_FieldPresence.EXPLICIT — proto3 `optional`, oneofs, messages. */
157
+ const EXPLICIT_PRESENCE = 1;
158
+ /** The entity message a List response repeats, read off its descriptor. */
159
+ function listItemSchema(list) {
160
+ const items = list.responseSchema.field[list.itemsField];
161
+ return items?.fieldKind === "list" && items.listKind === "message"
162
+ ? items.message
163
+ : undefined;
164
+ }
165
+ /** Scalars and enums compare by value; messages, bytes, lists and maps do not. */
166
+ function isComparable(field) {
167
+ if (field.fieldKind === "enum")
168
+ return true;
169
+ return field.fieldKind === "scalar" && field.scalar !== ScalarType.BYTES;
170
+ }
171
+ function sameValue(a, b) {
172
+ if (a === b)
173
+ return true;
174
+ // A 64-bit field is a bigint — or a string under jstype=JS_STRING — and
175
+ // the two sides are not guaranteed to agree on which.
176
+ if (a == null || b == null)
177
+ return false;
178
+ return ((typeof a === "bigint" || typeof b === "bigint") && String(a) === String(b));
179
+ }
180
+ function enumName(field, value) {
181
+ return field.fieldKind === "enum"
182
+ ? field.enum.values.find((v) => v.number === value)?.name
183
+ : undefined;
184
+ }
185
+ /** The value a column sorts by, as Postgres would order the stored column. */
186
+ function sortKey(field, value) {
187
+ if (value == null)
188
+ return undefined;
189
+ // Enum columns hold the value NAME, so they sort alphabetically by name.
190
+ if (field.fieldKind === "enum")
191
+ return enumName(field, value) ?? "";
192
+ if (field.fieldKind === "scalar")
193
+ return value;
194
+ if (field.fieldKind === "message" &&
195
+ field.message.typeName === "google.protobuf.Timestamp") {
196
+ const ts = value;
197
+ return BigInt(ts.seconds) * 1000000000n + BigInt(ts.nanos);
198
+ }
199
+ return undefined;
200
+ }
201
+ /** Postgres's default: NULL sorts after every value ascending. */
202
+ function compareKeys(a, b) {
203
+ if (a === undefined || b === undefined) {
204
+ return a === b ? 0 : a === undefined ? 1 : -1;
205
+ }
206
+ const x = a;
207
+ const y = b;
208
+ return x < y ? -1 : x > y ? 1 : 0;
209
+ }
210
+ function invalidArgument(message) {
211
+ return new ConnectError(`mock-transport: ${message}`, Code.InvalidArgument);
212
+ }
213
+ /** Parse `order_by` the way orm.ValidateOrderBy reads it. */
214
+ function parseOrderBy(clause, descending, itemSchema) {
215
+ return clause.split(",").map((part) => {
216
+ const [column = "", direction] = part.trim().split(/\s+/);
217
+ const field = itemSchema.fields.find((f) => f.name === column || f.localName === column);
218
+ if (!field) {
219
+ throw invalidArgument(`unknown order-by column "${column}"`);
220
+ }
221
+ const dir = direction?.toUpperCase();
222
+ if (dir !== undefined && dir !== "ASC" && dir !== "DESC") {
223
+ throw invalidArgument(`invalid order-by direction "${direction}"`);
224
+ }
225
+ return { field, desc: dir === undefined ? descending : dir === "DESC" };
226
+ });
227
+ }
228
+ /** A count in whatever JS type the response's total_count field takes. */
229
+ function countValue(field, n) {
230
+ if (field.fieldKind !== "scalar")
231
+ return n;
232
+ switch (field.scalar) {
233
+ case ScalarType.INT64:
234
+ case ScalarType.UINT64:
235
+ case ScalarType.SINT64:
236
+ case ScalarType.FIXED64:
237
+ case ScalarType.SFIXED64:
238
+ return field.longAsString ? String(n) : BigInt(n);
239
+ default:
240
+ return n;
241
+ }
242
+ }
243
+ /**
244
+ * Answer one List call from the entity's rows. See the List semantics note
245
+ * above for the contract; the engine's tests pin each clause.
246
+ */
247
+ function serveList(rows, list, input, requestSchema) {
248
+ const req = (input ?? {});
249
+ const itemSchema = listItemSchema(list);
250
+ let matched = rows;
251
+ if (itemSchema && requestSchema) {
252
+ for (const filter of requestSchema.fields) {
253
+ if (LIST_CONTROL_FIELDS.has(filter.localName))
254
+ continue;
255
+ if (filter.presence !== EXPLICIT_PRESENCE || !isComparable(filter)) {
256
+ continue;
257
+ }
258
+ const want = req[filter.localName];
259
+ if (want == null)
260
+ continue; // unset: no filter
261
+ const target = itemSchema.field[filter.localName];
262
+ if (!target || !isComparable(target))
263
+ continue;
264
+ matched = matched.filter((row) => sameValue(row[filter.localName], want));
265
+ }
266
+ }
267
+ const search = typeof req.search === "string" ? req.search.trim().toLowerCase() : "";
268
+ if (itemSchema && search) {
269
+ const searchable = itemSchema.fields.filter((f) => f.fieldKind === "enum" ||
270
+ (f.fieldKind === "scalar" && f.scalar === ScalarType.STRING));
271
+ matched = matched.filter((row) => searchable.some((f) => {
272
+ const value = row[f.localName];
273
+ const text = f.fieldKind === "enum" ? enumName(f, value) : value;
274
+ return typeof text === "string" && text.toLowerCase().includes(search);
275
+ }));
276
+ }
277
+ const orderBy = typeof req.orderBy === "string" ? req.orderBy.trim() : "";
278
+ if (itemSchema && orderBy) {
279
+ const keys = parseOrderBy(orderBy, req.descending === true, itemSchema);
280
+ matched = [...matched].sort((a, b) => {
281
+ for (const { field, desc } of keys) {
282
+ const c = compareKeys(sortKey(field, a[field.localName]), sortKey(field, b[field.localName]));
283
+ if (c !== 0)
284
+ return desc ? -c : c;
285
+ }
286
+ return 0;
287
+ });
288
+ }
289
+ const total = matched.length;
290
+ let offset = 0;
291
+ const token = typeof req.pageToken === "string" ? req.pageToken : "";
292
+ if (token) {
293
+ // The mock's own cursor: an offset into the filtered rows. Opaque to the
294
+ // client, exactly like the backend's keyset cursor.
295
+ if (!/^\d+$/.test(token))
296
+ throw invalidArgument("invalid page token");
297
+ offset = Number(token);
298
+ }
299
+ const requested = Number(req.pageSize ?? 0);
300
+ const pageSize = requested > 0 ? Math.min(requested, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE;
301
+ const end = offset + pageSize;
302
+ const init = { [list.itemsField]: matched.slice(offset, end) };
303
+ const totalField = list.responseSchema.field.totalCount;
304
+ if (totalField)
305
+ init.totalCount = countValue(totalField, total);
306
+ if (list.responseSchema.field.nextPageToken && !orderBy && end < total) {
307
+ init.nextPageToken = String(end);
308
+ }
309
+ return create(list.responseSchema, init);
310
+ }
120
311
  /**
121
312
  * Compile the entity descriptors into a flat `${service}/${rpc}` → handler
122
313
  * map. Building it once per transport keeps per-RPC dispatch to a single
@@ -128,10 +319,8 @@ function buildFixtureTable(entities) {
128
319
  const pk = entity.pkField;
129
320
  const key = (rpc) => `${entity.service}/${rpc}`;
130
321
  if (entity.list) {
131
- const { rpc, responseSchema, itemsField } = entity.list;
132
- table.set(key(rpc), () => create(responseSchema, {
133
- [itemsField]: Array.from(storeFor(entity).values()),
134
- }));
322
+ const list = entity.list;
323
+ table.set(key(list.rpc), (input, requestSchema) => serveList(Array.from(storeFor(entity).values()), list, input, requestSchema));
135
324
  }
136
325
  if (entity.get) {
137
326
  const { rpc, responseSchema, entityField } = entity.get;
@@ -262,7 +451,10 @@ export function createMockTransport(options) {
262
451
  // (Code.NotFound), never a silent wrong-record fallback.
263
452
  const fixture = fixtures.get(key);
264
453
  if (fixture) {
265
- return makeUnaryResponse(method, fixture(input));
454
+ // `method.input` is typed as always present, but a hand-built method
455
+ // object (a test, a custom harness) may omit it.
456
+ const requestSchema = method.input;
457
+ return makeUnaryResponse(method, fixture(input, requestSchema));
266
458
  }
267
459
  // 4) Nothing matched. Surface a clear error instead of hanging.
268
460
  return Promise.reject(new ConnectError(`mock-transport: no scenario handler or entity fixture for ${key}`, Code.Unimplemented));
@@ -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 {\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"]}
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// A fixture List answers the way the backend's generated CRUD List does\n// (pkg/crud HandleList), so a filtered table, a paged table and a \"how many\"\n// tile read the same against fixtures as against a server — see\n// `serveList` for the exact contract. Custom (non-CRUD) RPCs are not\n// interpreted at all; they stay hand-written scenario handlers.\n//\n// Imported through the \"@reliantlabs/forge-web-runtime/mock-transport\" subpath,\n// never the barrel: a production bundle must be able to shake the fixtures\n// and this engine out entirely, which is why the generated `connect.ts`\n// reaches its own shim through a dynamic `require()` gated on the mock-mode\n// env var.\nimport {\n create,\n ScalarType,\n type DescField,\n type DescMessage,\n type MessageInitShape,\n} from \"@bufbuild/protobuf\";\nimport {\n Code,\n ConnectError,\n type Transport,\n type UnaryResponse,\n} from \"@connectrpc/connect\";\n\n/**\n * The shape the generated scenario files satisfy. Structural on purpose: the\n * project's own `Scenario` type (src/mocks/scenario-types.ts) carries a\n * richer, per-RPC-typed `handlers` map, and this is the subset the engine\n * dispatches through.\n */\nexport interface MockScenario {\n name: string;\n /** Per-RPC overrides keyed by `${serviceTypeName}/${methodName}`. */\n handlers: Record<string, ((req: never) => unknown) | undefined>;\n /** Forward unmatched RPCs to the real backend (hybrid mode only). */\n passthrough?: boolean;\n /** Non-RPC side effects, run once before the first RPC fires. */\n setup?: () => void;\n}\n\n/** The generated `src/mocks/scenarios` barrel, as the engine consumes it. */\nexport interface MockScenarioRegistry {\n byName(name: string): MockScenario | undefined;\n defaultScenario: MockScenario;\n}\n\n/**\n * One CRUD arm of an entity's dispatch table.\n *\n * The List arm needs nothing beyond these three fields to honour filters,\n * paging and total_count: the REQUEST schema arrives on every call as the\n * Connect method's `input`, and the entity schema is the element type of\n * `itemsField` on `responseSchema`.\n */\nexport interface MockListRpc {\n rpc: string;\n responseSchema: DescMessage;\n /**\n * The camelCase name of the list response's repeated field — the ACTUAL\n * proto field (e.g. `keys`), not the camelCased entity plural.\n */\n itemsField: string;\n}\n\nexport interface MockGetRpc {\n rpc: string;\n responseSchema: DescMessage;\n /** The camelCase field wrapping the entity on the response message. */\n entityField: string;\n}\n\nexport interface MockWriteRpc extends MockGetRpc {\n /**\n * The camelCase field wrapping the entity on the REQUEST message. AIP-134\n * requests nest the entity ({ item: {...}, updateMask }); the generated\n * forms submit it flattened. Both are accepted.\n */\n requestField: string;\n}\n\nexport interface MockDeleteRpc {\n rpc: string;\n}\n\n/**\n * Everything the engine needs to serve one entity's CRUD quintet from\n * fixtures. Generated into `src/lib/mock-transport.ts` from the project's\n * proto descriptors.\n */\nexport interface MockEntityDescriptor {\n /** Fully-qualified proto service name, e.g. \"demo.v1.ClinicService\". */\n service: string;\n /** Lowercase entity name, used verbatim in the NotFound message. */\n label: string;\n /**\n * The camelCase PRIMARY-KEY field. The session store keys records by this\n * field; hardcoding \"id\" breaks every surrogate-PK entity (a\n * `usage_event_id` PK projects to a message with no `id` field).\n */\n pkField: string;\n /** Deterministic seed rows — the same values the dev database is seeded with. */\n fixtures: readonly unknown[];\n /** The entity message schema. Required by the create/update write paths. */\n entitySchema?: DescMessage;\n list?: MockListRpc;\n get?: MockGetRpc;\n create?: MockWriteRpc;\n update?: MockWriteRpc;\n delete?: MockDeleteRpc;\n}\n\nexport interface CreateMockTransportOptions {\n /** The resolved scenario — see {@link resolveActiveScenario}. */\n scenario: MockScenario;\n /** Per-entity fixture dispatch. Omit for a scenario-only project. */\n entities?: readonly MockEntityDescriptor[];\n /**\n * Optional real Connect transport. When the active scenario declares\n * `passthrough: true`, any RPC not matched by a scenario handler is\n * forwarded here instead of falling through to the fixtures.\n */\n fallback?: Transport;\n}\n\n/**\n * Mutable session stores, keyed by descriptor identity.\n *\n * Create/Update/Delete round-trip within one browser session (a created row\n * appears in the next List; a deleted one disappears) and reset to the\n * deterministic fixtures on reload. The scaffolded original got that from\n * module-scope `const store = new Map(...)`; keying off the descriptor\n * object preserves it — a module-level descriptor array hands back the same\n * store on every call, so building the transport twice (hybrid mode, a test\n * swapping transports) does not silently fork the data.\n */\nconst stores = new WeakMap<MockEntityDescriptor, Map<string, unknown>>();\n\nfunction storeFor(entity: MockEntityDescriptor): Map<string, unknown> {\n let store = stores.get(entity);\n if (!store) {\n store = new Map(\n entity.fixtures.map((row) => [\n String((row as Record<string, unknown>)[entity.pkField]),\n row,\n ]),\n );\n stores.set(entity, store);\n }\n return store;\n}\n\n/** Scenarios whose `setup()` has already run. */\nconst initialized = new WeakSet<MockScenario>();\n\n/**\n * Resolve the scenario the app should run under, and run its `setup()` once.\n *\n * Call this at MODULE SCOPE (the generated `src/lib/mock-transport.ts` does).\n * Reading the URL once — rather than on every RPC — means navigating away\n * from `?scenario=` keeps the scenario active until a full reload, matching\n * the agent-driven flow where the URL is the single source of truth.\n *\n * `setup()` is for non-RPC state (localStorage flags, sessionStorage). It is\n * synchronous and makes no network calls.\n */\nexport function resolveActiveScenario(\n registry: MockScenarioRegistry,\n): MockScenario {\n const requested =\n typeof globalThis !== \"undefined\" && globalThis.location\n ? new URLSearchParams(globalThis.location.search).get(\"scenario\")\n : null;\n // A ternary, not `&&`: with `??` alone the empty-string falsy branch leaks\n // '' into the inferred union, and '' survives `??` (which only replaces\n // null/undefined), so downstream access on .setup / .handlers would fail\n // under strict tsc.\n const active =\n (requested ? registry.byName(requested) : undefined) ??\n registry.defaultScenario;\n\n if (!initialized.has(active)) {\n initialized.add(active);\n active.setup?.();\n }\n return active;\n}\n\n/**\n * Normalize whatever a streaming scenario handler returns into an\n * AsyncIterable<unknown>. Accepts arrays, iterables, async iterables, and\n * promises that resolve to any of the above. Single (non-iterable) values\n * are wrapped as a one-element stream — handy for handlers that conceptually\n * yield once.\n */\nexport async function* toAsyncIterable(value: unknown): AsyncIterable<unknown> {\n const resolved = await Promise.resolve(value);\n if (resolved == null) return;\n // AsyncIterable\n if (\n typeof (resolved as { [Symbol.asyncIterator]?: unknown })[\n Symbol.asyncIterator\n ] === \"function\"\n ) {\n for await (const m of resolved as AsyncIterable<unknown>) yield m;\n return;\n }\n // Iterable (including arrays)\n if (\n typeof (resolved as { [Symbol.iterator]?: unknown })[Symbol.iterator] ===\n \"function\"\n ) {\n for (const m of resolved as Iterable<unknown>) yield m;\n return;\n }\n // Single value\n yield resolved;\n}\n\nfunction makeUnaryResponse<T>(\n method: { name: string; parent: { typeName: string } },\n message: T,\n): UnaryResponse<never, never> {\n return {\n service: method.parent as never,\n method: method as never,\n stream: false,\n header: new Headers(),\n message: message as never,\n trailer: new Headers(),\n };\n}\n\n/**\n * A fixture-backed handler for one RPC key. `requestSchema` is the Connect\n * method's input descriptor — absent only when a caller hand-builds a method\n * object without one, in which case List serves paging alone.\n */\ntype FixtureHandler = (\n input: unknown,\n requestSchema: DescMessage | undefined,\n) => unknown;\n\n// ── List semantics ─────────────────────────────────────────────────────\n//\n// What the backend's generated List does, and therefore what the fixture\n// List does (pkg/crud HandleList + the generated Filters closure):\n//\n// - Every `optional` request field that is SET filters by equality on the\n// entity field of the same name. Enums compare by value. A field with\n// IMPLICIT presence (no `optional`) is never a filter — the backend\n// generates none for it, so the mock must not invent one.\n// - `search` matches case-insensitively against the entity's string\n// fields and enum value names (enum columns are stored as names).\n// - `order_by` (snake_case column, comma-separated, optional ASC/DESC per\n// column) plus `descending` sorts; an unknown column is InvalidArgument.\n// - `page_size` defaults to 50 and is clamped to 100. `total_count` is the\n// filtered count BEFORE paging. `next_page_token` is minted only for the\n// default (primary-key) order — an ordered list is single-page on the\n// backend, and a mock that paged it would hide that.\n//\n// Paging, ordering and search arrive through these request fields, by the\n// names forge's CRUD List requests declare. They are never equality filters.\nconst LIST_CONTROL_FIELDS = new Set([\n \"pageSize\",\n \"pageToken\",\n \"orderBy\",\n \"descending\",\n \"search\",\n]);\n\n/** pkg/crud ListOp's defaults: 0 means 50, and no page exceeds 100. */\nconst DEFAULT_PAGE_SIZE = 50;\nconst MAX_PAGE_SIZE = 100;\n\n/** FeatureSet_FieldPresence.EXPLICIT — proto3 `optional`, oneofs, messages. */\nconst EXPLICIT_PRESENCE = 1;\n\ntype Row = Record<string, unknown>;\n\n/** The entity message a List response repeats, read off its descriptor. */\nfunction listItemSchema(list: MockListRpc): DescMessage | undefined {\n const items = list.responseSchema.field[list.itemsField];\n return items?.fieldKind === \"list\" && items.listKind === \"message\"\n ? items.message\n : undefined;\n}\n\n/** Scalars and enums compare by value; messages, bytes, lists and maps do not. */\nfunction isComparable(field: DescField): boolean {\n if (field.fieldKind === \"enum\") return true;\n return field.fieldKind === \"scalar\" && field.scalar !== ScalarType.BYTES;\n}\n\nfunction sameValue(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n // A 64-bit field is a bigint — or a string under jstype=JS_STRING — and\n // the two sides are not guaranteed to agree on which.\n if (a == null || b == null) return false;\n return (\n (typeof a === \"bigint\" || typeof b === \"bigint\") && String(a) === String(b)\n );\n}\n\nfunction enumName(field: DescField, value: unknown): string | undefined {\n return field.fieldKind === \"enum\"\n ? field.enum.values.find((v) => v.number === value)?.name\n : undefined;\n}\n\n/** The value a column sorts by, as Postgres would order the stored column. */\nfunction sortKey(field: DescField, value: unknown): unknown {\n if (value == null) return undefined;\n // Enum columns hold the value NAME, so they sort alphabetically by name.\n if (field.fieldKind === \"enum\") return enumName(field, value) ?? \"\";\n if (field.fieldKind === \"scalar\") return value;\n if (\n field.fieldKind === \"message\" &&\n field.message.typeName === \"google.protobuf.Timestamp\"\n ) {\n const ts = value as { seconds: bigint; nanos: number };\n return BigInt(ts.seconds) * 1_000_000_000n + BigInt(ts.nanos);\n }\n return undefined;\n}\n\n/** Postgres's default: NULL sorts after every value ascending. */\nfunction compareKeys(a: unknown, b: unknown): number {\n if (a === undefined || b === undefined) {\n return a === b ? 0 : a === undefined ? 1 : -1;\n }\n const x = a as string | number | bigint | boolean;\n const y = b as string | number | bigint | boolean;\n return x < y ? -1 : x > y ? 1 : 0;\n}\n\nfunction invalidArgument(message: string): ConnectError {\n return new ConnectError(`mock-transport: ${message}`, Code.InvalidArgument);\n}\n\n/** Parse `order_by` the way orm.ValidateOrderBy reads it. */\nfunction parseOrderBy(\n clause: string,\n descending: boolean,\n itemSchema: DescMessage,\n): { field: DescField; desc: boolean }[] {\n return clause.split(\",\").map((part) => {\n const [column = \"\", direction] = part.trim().split(/\\s+/);\n const field = itemSchema.fields.find(\n (f) => f.name === column || f.localName === column,\n );\n if (!field) {\n throw invalidArgument(`unknown order-by column \"${column}\"`);\n }\n const dir = direction?.toUpperCase();\n if (dir !== undefined && dir !== \"ASC\" && dir !== \"DESC\") {\n throw invalidArgument(`invalid order-by direction \"${direction}\"`);\n }\n return { field, desc: dir === undefined ? descending : dir === \"DESC\" };\n });\n}\n\n/** A count in whatever JS type the response's total_count field takes. */\nfunction countValue(field: DescField, n: number): unknown {\n if (field.fieldKind !== \"scalar\") return n;\n switch (field.scalar) {\n case ScalarType.INT64:\n case ScalarType.UINT64:\n case ScalarType.SINT64:\n case ScalarType.FIXED64:\n case ScalarType.SFIXED64:\n return field.longAsString ? String(n) : BigInt(n);\n default:\n return n;\n }\n}\n\n/**\n * Answer one List call from the entity's rows. See the List semantics note\n * above for the contract; the engine's tests pin each clause.\n */\nfunction serveList(\n rows: readonly unknown[],\n list: MockListRpc,\n input: unknown,\n requestSchema: DescMessage | undefined,\n): unknown {\n const req = (input ?? {}) as Row;\n const itemSchema = listItemSchema(list);\n let matched = rows as readonly Row[];\n\n if (itemSchema && requestSchema) {\n for (const filter of requestSchema.fields) {\n if (LIST_CONTROL_FIELDS.has(filter.localName)) continue;\n if (filter.presence !== EXPLICIT_PRESENCE || !isComparable(filter)) {\n continue;\n }\n const want = req[filter.localName];\n if (want == null) continue; // unset: no filter\n const target = itemSchema.field[filter.localName];\n if (!target || !isComparable(target)) continue;\n matched = matched.filter((row) => sameValue(row[filter.localName], want));\n }\n }\n\n const search =\n typeof req.search === \"string\" ? req.search.trim().toLowerCase() : \"\";\n if (itemSchema && search) {\n const searchable = itemSchema.fields.filter(\n (f) =>\n f.fieldKind === \"enum\" ||\n (f.fieldKind === \"scalar\" && f.scalar === ScalarType.STRING),\n );\n matched = matched.filter((row) =>\n searchable.some((f) => {\n const value = row[f.localName];\n const text = f.fieldKind === \"enum\" ? enumName(f, value) : value;\n return typeof text === \"string\" && text.toLowerCase().includes(search);\n }),\n );\n }\n\n const orderBy = typeof req.orderBy === \"string\" ? req.orderBy.trim() : \"\";\n if (itemSchema && orderBy) {\n const keys = parseOrderBy(orderBy, req.descending === true, itemSchema);\n matched = [...matched].sort((a, b) => {\n for (const { field, desc } of keys) {\n const c = compareKeys(\n sortKey(field, a[field.localName]),\n sortKey(field, b[field.localName]),\n );\n if (c !== 0) return desc ? -c : c;\n }\n return 0;\n });\n }\n\n const total = matched.length;\n\n let offset = 0;\n const token = typeof req.pageToken === \"string\" ? req.pageToken : \"\";\n if (token) {\n // The mock's own cursor: an offset into the filtered rows. Opaque to the\n // client, exactly like the backend's keyset cursor.\n if (!/^\\d+$/.test(token)) throw invalidArgument(\"invalid page token\");\n offset = Number(token);\n }\n const requested = Number(req.pageSize ?? 0);\n const pageSize =\n requested > 0 ? Math.min(requested, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE;\n const end = offset + pageSize;\n\n const init: Row = { [list.itemsField]: matched.slice(offset, end) };\n const totalField = list.responseSchema.field.totalCount;\n if (totalField) init.totalCount = countValue(totalField, total);\n if (list.responseSchema.field.nextPageToken && !orderBy && end < total) {\n init.nextPageToken = String(end);\n }\n return create(list.responseSchema, init as MessageInitShape<DescMessage>);\n}\n\n/**\n * Compile the entity descriptors into a flat `${service}/${rpc}` → handler\n * map. Building it once per transport keeps per-RPC dispatch to a single\n * lookup, exactly like the generated `switch` it replaces.\n */\nfunction buildFixtureTable(\n entities: readonly MockEntityDescriptor[],\n): Map<string, FixtureHandler> {\n const table = new Map<string, FixtureHandler>();\n\n for (const entity of entities) {\n const pk = entity.pkField;\n const key = (rpc: string) => `${entity.service}/${rpc}`;\n\n if (entity.list) {\n const list = entity.list;\n table.set(key(list.rpc), (input, requestSchema) =>\n serveList(\n Array.from(storeFor(entity).values()),\n list,\n input,\n requestSchema,\n ),\n );\n }\n\n if (entity.get) {\n const { rpc, responseSchema, entityField } = entity.get;\n table.set(key(rpc), (input) => {\n // Read the key off the SAME field the store is keyed by, not a\n // hardcoded `id` — a surrogate-PK entity would otherwise look up\n // String(undefined) and miss every record.\n const req = input as Record<string, unknown> | undefined;\n const found = storeFor(entity).get(String(req?.[pk]));\n if (!found) {\n // A miss is NotFound — exactly what the real backend returns.\n // (Falling back to the first fixture made every detail page\n // \"work\" against the wrong record and hid bad-id bugs.)\n throw new ConnectError(\n `${entity.label} ${String(req?.[pk])} not found`,\n Code.NotFound,\n );\n }\n return create(responseSchema, {\n [entityField]: found,\n } as MessageInitShape<DescMessage>);\n });\n }\n\n if (entity.create && entity.entitySchema) {\n const { rpc, responseSchema, entityField, requestField } = entity.create;\n const entitySchema = entity.entitySchema;\n table.set(key(rpc), (input) => {\n // Create requests carry the entity fields flattened (that's how the\n // generated form submits); a nested { <entity>: {...} } payload is\n // accepted too.\n const req = (input ?? {}) as Record<string, unknown>;\n const init = {\n ...((req[requestField] ?? req) as Record<string, unknown>),\n };\n delete init.$typeName;\n // Mint the primary key on the PK field the store is keyed by — for a\n // surrogate-PK entity this is NOT `id`, and writing a generated `id`\n // would leave the store key undefined so the row never round-trips.\n const id =\n typeof init[pk] === \"string\" && init[pk]\n ? (init[pk] as string)\n : crypto.randomUUID();\n const created = create(entitySchema, {\n ...init,\n [pk]: id,\n } as MessageInitShape<DescMessage>);\n storeFor(entity).set(String(id), created);\n return create(responseSchema, {\n [entityField]: created,\n } as MessageInitShape<DescMessage>);\n });\n }\n\n if (entity.update && entity.entitySchema) {\n const { rpc, responseSchema, entityField, requestField } = entity.update;\n const entitySchema = entity.entitySchema;\n table.set(key(rpc), (input) => {\n const req = (input ?? {}) as Record<string, unknown>;\n // AIP-134 requests wrap the entity ({ <entity>: {...}, updateMask })\n // — the id lives inside the wrapper; flat requests carry it at the\n // top level.\n const patch = {\n ...((req[requestField] ?? req) as Record<string, unknown>),\n };\n delete patch.$typeName;\n const id = String(patch[pk] ?? req[pk] ?? \"\");\n const store = storeFor(entity);\n const existing = store.get(id);\n if (!existing) {\n throw new ConnectError(\n `${entity.label} ${id} not found`,\n Code.NotFound,\n );\n }\n const updated = create(entitySchema, {\n ...(existing as Record<string, unknown>),\n ...patch,\n [pk]: id,\n } as MessageInitShape<DescMessage>);\n store.set(id, updated);\n return create(responseSchema, {\n [entityField]: updated,\n } as MessageInitShape<DescMessage>);\n });\n }\n\n if (entity.delete) {\n table.set(key(entity.delete.rpc), (input) => {\n // Delete by the SAME field the store is keyed by.\n const req = input as Record<string, unknown> | undefined;\n storeFor(entity).delete(String(req?.[pk]));\n // Delete returns an empty response — the standard CRUD shape.\n return {};\n });\n }\n }\n\n return table;\n}\n\n/**\n * Create the mock transport.\n *\n * The generated `src/lib/mock-transport.ts` supplies the resolved scenario\n * and the project's entity descriptors; `connect.ts` supplies the optional\n * real transport when running in hybrid mode.\n */\nexport function createMockTransport(\n options: CreateMockTransportOptions,\n): Transport {\n const { scenario, entities = [], fallback } = options;\n const passthrough = scenario.passthrough === true && fallback != null;\n const fixtures = buildFixtureTable(entities);\n\n // Bind the object literal to a Transport-typed variable rather than\n // casting at the return site. A trailing assertion cast does not propagate\n // Connect's Transport signature backwards into the literal's method\n // bodies, so under `strict` tsc every callback parameter on unary/stream\n // errors with TS7006 (\"implicitly has an 'any' type\").\n const transport: Transport = {\n // Connect v2's Transport.unary signature is\n // (method, signal, timeoutMs, header, input, contextValues)\n // — method first, no separate service arg. `method.parent.typeName` is\n // the fully-qualified service name; that is what handler keys and\n // fixture keys are matched against.\n async unary(method, signal, timeoutMs, header, input, contextValues) {\n const key = `${method.parent.typeName}/${method.name}`;\n\n // 1) Scenario overlay.\n const handler = scenario.handlers[key];\n if (handler) {\n const result = await handler(input as never);\n return makeUnaryResponse(method, result);\n }\n\n // 2) Passthrough: route to the real backend instead of fixtures.\n // Skips fixtures entirely — in hybrid mode the whole point is\n // \"use real data for anything not explicitly stubbed\".\n if (passthrough) {\n return fallback!.unary(\n method,\n signal,\n timeoutMs,\n header,\n input,\n contextValues,\n );\n }\n\n // 3) Base fixture dispatch, backed by the mutable per-entity stores so\n // writes round-trip within the session. Misses are REAL errors\n // (Code.NotFound), never a silent wrong-record fallback.\n const fixture = fixtures.get(key);\n if (fixture) {\n // `method.input` is typed as always present, but a hand-built method\n // object (a test, a custom harness) may omit it.\n const requestSchema = (method as { input?: DescMessage }).input;\n return makeUnaryResponse(method, fixture(input, requestSchema));\n }\n\n // 4) Nothing matched. Surface a clear error instead of hanging.\n return Promise.reject(\n new ConnectError(\n `mock-transport: no scenario handler or entity fixture for ${key}`,\n Code.Unimplemented,\n ),\n );\n },\n\n // Streaming: a scenario handler can return an array, iterable, async\n // iterable, or a Promise resolving to any of those. The transport adapts\n // it into the AsyncIterable<Response> shape Connect expects. If no\n // handler matches, reject with Unimplemented — base-fixture streaming is\n // intentionally not served (there is no canonical \"list of N messages\"\n // shape for an arbitrary streaming RPC).\n //\n // No explicit return-type annotation: the outer `const transport:\n // Transport` binding lets tsc infer the per-callback signature from\n // Connect's generic Transport.stream<I, O>. Annotating this as\n // Promise<StreamResponse<never, never>> makes the passthrough branch\n // ill-typed (TS2322) because fallback.stream returns StreamResponse<I,O>.\n async stream(method, signal, timeoutMs, header, input, contextValues) {\n const key = `${method.parent.typeName}/${method.name}`;\n const handler = scenario.handlers[key];\n if (!handler) {\n // Passthrough mirrors the unary path: in hybrid mode, unmatched\n // streaming RPCs go to the real backend.\n if (passthrough) {\n return fallback!.stream(\n method,\n signal,\n timeoutMs,\n header,\n input,\n contextValues,\n );\n }\n return Promise.reject(\n new ConnectError(\n `mock-transport: no scenario handler for streaming RPC ${key}`,\n Code.Unimplemented,\n ),\n );\n }\n // Read the request stream eagerly into an array so the scenario handler\n // sees the full client-streaming payload. For server-stream and\n // unary-into-stream-out RPCs this is a single message.\n const requestMessages: unknown[] = [];\n try {\n for await (const m of input as AsyncIterable<unknown>) {\n requestMessages.push(m);\n }\n } catch {\n // Synthetic clients sometimes pass a single value rather than an\n // iterable; treat that as an empty request payload.\n }\n const handlerInput =\n requestMessages.length <= 1 ? requestMessages[0] : requestMessages;\n const result = handler(handlerInput as never);\n return {\n service: method.parent as never,\n method: method as never,\n stream: true,\n header: new Headers(),\n message: toAsyncIterable(result) as AsyncIterable<never>,\n trailer: new Headers(),\n };\n },\n };\n return transport;\n}\n"]}
@@ -0,0 +1,69 @@
1
+ /** Currency and locale for a money helper. Both optional. */
2
+ export interface MoneyOptions {
3
+ /** ISO 4217 code. Default "USD"; "" (a proto3 unset string) also means USD. */
4
+ currency?: string;
5
+ /** BCP 47 locale for separators and symbol placement. Default "en-US". */
6
+ locale?: string;
7
+ }
8
+ /** Options for {@link parseMinorUnits}. */
9
+ export interface ParseMoneyOptions extends MoneyOptions {
10
+ /** Accept a leading "-" / "−". Default false: a minus sign returns null. */
11
+ allowNegative?: boolean;
12
+ }
13
+ /** Locale for a basis-point helper. */
14
+ export interface BasisPointsOptions {
15
+ /** BCP 47 locale for separators and the percent sign. Default "en-US". */
16
+ locale?: string;
17
+ }
18
+ /** Options for {@link parseBasisPoints}. */
19
+ export interface ParseBasisPointsOptions extends BasisPointsOptions {
20
+ /** Accept a leading "-" / "−". Default false: a minus sign returns null. */
21
+ allowNegative?: boolean;
22
+ /** Largest magnitude accepted, in basis points. Default 10000 (100%). */
23
+ max?: number;
24
+ }
25
+ /**
26
+ * formatMinorUnits renders an integer minor-unit amount as currency for a
27
+ * locale: 123456n → "$1,234.56", 1234n JPY → "¥1,234". Exact at any size.
28
+ * Unset renders "—"; a non-integer number throws RangeError.
29
+ */
30
+ export declare function formatMinorUnits(minor: bigint | number | null | undefined, options?: MoneyOptions): string;
31
+ /**
32
+ * parseMinorUnits reads user input such as "1,234.5", "$1234.56" or "12" into
33
+ * minor units. Returns null for anything it cannot represent exactly — too
34
+ * many decimals, junk, a misplaced separator, a negative amount without
35
+ * `allowNegative`, or a value beyond int64 — so a form rejects it instead of
36
+ * silently rounding.
37
+ */
38
+ export declare function parseMinorUnits(input: string, options?: ParseMoneyOptions): bigint | null;
39
+ /**
40
+ * minorUnitsToInput renders minor units as the value of an editable field:
41
+ * 123456n → "1234.56" (de-DE: "1234,56"). No grouping, every minor digit
42
+ * shown, and the result parses back through parseMinorUnits. Unset → "";
43
+ * a non-integer number throws RangeError.
44
+ */
45
+ export declare function minorUnitsToInput(minor: bigint | number | null | undefined, options?: MoneyOptions): string;
46
+ /**
47
+ * currencyMinorDigits returns how many decimal digits a currency's minor unit
48
+ * has — USD 2, JPY 0, KWD 3 — as Intl reports it. Useful for an input's
49
+ * `step` or a validation message. Throws RangeError on a malformed code.
50
+ */
51
+ export declare function currencyMinorDigits(currency?: string): number;
52
+ /**
53
+ * formatBasisPoints renders an integer basis-point rate as a percentage:
54
+ * 825 → "8.25%", 1000 → "10%". Unset renders "—"; a non-integer number
55
+ * (a percent passed where basis points belong) throws RangeError.
56
+ */
57
+ export declare function formatBasisPoints(bps: bigint | number | null | undefined, options?: BasisPointsOptions): string;
58
+ /**
59
+ * parseBasisPoints reads a percentage such as "8.25" or "8.25%" into basis
60
+ * points (825). Returns null for more than two decimals, junk, a negative
61
+ * rate without `allowNegative`, or a magnitude above `max` (default 100%).
62
+ */
63
+ export declare function parseBasisPoints(input: string, options?: ParseBasisPointsOptions): number | null;
64
+ /**
65
+ * basisPointsToInput renders basis points as the value of an editable percent
66
+ * field: 825 → "8.25", 850 → "8.5", 1000 → "10". Parses back through
67
+ * parseBasisPoints. Unset → "".
68
+ */
69
+ export declare function basisPointsToInput(bps: bigint | number | null | undefined, options?: BasisPointsOptions): string;