@timber-js/app 0.2.0-alpha.60 → 0.2.0-alpha.62
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/_chunks/{als-registry-Ba7URUIn.js → als-registry-BJARkOcu.js} +1 -1
- package/dist/_chunks/{als-registry-Ba7URUIn.js.map → als-registry-BJARkOcu.js.map} +1 -1
- package/dist/_chunks/{define-D5STJpIr.js → define-Djpqoe1K.js} +2 -2
- package/dist/_chunks/{define-D5STJpIr.js.map → define-Djpqoe1K.js.map} +1 -1
- package/dist/_chunks/{define-cookie-DtAavax4.js → define-cookie-B2djY9w0.js} +4 -4
- package/dist/_chunks/{define-cookie-DtAavax4.js.map → define-cookie-B2djY9w0.js.map} +1 -1
- package/dist/_chunks/{define-TK8C1M3x.js → define-hdajFTq7.js} +2 -2
- package/dist/_chunks/{define-TK8C1M3x.js.map → define-hdajFTq7.js.map} +1 -1
- package/dist/_chunks/{error-boundary-DpZJBCqh.js → error-boundary-A_sgyyUP.js} +1 -1
- package/dist/_chunks/{error-boundary-DpZJBCqh.js.map → error-boundary-A_sgyyUP.js.map} +1 -1
- package/dist/_chunks/{tracing-VYETCQsg.js → handler-store-CaE0ZgVG.js} +54 -3
- package/dist/_chunks/handler-store-CaE0ZgVG.js.map +1 -0
- package/dist/_chunks/{interception-Cey5DCGr.js → interception-BVm64Jr5.js} +7 -13
- package/dist/_chunks/interception-BVm64Jr5.js.map +1 -0
- package/dist/_chunks/{metadata-routes-BU684ls2.js → metadata-routes-DS3eKNmf.js} +1 -1
- package/dist/_chunks/{metadata-routes-BU684ls2.js.map → metadata-routes-DS3eKNmf.js.map} +1 -1
- package/dist/_chunks/{request-context-0wfZsnhh.js → request-context-B_u9dyhZ.js} +4 -4
- package/dist/_chunks/{request-context-0wfZsnhh.js.map → request-context-B_u9dyhZ.js.map} +1 -1
- package/dist/_chunks/segment-classify-BDNn6EzD.js +65 -0
- package/dist/_chunks/segment-classify-BDNn6EzD.js.map +1 -0
- package/dist/_chunks/{segment-context-CyaM1mrD.js → segment-context-CVRHlkkQ.js} +1 -1
- package/dist/_chunks/{segment-context-CyaM1mrD.js.map → segment-context-CVRHlkkQ.js.map} +1 -1
- package/dist/_chunks/{stale-reload-DKN3aXxR.js → stale-reload-BeyHXZ5B.js} +5 -2
- package/dist/_chunks/{stale-reload-DKN3aXxR.js.map → stale-reload-BeyHXZ5B.js.map} +1 -1
- package/dist/_chunks/{use-query-states-wEXY2JQB.js → use-query-states-DAhgj8Gx.js} +1 -1
- package/dist/_chunks/{use-query-states-wEXY2JQB.js.map → use-query-states-DAhgj8Gx.js.map} +1 -1
- package/dist/_chunks/{wrappers-BaG1bnM3.js → wrappers-CJQ3KwVr.js} +1 -1
- package/dist/_chunks/{wrappers-BaG1bnM3.js.map → wrappers-CJQ3KwVr.js.map} +1 -1
- package/dist/cache/cache-api.d.ts +24 -0
- package/dist/cache/cache-api.d.ts.map +1 -0
- package/dist/cache/handler-store.d.ts +31 -0
- package/dist/cache/handler-store.d.ts.map +1 -0
- package/dist/cache/index.d.ts +2 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +33 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +50 -21
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts +6 -0
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/stale-reload.d.ts.map +1 -1
- package/dist/client/transition-root.d.ts +12 -0
- package/dist/client/transition-root.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/index.js +55 -3
- package/dist/index.js.map +1 -1
- package/dist/params/define.d.ts.map +1 -1
- package/dist/params/index.js +3 -3
- package/dist/plugins/dev-browser-logs.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/routing/index.d.ts +2 -0
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/index.js +3 -2
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +46 -0
- package/dist/routing/segment-classify.d.ts.map +1 -0
- package/dist/search-params/index.js +3 -3
- package/dist/server/actions.d.ts +0 -3
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/fallback-error.d.ts.map +1 -1
- package/dist/server/index.js +100 -17
- package/dist/server/index.js.map +1 -1
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/safe-load.d.ts +46 -0
- package/dist/server/safe-load.d.ts.map +1 -0
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/stream-utils.d.ts.map +1 -1
- package/dist/server/tracing.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cache/cache-api.ts +38 -0
- package/src/cache/handler-store.ts +68 -0
- package/src/cache/index.ts +2 -1
- package/src/client/browser-entry.ts +90 -31
- package/src/client/index.ts +1 -1
- package/src/client/link.tsx +81 -46
- package/src/client/rsc-fetch.ts +3 -1
- package/src/client/stale-reload.ts +19 -2
- package/src/client/transition-root.tsx +27 -0
- package/src/params/define.ts +11 -4
- package/src/plugins/dev-browser-logs.ts +10 -0
- package/src/plugins/entries.ts +61 -0
- package/src/plugins/routing.ts +1 -1
- package/src/routing/index.ts +2 -0
- package/src/routing/scanner.ts +7 -16
- package/src/routing/segment-classify.ts +89 -0
- package/src/server/actions.ts +7 -6
- package/src/server/deny-renderer.ts +4 -3
- package/src/server/error-boundary-wrapper.ts +9 -6
- package/src/server/fallback-error.ts +20 -7
- package/src/server/pipeline-interception.ts +16 -15
- package/src/server/pipeline.ts +20 -2
- package/src/server/route-element-builder.ts +5 -4
- package/src/server/rsc-entry/api-handler.ts +4 -3
- package/src/server/rsc-entry/error-renderer.ts +25 -10
- package/src/server/rsc-entry/index.ts +24 -0
- package/src/server/rsc-entry/rsc-payload.ts +1 -1
- package/src/server/rsc-entry/ssr-bridge.ts +13 -4
- package/src/server/rsc-entry/ssr-renderer.ts +12 -1
- package/src/server/safe-load.ts +60 -0
- package/src/server/slot-resolver.ts +3 -1
- package/src/server/stream-utils.ts +10 -6
- package/src/server/tracing.ts +14 -3
- package/dist/_chunks/interception-Cey5DCGr.js.map +0 -1
- package/dist/_chunks/tracing-VYETCQsg.js.map +0 -1
|
@@ -38,4 +38,4 @@ var waitUntilAls = new AsyncLocalStorage();
|
|
|
38
38
|
//#endregion
|
|
39
39
|
export { timingAls as a, revalidationAls as i, formFlashAls as n, traceAls as o, requestContextAls as r, waitUntilAls as s, earlyHintsSenderAls as t };
|
|
40
40
|
|
|
41
|
-
//# sourceMappingURL=als-registry-
|
|
41
|
+
//# sourceMappingURL=als-registry-BJARkOcu.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"als-registry-
|
|
1
|
+
{"version":3,"file":"als-registry-BJARkOcu.js","names":[],"sources":["../../src/server/als-registry.ts"],"sourcesContent":["/**\n * Centralized AsyncLocalStorage registry for server-side per-request state.\n *\n * ALL ALS instances used by the server framework live here. Individual\n * modules (request-context.ts, tracing.ts, actions.ts, etc.) import from\n * this registry and re-export public accessor functions.\n *\n * Why: ALS instances require singleton semantics — if two copies of the\n * same ALS exist (one from a relative import, one from a barrel import),\n * one module writes to its copy and another reads from an empty copy.\n * Centralizing ALS creation in a single module eliminates this class of bug.\n *\n * The `timber-shims` plugin ensures `@timber-js/app/server` resolves to\n * src/ in RSC and SSR environments, so all import paths converge here.\n *\n * DO NOT create ALS instances outside this file. If you need a new ALS,\n * add it here and import from `./als-registry.js` in the consuming module.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\".\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { DebugComponentEntry } from './rsc-entry/helpers.js';\n\n// ─── Request Context ──────────────────────────────────────────────────────\n// Used by: request-context.ts (headers(), cookies(), searchParams())\n// Design doc: design/04-authorization.md\n\n/** @internal — import via request-context.ts public API */\nexport const requestContextAls = new AsyncLocalStorage<RequestContextStore>();\n\nexport interface RequestContextStore {\n /** Incoming request headers (read-only view). */\n headers: Headers;\n /** Raw cookie header string, parsed lazily into a Map on first access. */\n cookieHeader: string;\n /** Lazily-parsed cookie map (mutable — reflects write-overlay from set()). */\n parsedCookies?: Map<string, string>;\n /** Original (pre-overlay) frozen headers, kept for overlay merging. */\n originalHeaders: Headers;\n /**\n * Promise resolving to the raw URLSearchParams for the current request.\n * To get typed parsed params, import a search params definition and\n * call `.parse(searchParams())`.\n */\n searchParamsPromise: Promise<URLSearchParams>;\n /**\n * Raw search string from the request URL (e.g. \"?foo=bar&baz=1\").\n * Available synchronously for use in `redirect()` with `preserveSearchParams`.\n */\n searchString: string;\n /**\n * Promise resolving to the coerced segment params for the current request.\n * Set by the pipeline after route matching and param coercion, before\n * middleware and rendering. Pages and layouts read params via\n * `rawSegmentParams()` instead of receiving them as a prop.\n *\n * See design/07-routing.md §\"params.ts — Convention File for Typed Params\"\n */\n segmentParamsPromise?: Promise<Record<string, string | string[]>>;\n /** Outgoing Set-Cookie entries (name → serialized value + options). Last write wins. */\n cookieJar: Map<string, CookieEntry>;\n /** Whether the response has flushed (headers committed). */\n flushed: boolean;\n /** Whether the current context allows cookie mutation. */\n mutableContext: boolean;\n /**\n * Dev-only: getter for the current request's RSC debug components.\n * Set by renderRoute() so onPipelineError can include component tree\n * context for render-phase errors without module-level shared state.\n */\n debugComponentsGetter?: () => DebugComponentEntry[];\n}\n\n/** A single outgoing cookie entry in the cookie jar. */\nexport interface CookieEntry {\n name: string;\n value: string;\n options: import('./request-context.js').CookieOptions;\n}\n\n// ─── Tracing ──────────────────────────────────────────────────────────────\n// Used by: tracing.ts (traceId(), spanId())\n// Design doc: design/17-logging.md\n\nexport interface TraceStore {\n /** 32-char lowercase hex trace ID (OTEL or UUID fallback). */\n traceId: string;\n /** OTEL span ID if available, undefined otherwise. */\n spanId?: string;\n}\n\n/** @internal — import via tracing.ts public API */\nexport const traceAls = new AsyncLocalStorage<TraceStore>();\n\n// ─── Server-Timing ────────────────────────────────────────────────────────\n// Used by: server-timing.ts (recordTiming(), withTiming())\n// Design doc: (dev-only performance instrumentation)\n\nexport interface TimingStore {\n entries: import('./server-timing.js').TimingEntry[];\n}\n\n/** @internal — import via server-timing.ts public API */\nexport const timingAls = new AsyncLocalStorage<TimingStore>();\n\n// ─── Revalidation ─────────────────────────────────────────────────────────\n// Used by: actions.ts (revalidatePath(), revalidateTag())\n// Design doc: design/08-forms-and-actions.md\n\nexport interface RevalidationState {\n /** Paths to re-render (populated by revalidatePath calls). */\n paths: string[];\n /** Tags to invalidate (populated by revalidateTag calls). */\n tags: string[];\n}\n\n/** @internal — import via actions.ts public API */\nexport const revalidationAls = new AsyncLocalStorage<RevalidationState>();\n\n// ─── Form Flash ───────────────────────────────────────────────────────────\n// Used by: form-flash.ts (getFormFlash())\n// Design doc: design/08-forms-and-actions.md §\"No-JS Error Round-Trip\"\n\n/** @internal — import via form-flash.ts public API */\nexport const formFlashAls = new AsyncLocalStorage<import('./form-flash.js').FormFlashData>();\n\n// ─── Early Hints Sender ──────────────────────────────────────────────────\n// Used by: early-hints-sender.ts (sendEarlyHints103())\n// Design doc: design/02-rendering-pipeline.md §\"Early Hints (103)\"\n\n/** Function that sends Link header values as a 103 Early Hints response. */\nexport type EarlyHintsSenderFn = (links: string[]) => void;\n\n/** @internal — import via early-hints-sender.ts public API */\nexport const earlyHintsSenderAls = new AsyncLocalStorage<EarlyHintsSenderFn>();\n\n// ─── waitUntil Bridge ────────────────────────────────────────────────────\n// Used by: waituntil-bridge.ts (waitUntil())\n// Design doc: design/11-platform.md §\"waitUntil()\"\n\n/** Function that extends the request lifecycle with a background promise. */\nexport type WaitUntilFn = (promise: Promise<unknown>) => void;\n\n/** @internal — import via waituntil-bridge.ts public API */\nexport const waitUntilAls = new AsyncLocalStorage<WaitUntilFn>();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,oBAAoB,IAAI,mBAAwC;;AAgE7E,IAAa,WAAW,IAAI,mBAA+B;;AAW3D,IAAa,YAAY,IAAI,mBAAgC;;AAc7D,IAAa,kBAAkB,IAAI,mBAAsC;;AAOzE,IAAa,eAAe,IAAI,mBAA4D;;AAU5F,IAAa,sBAAsB,IAAI,mBAAuC;;AAU9E,IAAa,eAAe,IAAI,mBAAgC"}
|
|
@@ -106,7 +106,7 @@ function defineSegmentParams(codecs) {
|
|
|
106
106
|
}
|
|
107
107
|
async function load() {
|
|
108
108
|
if (typeof window !== "undefined") throw new Error("[timber] segmentParams.load() is server-only. Use useSegmentParams() on the client.");
|
|
109
|
-
return
|
|
109
|
+
return await getRawSegmentParams();
|
|
110
110
|
}
|
|
111
111
|
return {
|
|
112
112
|
parse,
|
|
@@ -118,4 +118,4 @@ function defineSegmentParams(codecs) {
|
|
|
118
118
|
//#endregion
|
|
119
119
|
export { defineSegmentParams as n, _setRawSegmentParamsFn as t };
|
|
120
120
|
|
|
121
|
-
//# sourceMappingURL=define-
|
|
121
|
+
//# sourceMappingURL=define-Djpqoe1K.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-D5STJpIr.js","names":[],"sources":["../../src/params/define.ts"],"sourcesContent":["/**\n * defineParams — factory for typed route param coercion.\n *\n * Creates a ParamsDefinition that coerces raw string params from the\n * URL into typed values. Used by exporting from layout.tsx (segment-level)\n * or page.tsx (fallback).\n *\n * Reuses the shared Codec<T> protocol with Standard Schema auto-detection,\n * same pattern as defineSearchParams. Runtime constraints are stricter:\n * - serialize must return string (not null — path segments can't be omitted)\n * - parse throwing → 404 (invalid param value)\n *\n * Design doc: design/07a-route-params-triage.md\n */\n\nimport type { Codec } from '../codec.js';\n\n// ---------------------------------------------------------------------------\n// Server-only ALS reference for .load()\n// ---------------------------------------------------------------------------\n\n// Same pattern as search-params: eagerly registered at server startup\n// to avoid dynamic imports that lose ALS context. See TIM-523.\nlet _rawSegmentParams: (() => Promise<Record<string, string | string[]>>) | undefined;\n\n/**\n * Register the rawSegmentParams function. Called once at module load time\n * from request-context.ts to avoid dynamic import at call time.\n * @internal\n */\nexport function _setRawSegmentParamsFn(fn: () => Promise<Record<string, string | string[]>>): void {\n _rawSegmentParams = fn;\n}\n\nfunction getRawSegmentParams(): Promise<Record<string, string | string[]>> {\n if (!_rawSegmentParams) {\n throw new Error(\n '[timber] segmentParams.load() is only available on the server. ' +\n 'Use useSegmentParams() on the client.'\n );\n }\n return _rawSegmentParams();\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Infer the output type from a Codec or StandardSchemaV1. */\nexport type InferParamField<V> =\n V extends Codec<infer T> ? T : V extends StandardSchemaV1<infer T> ? T : never;\n\n/** Acceptable field value for defineParams: a Codec or a Standard Schema. */\nexport type ParamField<T = unknown> = Codec<T> | StandardSchemaV1<T>;\n\n/**\n * A typed route params definition.\n *\n * Returned by defineParams(). Provides parse (string → typed) and\n * serialize (typed → string) for each declared param.\n */\nexport interface ParamsDefinition<T extends Record<string, unknown>> {\n /** Parse raw string params into typed values. Throws on invalid values. */\n parse(raw: Record<string, string | string[]>): T;\n\n /** Serialize typed values back to strings for URL construction. */\n serialize(values: T): Record<string, string>;\n\n /**\n * Load typed segment params from the current request context (ALS).\n *\n * Server-only. Reads rawSegmentParams() from ALS and coerces through\n * this definition's codecs, returning fully typed params.\n *\n * ```ts\n * // app/products/[id]/params.ts\n * export const segmentParams = defineSegmentParams({ id: z.coerce.number() })\n *\n * // app/products/[id]/page.tsx\n * import { segmentParams } from './params'\n * export default async function Page() {\n * const { id } = await segmentParams.load() // id: number\n * }\n * ```\n */\n load(): Promise<T>;\n\n /** Read-only codec map. */\n codecs: { [K in keyof T]: Codec<T[K]> };\n}\n\n// ---------------------------------------------------------------------------\n// Standard Schema interface (subset — same as in search-params/define.ts)\n// ---------------------------------------------------------------------------\n\ninterface StandardSchemaV1<Output = unknown> {\n '~standard': {\n validate(\n value: unknown\n ):\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> }\n | Promise<\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> }\n >;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction isStandardSchema(value: unknown): value is StandardSchemaV1 {\n return (\n typeof value === 'object' &&\n value !== null &&\n '~standard' in value &&\n typeof (value as StandardSchemaV1)['~standard']?.validate === 'function'\n );\n}\n\nfunction isCodec(value: unknown): value is Codec<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as Codec<unknown>).parse === 'function' &&\n typeof (value as Codec<unknown>).serialize === 'function'\n );\n}\n\n/**\n * Validate sync for Standard Schema (same helper as search-params/codecs.ts).\n */\nfunction validateSync<Output>(\n schema: StandardSchemaV1<Output>,\n value: unknown\n):\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> } {\n const result = schema['~standard'].validate(value);\n if (result instanceof Promise) {\n throw new Error(\n '[timber] defineParams: schema returned a Promise — only sync schemas are supported.'\n );\n }\n return result;\n}\n\n/**\n * Wrap a Standard Schema into a Codec for route params.\n *\n * Unlike fromSchema for search params:\n * - Parse throws on failure (no fallback to default)\n * - Serialize returns string (not null)\n */\nfunction fromParamSchema<T>(fieldName: string, schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // Route params are always strings (single segment) or string[] (catch-all)\n const input = Array.isArray(value) ? value : value;\n\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // For route params, parse failure means the param is invalid → throw\n const messages = result.issues.map((i) => i.message).join(', ');\n throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n // Catch-all segments produce arrays — join with '/' for path reconstruction\n if (Array.isArray(value)) {\n return value.join('/');\n }\n return String(value);\n },\n };\n}\n\n/**\n * Resolve a field value to a Codec. Auto-detects Standard Schema objects.\n */\nfunction resolveField(fieldName: string, value: ParamField): Codec<unknown> {\n if (isCodec(value)) {\n return value;\n }\n\n if (isStandardSchema(value)) {\n return fromParamSchema(fieldName, value);\n }\n\n throw new Error(\n `[timber] defineParams: field '${fieldName}' is not a valid codec or Standard Schema. ` +\n `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +\n `(Zod, Valibot, ArkType).`\n );\n}\n\n/**\n * Validate that no codec's serialize returns null.\n * Route params are structural — they must produce a valid path segment.\n */\nfunction validateSerialize(codecMap: Record<string, Codec<unknown>>): void {\n for (const [key, codec] of Object.entries(codecMap)) {\n // Test serialize with a sample parsed value to check for null\n // We can't exhaustively test, but we can check that serialize(parse(\"test\"))\n // doesn't return null for a basic input.\n try {\n const testValue = codec.parse('test');\n const serialized = codec.serialize(testValue);\n if (serialized === null) {\n throw new Error(\n `[timber] defineParams: field '${key}' codec.serialize() returned null.\\n` +\n ` Route params are path segments — they cannot be omitted.\\n` +\n ` Ensure serialize() always returns a string.`\n );\n }\n } catch (e) {\n // parse('test') may throw for strict codecs (e.g., number-only).\n // That's fine — it means the codec validates. We only care about\n // serialize returning null, which we can't test without a valid value.\n if (e instanceof Error && e.message.includes('returned null')) {\n throw e;\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a ParamsDefinition from a map of codecs and/or Standard Schema objects.\n *\n * ```ts\n * // app/products/[id]/layout.tsx\n * import { defineParams } from '@timber-js/app/params'\n * import { z } from 'zod/v4'\n *\n * export const params = defineParams({\n * id: z.coerce.number().int().positive(),\n * })\n *\n * export default function Layout({ children }) { return children }\n * ```\n */\nexport function defineSegmentParams<C extends Record<string, ParamField>>(\n codecs: C\n): ParamsDefinition<{ [K in keyof C]: InferParamField<C[K]> }> {\n type T = { [K in keyof C]: InferParamField<C[K]> };\n\n const resolvedCodecs: Record<string, Codec<unknown>> = {};\n\n for (const [key, value] of Object.entries(codecs)) {\n resolvedCodecs[key] = resolveField(key, value as ParamField);\n }\n\n // Validate that serialize doesn't return null\n validateSerialize(resolvedCodecs);\n\n // ---- parse ----\n function parse(raw: Record<string, string | string[]>): T {\n const result: Record<string, unknown> = {};\n\n for (const [key, codec] of Object.entries(resolvedCodecs)) {\n const rawValue = raw[key];\n // Route params are always present (the route matched)\n result[key] = codec.parse(rawValue);\n }\n\n return result as T;\n }\n\n // ---- serialize ----\n function serialize(values: T): Record<string, string> {\n const result: Record<string, string> = {};\n\n for (const [key, codec] of Object.entries(resolvedCodecs)) {\n const serialized = codec.serialize(values[key as keyof T] as unknown);\n if (serialized === null) {\n throw new Error(\n `[timber] params.serialize: field '${key}' serialized to null. ` +\n `Route params must produce a valid path segment.`\n );\n }\n result[key] = serialized;\n }\n\n return result;\n }\n\n // ---- load ----\n // ALS-backed: reads rawSegmentParams() from the current request context\n // and parses through codecs. Server-only — throws on client.\n async function load(): Promise<T> {\n if (typeof window !== 'undefined') {\n throw new Error(\n '[timber] segmentParams.load() is server-only. ' + 'Use useSegmentParams() on the client.'\n );\n }\n const raw = await getRawSegmentParams();\n return parse(raw);\n }\n\n const definition: ParamsDefinition<T> = {\n parse,\n serialize,\n load,\n codecs: resolvedCodecs as { [K in keyof T]: Codec<T[K]> },\n };\n\n return definition;\n}\n"],"mappings":";AAuBA,IAAI;;;;;;AAOJ,SAAgB,uBAAuB,IAA4D;AACjG,qBAAoB;;AAGtB,SAAS,sBAAkE;AACzE,KAAI,CAAC,kBACH,OAAM,IAAI,MACR,uGAED;AAEH,QAAO,mBAAmB;;AAwE5B,SAAS,iBAAiB,OAA2C;AACnE,QACE,OAAO,UAAU,YACjB,UAAU,QACV,eAAe,SACf,OAAQ,MAA2B,cAAc,aAAa;;AAIlE,SAAS,QAAQ,OAAyC;AACxD,QACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAAyB,UAAU,cAC3C,OAAQ,MAAyB,cAAc;;;;;AAOnD,SAAS,aACP,QACA,OAGoE;CACpE,MAAM,SAAS,OAAO,aAAa,SAAS,MAAM;AAClD,KAAI,kBAAkB,QACpB,OAAM,IAAI,MACR,sFACD;AAEH,QAAO;;;;;;;;;AAUT,SAAS,gBAAmB,WAAmB,QAAuC;AACpF,QAAO;EACL,MAAM,OAAyC;GAI7C,MAAM,SAAS,aAAa,QAFd,MAAM,QAAQ,MAAM,GAAG,QAAQ,MAEH;AAC1C,OAAI,CAAC,OAAO,OACV,QAAO,OAAO;GAIhB,MAAM,WAAW,OAAO,OAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,KAAK;AAC/D,SAAM,IAAI,MAAM,mBAAmB,UAAU,qBAAqB,WAAW;;EAG/E,UAAU,OAAyB;AACjC,OAAI,UAAU,QAAQ,UAAU,KAAA,EAC9B,QAAO;AAGT,OAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,KAAK,IAAI;AAExB,UAAO,OAAO,MAAM;;EAEvB;;;;;AAMH,SAAS,aAAa,WAAmB,OAAmC;AAC1E,KAAI,QAAQ,MAAM,CAChB,QAAO;AAGT,KAAI,iBAAiB,MAAM,CACzB,QAAO,gBAAgB,WAAW,MAAM;AAG1C,OAAM,IAAI,MACR,iCAAiC,UAAU,uJAG5C;;;;;;AAOH,SAAS,kBAAkB,UAAgD;AACzE,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,CAIjD,KAAI;EACF,MAAM,YAAY,MAAM,MAAM,OAAO;AAErC,MADmB,MAAM,UAAU,UAAU,KAC1B,KACjB,OAAM,IAAI,MACR,iCAAiC,IAAI,+IAGtC;UAEI,GAAG;AAIV,MAAI,aAAa,SAAS,EAAE,QAAQ,SAAS,gBAAgB,CAC3D,OAAM;;;;;;;;;;;;;;;;;;AAyBd,SAAgB,oBACd,QAC6D;CAG7D,MAAM,iBAAiD,EAAE;AAEzD,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,CAC/C,gBAAe,OAAO,aAAa,KAAK,MAAoB;AAI9D,mBAAkB,eAAe;CAGjC,SAAS,MAAM,KAA2C;EACxD,MAAM,SAAkC,EAAE;AAE1C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,eAAe,EAAE;GACzD,MAAM,WAAW,IAAI;AAErB,UAAO,OAAO,MAAM,MAAM,SAAS;;AAGrC,SAAO;;CAIT,SAAS,UAAU,QAAmC;EACpD,MAAM,SAAiC,EAAE;AAEzC,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,eAAe,EAAE;GACzD,MAAM,aAAa,MAAM,UAAU,OAAO,KAA2B;AACrE,OAAI,eAAe,KACjB,OAAM,IAAI,MACR,qCAAqC,IAAI,uEAE1C;AAEH,UAAO,OAAO;;AAGhB,SAAO;;CAMT,eAAe,OAAmB;AAChC,MAAI,OAAO,WAAW,YACpB,OAAM,IAAI,MACR,sFACD;AAGH,SAAO,MADK,MAAM,qBAAqB,CACtB;;AAUnB,QAPwC;EACtC;EACA;EACA;EACA,QAAQ;EACT"}
|
|
1
|
+
{"version":3,"file":"define-Djpqoe1K.js","names":[],"sources":["../../src/params/define.ts"],"sourcesContent":["/**\n * defineParams — factory for typed route param coercion.\n *\n * Creates a ParamsDefinition that coerces raw string params from the\n * URL into typed values. Used by exporting from layout.tsx (segment-level)\n * or page.tsx (fallback).\n *\n * Reuses the shared Codec<T> protocol with Standard Schema auto-detection,\n * same pattern as defineSearchParams. Runtime constraints are stricter:\n * - serialize must return string (not null — path segments can't be omitted)\n * - parse throwing → 404 (invalid param value)\n *\n * Design doc: design/07a-route-params-triage.md\n */\n\nimport type { Codec } from '../codec.js';\n\n// ---------------------------------------------------------------------------\n// Server-only ALS reference for .load()\n// ---------------------------------------------------------------------------\n\n// Same pattern as search-params: eagerly registered at server startup\n// to avoid dynamic imports that lose ALS context. See TIM-523.\nlet _rawSegmentParams: (() => Promise<Record<string, string | string[]>>) | undefined;\n\n/**\n * Register the rawSegmentParams function. Called once at module load time\n * from request-context.ts to avoid dynamic import at call time.\n * @internal\n */\nexport function _setRawSegmentParamsFn(fn: () => Promise<Record<string, string | string[]>>): void {\n _rawSegmentParams = fn;\n}\n\nfunction getRawSegmentParams(): Promise<Record<string, string | string[]>> {\n if (!_rawSegmentParams) {\n throw new Error(\n '[timber] segmentParams.load() is only available on the server. ' +\n 'Use useSegmentParams() on the client.'\n );\n }\n return _rawSegmentParams();\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Infer the output type from a Codec or StandardSchemaV1. */\nexport type InferParamField<V> =\n V extends Codec<infer T> ? T : V extends StandardSchemaV1<infer T> ? T : never;\n\n/** Acceptable field value for defineParams: a Codec or a Standard Schema. */\nexport type ParamField<T = unknown> = Codec<T> | StandardSchemaV1<T>;\n\n/**\n * A typed route params definition.\n *\n * Returned by defineParams(). Provides parse (string → typed) and\n * serialize (typed → string) for each declared param.\n */\nexport interface ParamsDefinition<T extends Record<string, unknown>> {\n /** Parse raw string params into typed values. Throws on invalid values. */\n parse(raw: Record<string, string | string[]>): T;\n\n /** Serialize typed values back to strings for URL construction. */\n serialize(values: T): Record<string, string>;\n\n /**\n * Load typed segment params from the current request context (ALS).\n *\n * Server-only. Reads rawSegmentParams() from ALS and coerces through\n * this definition's codecs, returning fully typed params.\n *\n * ```ts\n * // app/products/[id]/params.ts\n * export const segmentParams = defineSegmentParams({ id: z.coerce.number() })\n *\n * // app/products/[id]/page.tsx\n * import { segmentParams } from './params'\n * export default async function Page() {\n * const { id } = await segmentParams.load() // id: number\n * }\n * ```\n */\n load(): Promise<T>;\n\n /** Read-only codec map. */\n codecs: { [K in keyof T]: Codec<T[K]> };\n}\n\n// ---------------------------------------------------------------------------\n// Standard Schema interface (subset — same as in search-params/define.ts)\n// ---------------------------------------------------------------------------\n\ninterface StandardSchemaV1<Output = unknown> {\n '~standard': {\n validate(\n value: unknown\n ):\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> }\n | Promise<\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> }\n >;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction isStandardSchema(value: unknown): value is StandardSchemaV1 {\n return (\n typeof value === 'object' &&\n value !== null &&\n '~standard' in value &&\n typeof (value as StandardSchemaV1)['~standard']?.validate === 'function'\n );\n}\n\nfunction isCodec(value: unknown): value is Codec<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as Codec<unknown>).parse === 'function' &&\n typeof (value as Codec<unknown>).serialize === 'function'\n );\n}\n\n/**\n * Validate sync for Standard Schema (same helper as search-params/codecs.ts).\n */\nfunction validateSync<Output>(\n schema: StandardSchemaV1<Output>,\n value: unknown\n):\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> } {\n const result = schema['~standard'].validate(value);\n if (result instanceof Promise) {\n throw new Error(\n '[timber] defineParams: schema returned a Promise — only sync schemas are supported.'\n );\n }\n return result;\n}\n\n/**\n * Wrap a Standard Schema into a Codec for route params.\n *\n * Unlike fromSchema for search params:\n * - Parse throws on failure (no fallback to default)\n * - Serialize returns string (not null)\n */\nfunction fromParamSchema<T>(fieldName: string, schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // Route params are always strings (single segment) or string[] (catch-all)\n const input = Array.isArray(value) ? value : value;\n\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // For route params, parse failure means the param is invalid → throw\n const messages = result.issues.map((i) => i.message).join(', ');\n throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n // Catch-all segments produce arrays — join with '/' for path reconstruction\n if (Array.isArray(value)) {\n return value.join('/');\n }\n return String(value);\n },\n };\n}\n\n/**\n * Resolve a field value to a Codec. Auto-detects Standard Schema objects.\n */\nfunction resolveField(fieldName: string, value: ParamField): Codec<unknown> {\n if (isCodec(value)) {\n return value;\n }\n\n if (isStandardSchema(value)) {\n return fromParamSchema(fieldName, value);\n }\n\n throw new Error(\n `[timber] defineParams: field '${fieldName}' is not a valid codec or Standard Schema. ` +\n `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +\n `(Zod, Valibot, ArkType).`\n );\n}\n\n/**\n * Validate that no codec's serialize returns null.\n * Route params are structural — they must produce a valid path segment.\n */\nfunction validateSerialize(codecMap: Record<string, Codec<unknown>>): void {\n for (const [key, codec] of Object.entries(codecMap)) {\n // Test serialize with a sample parsed value to check for null\n // We can't exhaustively test, but we can check that serialize(parse(\"test\"))\n // doesn't return null for a basic input.\n try {\n const testValue = codec.parse('test');\n const serialized = codec.serialize(testValue);\n if (serialized === null) {\n throw new Error(\n `[timber] defineParams: field '${key}' codec.serialize() returned null.\\n` +\n ` Route params are path segments — they cannot be omitted.\\n` +\n ` Ensure serialize() always returns a string.`\n );\n }\n } catch (e) {\n // parse('test') may throw for strict codecs (e.g., number-only).\n // That's fine — it means the codec validates. We only care about\n // serialize returning null, which we can't test without a valid value.\n if (e instanceof Error && e.message.includes('returned null')) {\n throw e;\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a ParamsDefinition from a map of codecs and/or Standard Schema objects.\n *\n * ```ts\n * // app/products/[id]/layout.tsx\n * import { defineParams } from '@timber-js/app/params'\n * import { z } from 'zod/v4'\n *\n * export const params = defineParams({\n * id: z.coerce.number().int().positive(),\n * })\n *\n * export default function Layout({ children }) { return children }\n * ```\n */\nexport function defineSegmentParams<C extends Record<string, ParamField>>(\n codecs: C\n): ParamsDefinition<{ [K in keyof C]: InferParamField<C[K]> }> {\n type T = { [K in keyof C]: InferParamField<C[K]> };\n\n const resolvedCodecs: Record<string, Codec<unknown>> = {};\n\n for (const [key, value] of Object.entries(codecs)) {\n resolvedCodecs[key] = resolveField(key, value as ParamField);\n }\n\n // Validate that serialize doesn't return null\n validateSerialize(resolvedCodecs);\n\n // ---- parse ----\n function parse(raw: Record<string, string | string[]>): T {\n const result: Record<string, unknown> = {};\n\n for (const [key, codec] of Object.entries(resolvedCodecs)) {\n const rawValue = raw[key];\n // Route params are always present (the route matched)\n result[key] = codec.parse(rawValue);\n }\n\n return result as T;\n }\n\n // ---- serialize ----\n function serialize(values: T): Record<string, string> {\n const result: Record<string, string> = {};\n\n for (const [key, codec] of Object.entries(resolvedCodecs)) {\n const serialized = codec.serialize(values[key as keyof T] as unknown);\n if (serialized === null) {\n throw new Error(\n `[timber] params.serialize: field '${key}' serialized to null. ` +\n `Route params must produce a valid path segment.`\n );\n }\n result[key] = serialized;\n }\n\n return result;\n }\n\n // ---- load ----\n // ALS-backed: reads segment params from the current request context.\n // Server-only — throws on client.\n //\n // The pipeline already coerces params via coerceSegmentParams() which\n // calls parse() and stores typed values in ALS via setSegmentParams().\n // We return those directly instead of re-parsing, because codecs may\n // not be idempotent (e.g., a codec that only accepts raw strings would\n // throw if given an already-parsed value). See TIM-574.\n async function load(): Promise<T> {\n if (typeof window !== 'undefined') {\n throw new Error(\n '[timber] segmentParams.load() is server-only. ' + 'Use useSegmentParams() on the client.'\n );\n }\n const params = await getRawSegmentParams();\n // params are already coerced by the pipeline — return as-is.\n return params as unknown as T;\n }\n\n const definition: ParamsDefinition<T> = {\n parse,\n serialize,\n load,\n codecs: resolvedCodecs as { [K in keyof T]: Codec<T[K]> },\n };\n\n return definition;\n}\n"],"mappings":";AAuBA,IAAI;;;;;;AAOJ,SAAgB,uBAAuB,IAA4D;AACjG,qBAAoB;;AAGtB,SAAS,sBAAkE;AACzE,KAAI,CAAC,kBACH,OAAM,IAAI,MACR,uGAED;AAEH,QAAO,mBAAmB;;AAwE5B,SAAS,iBAAiB,OAA2C;AACnE,QACE,OAAO,UAAU,YACjB,UAAU,QACV,eAAe,SACf,OAAQ,MAA2B,cAAc,aAAa;;AAIlE,SAAS,QAAQ,OAAyC;AACxD,QACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAAyB,UAAU,cAC3C,OAAQ,MAAyB,cAAc;;;;;AAOnD,SAAS,aACP,QACA,OAGoE;CACpE,MAAM,SAAS,OAAO,aAAa,SAAS,MAAM;AAClD,KAAI,kBAAkB,QACpB,OAAM,IAAI,MACR,sFACD;AAEH,QAAO;;;;;;;;;AAUT,SAAS,gBAAmB,WAAmB,QAAuC;AACpF,QAAO;EACL,MAAM,OAAyC;GAI7C,MAAM,SAAS,aAAa,QAFd,MAAM,QAAQ,MAAM,GAAG,QAAQ,MAEH;AAC1C,OAAI,CAAC,OAAO,OACV,QAAO,OAAO;GAIhB,MAAM,WAAW,OAAO,OAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,KAAK;AAC/D,SAAM,IAAI,MAAM,mBAAmB,UAAU,qBAAqB,WAAW;;EAG/E,UAAU,OAAyB;AACjC,OAAI,UAAU,QAAQ,UAAU,KAAA,EAC9B,QAAO;AAGT,OAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,KAAK,IAAI;AAExB,UAAO,OAAO,MAAM;;EAEvB;;;;;AAMH,SAAS,aAAa,WAAmB,OAAmC;AAC1E,KAAI,QAAQ,MAAM,CAChB,QAAO;AAGT,KAAI,iBAAiB,MAAM,CACzB,QAAO,gBAAgB,WAAW,MAAM;AAG1C,OAAM,IAAI,MACR,iCAAiC,UAAU,uJAG5C;;;;;;AAOH,SAAS,kBAAkB,UAAgD;AACzE,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,CAIjD,KAAI;EACF,MAAM,YAAY,MAAM,MAAM,OAAO;AAErC,MADmB,MAAM,UAAU,UAAU,KAC1B,KACjB,OAAM,IAAI,MACR,iCAAiC,IAAI,+IAGtC;UAEI,GAAG;AAIV,MAAI,aAAa,SAAS,EAAE,QAAQ,SAAS,gBAAgB,CAC3D,OAAM;;;;;;;;;;;;;;;;;;AAyBd,SAAgB,oBACd,QAC6D;CAG7D,MAAM,iBAAiD,EAAE;AAEzD,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,CAC/C,gBAAe,OAAO,aAAa,KAAK,MAAoB;AAI9D,mBAAkB,eAAe;CAGjC,SAAS,MAAM,KAA2C;EACxD,MAAM,SAAkC,EAAE;AAE1C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,eAAe,EAAE;GACzD,MAAM,WAAW,IAAI;AAErB,UAAO,OAAO,MAAM,MAAM,SAAS;;AAGrC,SAAO;;CAIT,SAAS,UAAU,QAAmC;EACpD,MAAM,SAAiC,EAAE;AAEzC,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,eAAe,EAAE;GACzD,MAAM,aAAa,MAAM,UAAU,OAAO,KAA2B;AACrE,OAAI,eAAe,KACjB,OAAM,IAAI,MACR,qCAAqC,IAAI,uEAE1C;AAEH,UAAO,OAAO;;AAGhB,SAAO;;CAYT,eAAe,OAAmB;AAChC,MAAI,OAAO,WAAW,YACpB,OAAM,IAAI,MACR,sFACD;AAIH,SAFe,MAAM,qBAAqB;;AAY5C,QAPwC;EACtC;EACA;EACA;EACA,QAAQ;EACT"}
|
|
@@ -43,12 +43,12 @@ function defineCookie(name, options) {
|
|
|
43
43
|
options: resolvedOptions,
|
|
44
44
|
codec,
|
|
45
45
|
async getCookie() {
|
|
46
|
-
const { cookies } = await import("./request-context-
|
|
46
|
+
const { cookies } = await import("./request-context-B_u9dyhZ.js").then((n) => n.l);
|
|
47
47
|
const raw = cookies().get(name);
|
|
48
48
|
return codec.parse(raw);
|
|
49
49
|
},
|
|
50
50
|
async setCookie(value) {
|
|
51
|
-
const { cookies } = await import("./request-context-
|
|
51
|
+
const { cookies } = await import("./request-context-B_u9dyhZ.js").then((n) => n.l);
|
|
52
52
|
const serialized = codec.serialize(value);
|
|
53
53
|
if (serialized === null) cookies().delete(name, {
|
|
54
54
|
path: resolvedOptions.path,
|
|
@@ -57,7 +57,7 @@ function defineCookie(name, options) {
|
|
|
57
57
|
else cookies().set(name, serialized, resolvedOptions);
|
|
58
58
|
},
|
|
59
59
|
async deleteCookie() {
|
|
60
|
-
const { cookies } = await import("./request-context-
|
|
60
|
+
const { cookies } = await import("./request-context-B_u9dyhZ.js").then((n) => n.l);
|
|
61
61
|
cookies().delete(name, {
|
|
62
62
|
path: resolvedOptions.path,
|
|
63
63
|
domain: resolvedOptions.domain
|
|
@@ -90,4 +90,4 @@ function defineCookie(name, options) {
|
|
|
90
90
|
//#endregion
|
|
91
91
|
export { defineCookie as n, _registerUseCookieModule as t };
|
|
92
92
|
|
|
93
|
-
//# sourceMappingURL=define-cookie-
|
|
93
|
+
//# sourceMappingURL=define-cookie-B2djY9w0.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-cookie-
|
|
1
|
+
{"version":3,"file":"define-cookie-B2djY9w0.js","names":[],"sources":["../../src/cookies/define-cookie.ts"],"sourcesContent":["/**\n * defineCookie — typed cookie definitions.\n *\n * Bundles name + codec + options into a reusable CookieDefinition<T>\n * with async .getCookie(), .setCookie(), .deleteCookie() server methods\n * and a sync .useCookie() client hook.\n *\n * Server methods are async to future-proof the API for v2 features\n * (signed cookies via crypto.subtle, encrypted cookies, external stores).\n *\n * Reuses the SearchParamCodec protocol via fromSchema() bridge.\n * Validation on read returns the codec default (never throws).\n *\n * IMPORTANT: This module must NOT have top-level value imports from either\n * server or client modules. Server methods lazy-import request-context;\n * useCookie() lazy-imports use-cookie. This ensures:\n * - Client bundles don't pull in ALS/server code\n * - RSC bundles don't pull in useSyncExternalStore/client code\n * - Tree-shaking is not required for correctness\n *\n * See design/29-cookies.md §\"Typed Cookies with Schema Validation\"\n */\n\nimport type { CookieOptions } from '../server/request-context.js';\nimport type { ClientCookieOptions } from '../client/use-cookie.js';\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nimport type { Codec } from '../codec.js';\n\n/**\n * A codec that converts between string cookie values and typed values.\n * Type alias for the shared Codec<T> protocol.\n */\nexport type CookieCodec<T> = Codec<T>;\n\n/** Options for defineCookie: codec + CookieOptions merged. */\nexport interface DefineCookieOptions<T> extends CookieOptions {\n /** Codec for parsing/serializing the cookie value. */\n codec: CookieCodec<T>;\n}\n\n/** A fully typed cookie definition with server and client methods. */\nexport interface CookieDefinition<T> {\n /** The cookie name. */\n readonly name: string;\n /** The resolved cookie options (without codec). */\n readonly options: CookieOptions;\n /** The codec used for parsing/serializing. */\n readonly codec: CookieCodec<T>;\n\n /** Server: read the typed value from the current request. */\n getCookie(): Promise<T>;\n /** Server: set the typed value on the response. */\n setCookie(value: T): Promise<void>;\n /** Server: delete the cookie. */\n deleteCookie(): Promise<void>;\n\n /** Client: React hook for reading/writing this cookie. Returns [value, setter, deleter]. */\n useCookie(): [T, (value: T) => void, () => void];\n}\n\n// ─── Lazy Module References ───────────────────────────────────────────────\n//\n// These are resolved on first use, not at module load time. This prevents\n// the server module graph from pulling in client code and vice versa.\n// The dynamic import() in server methods is natural (they're async).\n// For useCookie() (sync), we cache the module reference after first load.\n\nlet _useCookieModule: typeof import('../client/use-cookie.js') | undefined;\n\nfunction getUseCookieModule(): typeof import('../client/use-cookie.js') {\n if (!_useCookieModule) {\n // In the client/SSR environment, this module is already in the module\n // graph (imported by the client entry). The throw is a safeguard —\n // if useCookie() is somehow called before the module is available,\n // the developer gets a clear error instead of a silent failure.\n throw new Error(\n '[timber] defineCookie().useCookie() requires @timber-js/app/client to be loaded. ' +\n 'This hook can only be used in client components.'\n );\n }\n return _useCookieModule;\n}\n\n/**\n * Register the client cookie module. Called by the client entry to wire\n * up the lazy reference without a top-level import.\n *\n * @internal — framework use only\n */\nexport function _registerUseCookieModule(mod: typeof import('../client/use-cookie.js')): void {\n _useCookieModule = mod;\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────\n\n/**\n * Define a typed cookie.\n *\n * ```ts\n * import { defineCookie } from '@timber-js/app/cookies';\n * import { fromSchema } from '@timber-js/app/search-params';\n * import { z } from 'zod/v4';\n *\n * export const themeCookie = defineCookie('theme', {\n * codec: fromSchema(z.enum(['light', 'dark', 'system']).default('system')),\n * httpOnly: false,\n * maxAge: 60 * 60 * 24 * 365,\n * });\n *\n * // Server\n * const theme = await themeCookie.getCookie();\n * await themeCookie.setCookie('dark');\n *\n * // Client\n * const [theme, setTheme] = themeCookie.useCookie();\n * ```\n */\nexport function defineCookie<T>(\n name: string,\n options: DefineCookieOptions<T>\n): CookieDefinition<T> {\n const { codec, ...cookieOpts } = options;\n const resolvedOptions: CookieOptions = { ...cookieOpts };\n\n return {\n name,\n options: resolvedOptions,\n codec,\n\n async getCookie(): Promise<T> {\n const { cookies } = await import('../server/request-context.js');\n const jar = cookies();\n const raw = jar.get(name);\n return codec.parse(raw);\n },\n\n async setCookie(value: T): Promise<void> {\n const { cookies } = await import('../server/request-context.js');\n const serialized = codec.serialize(value);\n if (serialized === null) {\n cookies().delete(name, {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n });\n } else {\n cookies().set(name, serialized, resolvedOptions);\n }\n },\n\n async deleteCookie(): Promise<void> {\n const { cookies } = await import('../server/request-context.js');\n cookies().delete(name, {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n });\n },\n\n useCookie(): [T, (value: T) => void, () => void] {\n const { useCookie: useRawCookie } = getUseCookieModule();\n\n // Extract client-safe options (no httpOnly — client cookies can't be httpOnly)\n const clientOpts: ClientCookieOptions = {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n maxAge: resolvedOptions.maxAge,\n expires: resolvedOptions.expires,\n sameSite: resolvedOptions.sameSite,\n secure: resolvedOptions.secure,\n };\n\n const [raw, setRaw, deleteRaw] = useRawCookie(name, clientOpts);\n const parsed = codec.parse(raw);\n\n const setTyped = (value: T): void => {\n const serialized = codec.serialize(value);\n if (serialized === null) {\n deleteRaw();\n } else {\n setRaw(serialized);\n }\n };\n\n return [parsed, setTyped, deleteRaw];\n },\n };\n}\n"],"mappings":";AAqEA,IAAI;AAEJ,SAAS,qBAA+D;AACtE,KAAI,CAAC,iBAKH,OAAM,IAAI,MACR,oIAED;AAEH,QAAO;;;;;;;;AAST,SAAgB,yBAAyB,KAAqD;AAC5F,oBAAmB;;;;;;;;;;;;;;;;;;;;;;;;AA2BrB,SAAgB,aACd,MACA,SACqB;CACrB,MAAM,EAAE,OAAO,GAAG,eAAe;CACjC,MAAM,kBAAiC,EAAE,GAAG,YAAY;AAExD,QAAO;EACL;EACA,SAAS;EACT;EAEA,MAAM,YAAwB;GAC5B,MAAM,EAAE,YAAY,MAAM,OAAO,iCAAA,MAAA,MAAA,EAAA,EAAA;GAEjC,MAAM,MADM,SAAS,CACL,IAAI,KAAK;AACzB,UAAO,MAAM,MAAM,IAAI;;EAGzB,MAAM,UAAU,OAAyB;GACvC,MAAM,EAAE,YAAY,MAAM,OAAO,iCAAA,MAAA,MAAA,EAAA,EAAA;GACjC,MAAM,aAAa,MAAM,UAAU,MAAM;AACzC,OAAI,eAAe,KACjB,UAAS,CAAC,OAAO,MAAM;IACrB,MAAM,gBAAgB;IACtB,QAAQ,gBAAgB;IACzB,CAAC;OAEF,UAAS,CAAC,IAAI,MAAM,YAAY,gBAAgB;;EAIpD,MAAM,eAA8B;GAClC,MAAM,EAAE,YAAY,MAAM,OAAO,iCAAA,MAAA,MAAA,EAAA,EAAA;AACjC,YAAS,CAAC,OAAO,MAAM;IACrB,MAAM,gBAAgB;IACtB,QAAQ,gBAAgB;IACzB,CAAC;;EAGJ,YAAiD;GAC/C,MAAM,EAAE,WAAW,iBAAiB,oBAAoB;GAYxD,MAAM,CAAC,KAAK,QAAQ,aAAa,aAAa,MATN;IACtC,MAAM,gBAAgB;IACtB,QAAQ,gBAAgB;IACxB,QAAQ,gBAAgB;IACxB,SAAS,gBAAgB;IACzB,UAAU,gBAAgB;IAC1B,QAAQ,gBAAgB;IACzB,CAE8D;GAC/D,MAAM,SAAS,MAAM,MAAM,IAAI;GAE/B,MAAM,YAAY,UAAmB;IACnC,MAAM,aAAa,MAAM,UAAU,MAAM;AACzC,QAAI,eAAe,KACjB,YAAW;QAEX,QAAO,WAAW;;AAItB,UAAO;IAAC;IAAQ;IAAU;IAAU;;EAEvC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as useQueryStates } from "./use-query-states-
|
|
1
|
+
import { n as useQueryStates } from "./use-query-states-DAhgj8Gx.js";
|
|
2
2
|
//#region src/search-params/codecs.ts
|
|
3
3
|
/**
|
|
4
4
|
* Zod v4's ~standard.validate() signature includes Promise in the return union
|
|
@@ -276,4 +276,4 @@ function buildDefinition(codecMap, urlKeys) {
|
|
|
276
276
|
//#endregion
|
|
277
277
|
export { fromSchema as i, defineSearchParams as n, fromArraySchema as r, _setRawSearchParamsFn as t };
|
|
278
278
|
|
|
279
|
-
//# sourceMappingURL=define-
|
|
279
|
+
//# sourceMappingURL=define-hdajFTq7.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-TK8C1M3x.js","names":[],"sources":["../../src/search-params/codecs.ts","../../src/search-params/define.ts"],"sourcesContent":["/**\n * Built-in codecs and the fromSchema bridge for Standard Schema-compatible\n * validation libraries (Zod, Valibot, ArkType).\n *\n * Design doc: design/09-typescript.md §\"The SearchParamCodec Protocol\"\n */\n\nimport type { SearchParamCodec } from './define.js';\n\n// ---------------------------------------------------------------------------\n// Standard Schema interface (subset)\n//\n// Standard Schema (https://github.com/standard-schema/standard-schema) defines\n// a minimal interface that Zod ≥3.24, Valibot ≥1.0, and ArkType all implement.\n// We depend only on `~standard.validate` to avoid coupling to any specific lib.\n// ---------------------------------------------------------------------------\n\ninterface StandardSchemaV1<Output = unknown> {\n '~standard': {\n validate(value: unknown): StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;\n };\n}\n\ntype StandardSchemaResult<Output> =\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> };\n\n// ---------------------------------------------------------------------------\n// Sync validate helper\n// ---------------------------------------------------------------------------\n\n/**\n * Zod v4's ~standard.validate() signature includes Promise in the return union\n * to satisfy the Standard Schema spec, but in practice Zod always validates\n * synchronously for the schema types we use. We assert the result is sync and\n * throw if it isn't — search params parsing must be synchronous.\n */\nfunction validateSync<Output>(\n schema: StandardSchemaV1<Output>,\n value: unknown\n): StandardSchemaResult<Output> {\n const result = schema['~standard'].validate(value);\n if (result instanceof Promise) {\n throw new Error(\n '[timber] fromSchema: schema returned a Promise — only sync schemas are supported for search params.'\n );\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// fromSchema — bridge from Standard Schema to SearchParamCodec\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema-compatible schema (Zod, Valibot, ArkType) to a\n * SearchParamCodec.\n *\n * Parse: coerces the raw URL string through the schema. On validation failure,\n * parses `undefined` to get the schema's default value (the schema should have\n * a `.default()` call). If that also fails, returns `undefined`.\n *\n * Serialize: uses `String()` for primitives, `null` for null/undefined.\n *\n * ```ts\n * import { fromSchema } from '@timber-js/app/search-params'\n * import { z } from 'zod/v4'\n *\n * const pageCodec = fromSchema(z.coerce.number().int().min(1).default(1))\n * ```\n */\nexport function fromSchema<T>(schema: StandardSchemaV1<T>): SearchParamCodec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // For array inputs, take the last value (consistent with URLSearchParams.get())\n const input = Array.isArray(value) ? value[value.length - 1] : value;\n\n // Try parsing the raw value\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // On failure, try parsing undefined to get the default\n const defaultResult = validateSync(schema, undefined);\n if (!defaultResult.issues) {\n return defaultResult.value;\n }\n\n // No default available — return undefined (codec design choice)\n return undefined as T;\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n return String(value);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// fromArraySchema — bridge for array-valued search params\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema for array values. Handles both single strings\n * and repeated query keys (`?tag=a&tag=b`).\n *\n * ```ts\n * import { fromArraySchema } from '@timber-js/app/search-params'\n * import { z } from 'zod/v4'\n *\n * const tagsCodec = fromArraySchema(z.array(z.string()).default([]))\n * ```\n */\nexport function fromArraySchema<T>(schema: StandardSchemaV1<T>): SearchParamCodec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // Coerce single string to array for array schemas\n let input: unknown = value;\n if (typeof value === 'string') {\n input = [value];\n } else if (value === undefined) {\n input = undefined;\n }\n\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // On failure, try undefined for default\n const defaultResult = validateSync(schema, undefined);\n if (!defaultResult.issues) {\n return defaultResult.value;\n }\n\n return undefined as T;\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (Array.isArray(value)) {\n return value.length === 0 ? null : value.join(',');\n }\n return String(value);\n },\n };\n}\n","/**\n * defineSearchParams — factory for SearchParamsDefinition<T>.\n *\n * Creates a typed, composable definition for a route's search parameters.\n * Accepts both SearchParamCodec values and Standard Schema objects (Zod,\n * Valibot, ArkType) with auto-detection. Supports URL key aliasing via\n * withUrlKey(), default-omission serialization, and composition via\n * .extend() / .pick().\n *\n * Design doc: design/23-search-params.md §\"defineSearchParams — The Factory\"\n */\n\nimport { useQueryStates as clientUseQueryStates } from '../client/use-query-states.js';\nimport { fromSchema } from './codecs.js';\nimport type { Codec } from '../codec.js';\n\n// Server-only reference for .load() — avoids pulling server ALS into client bundles.\n// In client environments, .load() throws before reaching this code path.\n//\n// IMPORTANT: This is set eagerly via _setRawSearchParamsFn() at server startup\n// (called from request-context.ts module initialization). It must NOT use\n// dynamic `await import()` at call time because the async microtask from the\n// dynamic import loses AsyncLocalStorage context in React's RSC Flight renderer,\n// breaking rawSearchParams() in parallel slot pages. See TIM-523.\nlet _rawSearchParams: (() => Promise<URLSearchParams>) | undefined;\n\n/**\n * Register the rawSearchParams function. Called once at module load time\n * from request-context.ts to avoid dynamic import at call time.\n * @internal\n */\nexport function _setRawSearchParamsFn(fn: () => Promise<URLSearchParams>): void {\n _rawSearchParams = fn;\n}\n\nfunction getRawSearchParams(): Promise<URLSearchParams> {\n if (!_rawSearchParams) {\n throw new Error(\n '[timber] searchParams.load() is only available on the server. ' +\n 'Use searchParams.useQueryStates() on the client.'\n );\n }\n return _rawSearchParams();\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * A codec that converts between URL string values and typed values.\n *\n * nuqs parsers implement this interface natively — no adapter needed.\n * Standard Schema objects (Zod, Valibot, ArkType) are auto-detected\n * by defineSearchParams and wrapped via fromSchema.\n */\nexport interface SearchParamCodec<T> extends Codec<T> {\n /** Optional URL key alias, set by withUrlKey(). */\n urlKey?: string;\n}\n\n/** A codec with a URL key alias attached via withUrlKey(). */\nexport interface SearchParamCodecWithUrlKey<T> extends SearchParamCodec<T> {\n urlKey: string;\n}\n\n/** Infer the output type of a codec. */\nexport type InferCodec<C> = C extends SearchParamCodec<infer T> ? T : never;\n\n/** Map of property names to codecs. */\nexport type CodecMap<T extends Record<string, unknown>> = {\n [K in keyof T]: SearchParamCodec<T[K]>;\n};\n\n/** Options for useQueryStates setter. */\nexport interface SetParamsOptions {\n /** Update URL without server roundtrip (default: false). */\n shallow?: boolean;\n /** Scroll to top after update (default: true). */\n scroll?: boolean;\n /** 'push' (default) or 'replace' for history state. */\n history?: 'push' | 'replace';\n}\n\n/** Setter function returned by useQueryStates. */\nexport type SetParams<T> = (values: Partial<T>, options?: SetParamsOptions) => void;\n\n/** Options for useQueryStates hook. */\nexport interface QueryStatesOptions {\n /** Update URL without server roundtrip (default: false). */\n shallow?: boolean;\n /** Scroll to top after update (default: true). */\n scroll?: boolean;\n /** 'push' (default) or 'replace' for history state. */\n history?: 'push' | 'replace';\n}\n\n/**\n * A fully typed, composable search params definition.\n *\n * Returned by defineSearchParams(). Carries a phantom _type property\n * for build-time type extraction.\n */\nexport interface SearchParamsDefinition<T extends Record<string, unknown>> {\n /** Parse raw URL search params into typed values. */\n parse(raw: URLSearchParams | Record<string, string | string[] | undefined>): T;\n /** Parse a Promise of URLSearchParams (e.g., from the ALS `searchParams()` API). */\n parse(raw: Promise<URLSearchParams | Record<string, string | string[] | undefined>>): Promise<T>;\n\n /**\n * Load typed search params from the current request context (ALS-backed).\n *\n * Server-only — reads rawSearchParams() from ALS and parses through codecs.\n * Throws on client. Eliminates the naming conflict between the definition\n * export and the server helper.\n *\n * ```tsx\n * // app/products/page.tsx\n * import { searchParams } from './params'\n * export default async function Page() {\n * const { page, category } = await searchParams.load()\n * }\n * ```\n */\n load(): Promise<T>;\n\n /** Client hook — reads current URL params and returns typed values + setter. */\n useQueryStates(options?: QueryStatesOptions): [T, SetParams<T>];\n\n /** Extend with additional codecs or Standard Schema objects. */\n extend<U extends Record<string, SearchParamCodec<unknown> | StandardSchemaV1<unknown>>>(\n codecs: U\n ): SearchParamsDefinition<T & { [K in keyof U]: InferField<U[K]> }>;\n\n /** Pick a subset of keys. Preserves codecs and aliases. */\n pick<K extends keyof T & string>(...keys: K[]): SearchParamsDefinition<Pick<T, K>>;\n\n /** Serialize values to a query string (no leading '?'), omitting defaults. */\n serialize(values: Partial<T>): string;\n\n /** Build a full path with query string, omitting defaults. */\n href(pathname: string, values: Partial<T>): string;\n\n /** Build a URLSearchParams instance, omitting defaults. */\n toSearchParams(values: Partial<T>): URLSearchParams;\n\n /** Read-only codec map for spreading into .extend(). */\n codecs: { [K in keyof T]: SearchParamCodec<T[K]> };\n\n /** Read-only URL key alias map. Maps property names to URL query parameter keys. */\n readonly urlKeys: Readonly<Record<string, string>>;\n\n /**\n * Phantom property for build-time type extraction.\n * Never set at runtime — exists only in the type system.\n */\n readonly _type?: T;\n}\n\n// ---------------------------------------------------------------------------\n// Standard Schema interface (subset)\n//\n// Standard Schema (https://github.com/standard-schema/standard-schema) defines\n// a minimal interface that Zod ≥3.24, Valibot ≥1.0, and ArkType all implement.\n// We check for the '~standard' property to auto-detect schemas.\n// ---------------------------------------------------------------------------\n\n/** Minimal Standard Schema interface for auto-detection. */\nexport interface StandardSchemaV1<Output = unknown> {\n '~standard': {\n validate(\n value: unknown\n ):\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> }\n | Promise<\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> }\n >;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Type-level helpers\n// ---------------------------------------------------------------------------\n\n/** Infer the output type from either a SearchParamCodec or a StandardSchemaV1. */\nexport type InferField<V> =\n V extends SearchParamCodec<infer T> ? T : V extends StandardSchemaV1<infer T> ? T : never;\n\n/** Acceptable field value for defineSearchParams: a codec or a Standard Schema. */\nexport type SearchParamField<T = unknown> = SearchParamCodec<T> | StandardSchemaV1<T>;\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Convert URLSearchParams or a plain record to a normalized record\n * where repeated keys produce arrays.\n */\nfunction normalizeRaw(\n raw: URLSearchParams | Record<string, string | string[] | undefined>\n): Record<string, string | string[] | undefined> {\n if (raw instanceof URLSearchParams) {\n const result: Record<string, string | string[] | undefined> = {};\n for (const key of new Set(raw.keys())) {\n const values = raw.getAll(key);\n result[key] = values.length === 1 ? values[0] : values;\n }\n return result;\n }\n return raw;\n}\n\n/**\n * Compute the serialized default value for a codec. Used for\n * default-omission: when serialize(value) === serialize(parse(undefined)),\n * the field is omitted from the URL.\n */\nfunction getDefaultSerialized<T>(codec: SearchParamCodec<T>): string | null {\n return codec.serialize(codec.parse(undefined));\n}\n\n/** Check if a value is a Standard Schema object. */\nfunction isStandardSchema(value: unknown): value is StandardSchemaV1 {\n return (\n typeof value === 'object' &&\n value !== null &&\n '~standard' in value &&\n typeof (value as StandardSchemaV1)['~standard']?.validate === 'function'\n );\n}\n\n/** Check if a value is a SearchParamCodec. */\nfunction isCodec(value: unknown): value is SearchParamCodec<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as SearchParamCodec<unknown>).parse === 'function' &&\n typeof (value as SearchParamCodec<unknown>).serialize === 'function'\n );\n}\n\n/**\n * Resolve a field value to a SearchParamCodec. Auto-detects Standard Schema\n * objects and wraps them with fromSchema. Reads .urlKey from codecs.\n */\nfunction resolveField(\n fieldName: string,\n value: SearchParamField\n): { codec: SearchParamCodec<unknown>; urlKey?: string } {\n // Check for codec first (codecs may also have '~standard' if they're nuqs parsers)\n if (isCodec(value)) {\n return { codec: value, urlKey: value.urlKey };\n }\n\n // Auto-detect Standard Schema\n if (isStandardSchema(value)) {\n return { codec: fromSchema(value) };\n }\n\n throw new Error(\n `[timber] defineSearchParams: field '${fieldName}' is not a valid codec or Standard Schema. ` +\n `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +\n `(Zod, Valibot, ArkType).`\n );\n}\n\n/**\n * Validate that all codecs handle absent params (parse(undefined) doesn't throw).\n * Catches schemas that throw on missing input. `undefined` and `null` are both\n * valid defaults — `undefined` is correct for optional fields (e.g., `z.string().optional()`).\n */\nfunction validateDefaults(codecMap: Record<string, SearchParamCodec<unknown>>): void {\n for (const [key, codec] of Object.entries(codecMap)) {\n try {\n codec.parse(undefined);\n } catch {\n throw new Error(\n `[timber] defineSearchParams: field '${key}' throws when the param is absent.\\n` +\n ` Search params are optional — the URL might not contain ?${key}=anything.\\n` +\n ` Add .default() or .optional() to your schema, or wrap with withDefault().`\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a SearchParamsDefinition from a map of codecs and/or Standard Schema\n * objects. Accepts both SearchParamCodec values and raw Zod/Valibot/ArkType\n * schemas with auto-detection.\n *\n * ```ts\n * import { defineSearchParams, withDefault, withUrlKey } from '@timber-js/app/search-params'\n * import { parseAsString, parseAsStringEnum } from 'nuqs'\n * import { z } from 'zod/v4'\n *\n * export const searchParams = defineSearchParams({\n * page: z.coerce.number().int().min(1).default(1), // Standard Schema — auto-wrapped\n * q: withUrlKey(parseAsString, 'search'), // nuqs codec with URL alias\n * sort: withDefault(parseAsStringEnum(['price', 'name']), 'price'),\n * })\n * ```\n */\nexport function defineSearchParams<C extends Record<string, SearchParamField>>(\n codecs: C\n): SearchParamsDefinition<{ [K in keyof C]: InferField<C[K]> }> {\n type T = { [K in keyof C]: InferField<C[K]> };\n\n const resolvedCodecs: Record<string, SearchParamCodec<unknown>> = {};\n const urlKeys: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(codecs)) {\n const resolved = resolveField(key, value as SearchParamField);\n resolvedCodecs[key] = resolved.codec;\n if (resolved.urlKey) {\n urlKeys[key] = resolved.urlKey;\n }\n }\n\n // Validate that all codecs handle absent params\n validateDefaults(resolvedCodecs);\n\n return buildDefinition<T>(resolvedCodecs as unknown as CodecMap<T>, urlKeys);\n}\n\n// ---------------------------------------------------------------------------\n// Internal: build the definition object\n// ---------------------------------------------------------------------------\n\n/**\n * Internal: build a SearchParamsDefinition from a typed codec map and url keys.\n */\nfunction buildDefinition<T extends Record<string, unknown>>(\n codecMap: CodecMap<T>,\n urlKeys: Record<string, string>\n): SearchParamsDefinition<T> {\n // Pre-compute default serialized values for omission check\n const defaultSerialized: Record<string, string | null> = {};\n for (const key of Object.keys(codecMap)) {\n defaultSerialized[key] = getDefaultSerialized(codecMap[key as keyof T]);\n }\n\n function getUrlKey(prop: string): string {\n return urlKeys[prop] ?? prop;\n }\n\n // ---- parse ----\n function parseSync(raw: URLSearchParams | Record<string, string | string[] | undefined>): T {\n const normalized = normalizeRaw(raw);\n const result: Record<string, unknown> = {};\n\n for (const prop of Object.keys(codecMap)) {\n const urlKey = getUrlKey(prop);\n const rawValue = normalized[urlKey];\n result[prop] = (codecMap[prop as keyof T] as SearchParamCodec<unknown>).parse(rawValue);\n }\n\n return result as T;\n }\n\n // Overloaded parse: sync when given raw params, async when given a Promise.\n // This enables the ergonomic pattern: await def.parse(searchParams())\n function parse(raw: URLSearchParams | Record<string, string | string[] | undefined>): T;\n function parse(\n raw: Promise<URLSearchParams | Record<string, string | string[] | undefined>>\n ): Promise<T>;\n function parse(\n raw:\n | URLSearchParams\n | Record<string, string | string[] | undefined>\n | Promise<URLSearchParams | Record<string, string | string[] | undefined>>\n ): T | Promise<T> {\n if (raw instanceof Promise) {\n return raw.then(parseSync);\n }\n return parseSync(raw);\n }\n\n // ---- serialize ----\n function serialize(values: Partial<T>): string {\n const parts: string[] = [];\n\n for (const prop of Object.keys(codecMap)) {\n if (!(prop in values)) continue;\n const codec = codecMap[prop as keyof T] as SearchParamCodec<unknown>;\n const serialized = codec.serialize(values[prop as keyof T] as unknown);\n\n // Omit if serialized value matches the default\n if (serialized === defaultSerialized[prop]) continue;\n if (serialized === null) continue;\n\n parts.push(`${encodeURIComponent(getUrlKey(prop))}=${encodeURIComponent(serialized)}`);\n }\n\n return parts.join('&');\n }\n\n // ---- href ----\n function href(pathname: string, values: Partial<T>): string {\n const qs = serialize(values);\n return qs ? `${pathname}?${qs}` : pathname;\n }\n\n // ---- toSearchParams ----\n function toSearchParams(values: Partial<T>): URLSearchParams {\n const usp = new URLSearchParams();\n\n for (const prop of Object.keys(codecMap)) {\n if (!(prop in values)) continue;\n const codec = codecMap[prop as keyof T] as SearchParamCodec<unknown>;\n const serialized = codec.serialize(values[prop as keyof T] as unknown);\n\n if (serialized === defaultSerialized[prop]) continue;\n if (serialized === null) continue;\n\n usp.set(getUrlKey(prop), serialized);\n }\n\n return usp;\n }\n\n // ---- extend ----\n function extend<U extends Record<string, SearchParamCodec<unknown> | StandardSchemaV1<unknown>>>(\n newCodecs: U\n ): SearchParamsDefinition<T & { [K in keyof U]: InferField<U[K]> }> {\n type Combined = T & { [K in keyof U]: InferField<U[K]> };\n\n // Resolve any Standard Schema objects in the extension\n const resolvedNewCodecs: Record<string, SearchParamCodec<unknown>> = {};\n const newUrlKeys: Record<string, string> = {};\n for (const [key, value] of Object.entries(newCodecs)) {\n const resolved = resolveField(key, value as SearchParamField);\n resolvedNewCodecs[key] = resolved.codec;\n if (resolved.urlKey) {\n newUrlKeys[key] = resolved.urlKey;\n }\n }\n\n const combinedCodecs = {\n ...codecMap,\n ...resolvedNewCodecs,\n } as unknown as CodecMap<Combined>;\n\n // Merge URL keys: base keys + new codec urlKeys from withUrlKey\n const combinedUrlKeys: Record<string, string> = { ...urlKeys, ...newUrlKeys };\n\n return buildDefinition<Combined>(combinedCodecs, combinedUrlKeys);\n }\n\n // ---- pick ----\n function pick<K extends keyof T & string>(...keys: K[]): SearchParamsDefinition<Pick<T, K>> {\n const pickedCodecs: Record<string, SearchParamCodec<unknown>> = {};\n const pickedUrlKeys: Record<string, string> = {};\n\n for (const key of keys) {\n pickedCodecs[key] = codecMap[key] as SearchParamCodec<unknown>;\n if (key in urlKeys) {\n pickedUrlKeys[key] = urlKeys[key];\n }\n }\n\n return buildDefinition<Pick<T, K>>(\n pickedCodecs as unknown as CodecMap<Pick<T, K>>,\n pickedUrlKeys\n );\n }\n\n // ---- useQueryStates ----\n // Delegates to the 'use client' implementation from use-query-states.ts.\n //\n // In the RSC environment: use-query-states.ts is transformed by the RSC\n // plugin into a client reference proxy. Calling it throws — correct,\n // because hooks can't run during server component rendering.\n // In SSR: use-query-states.ts is the real nuqs-backed function. Hooks\n // work during SSR's renderToReadableStream, so this works correctly.\n // On the client: same as SSR — the real function is available.\n function useQueryStates(options?: QueryStatesOptions): [T, SetParams<T>] {\n return clientUseQueryStates(codecMap, options, Object.freeze({ ...urlKeys })) as [\n T,\n SetParams<T>,\n ];\n }\n\n // ---- load ----\n // ALS-backed: reads rawSearchParams() from the current request context\n // and parses through codecs. Server-only — throws on client.\n async function load(): Promise<T> {\n if (typeof window !== 'undefined') {\n throw new Error(\n '[timber] searchParams.load() is server-only. ' +\n 'Use searchParams.useQueryStates() on the client.'\n );\n }\n const raw = await getRawSearchParams();\n return parseSync(raw);\n }\n\n const definition: SearchParamsDefinition<T> = {\n parse,\n load,\n useQueryStates,\n extend,\n pick,\n serialize,\n href,\n toSearchParams,\n codecs: codecMap,\n urlKeys: Object.freeze({ ...urlKeys }),\n };\n\n return definition;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"define-hdajFTq7.js","names":[],"sources":["../../src/search-params/codecs.ts","../../src/search-params/define.ts"],"sourcesContent":["/**\n * Built-in codecs and the fromSchema bridge for Standard Schema-compatible\n * validation libraries (Zod, Valibot, ArkType).\n *\n * Design doc: design/09-typescript.md §\"The SearchParamCodec Protocol\"\n */\n\nimport type { SearchParamCodec } from './define.js';\n\n// ---------------------------------------------------------------------------\n// Standard Schema interface (subset)\n//\n// Standard Schema (https://github.com/standard-schema/standard-schema) defines\n// a minimal interface that Zod ≥3.24, Valibot ≥1.0, and ArkType all implement.\n// We depend only on `~standard.validate` to avoid coupling to any specific lib.\n// ---------------------------------------------------------------------------\n\ninterface StandardSchemaV1<Output = unknown> {\n '~standard': {\n validate(value: unknown): StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;\n };\n}\n\ntype StandardSchemaResult<Output> =\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> };\n\n// ---------------------------------------------------------------------------\n// Sync validate helper\n// ---------------------------------------------------------------------------\n\n/**\n * Zod v4's ~standard.validate() signature includes Promise in the return union\n * to satisfy the Standard Schema spec, but in practice Zod always validates\n * synchronously for the schema types we use. We assert the result is sync and\n * throw if it isn't — search params parsing must be synchronous.\n */\nfunction validateSync<Output>(\n schema: StandardSchemaV1<Output>,\n value: unknown\n): StandardSchemaResult<Output> {\n const result = schema['~standard'].validate(value);\n if (result instanceof Promise) {\n throw new Error(\n '[timber] fromSchema: schema returned a Promise — only sync schemas are supported for search params.'\n );\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// fromSchema — bridge from Standard Schema to SearchParamCodec\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema-compatible schema (Zod, Valibot, ArkType) to a\n * SearchParamCodec.\n *\n * Parse: coerces the raw URL string through the schema. On validation failure,\n * parses `undefined` to get the schema's default value (the schema should have\n * a `.default()` call). If that also fails, returns `undefined`.\n *\n * Serialize: uses `String()` for primitives, `null` for null/undefined.\n *\n * ```ts\n * import { fromSchema } from '@timber-js/app/search-params'\n * import { z } from 'zod/v4'\n *\n * const pageCodec = fromSchema(z.coerce.number().int().min(1).default(1))\n * ```\n */\nexport function fromSchema<T>(schema: StandardSchemaV1<T>): SearchParamCodec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // For array inputs, take the last value (consistent with URLSearchParams.get())\n const input = Array.isArray(value) ? value[value.length - 1] : value;\n\n // Try parsing the raw value\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // On failure, try parsing undefined to get the default\n const defaultResult = validateSync(schema, undefined);\n if (!defaultResult.issues) {\n return defaultResult.value;\n }\n\n // No default available — return undefined (codec design choice)\n return undefined as T;\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n return String(value);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// fromArraySchema — bridge for array-valued search params\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema for array values. Handles both single strings\n * and repeated query keys (`?tag=a&tag=b`).\n *\n * ```ts\n * import { fromArraySchema } from '@timber-js/app/search-params'\n * import { z } from 'zod/v4'\n *\n * const tagsCodec = fromArraySchema(z.array(z.string()).default([]))\n * ```\n */\nexport function fromArraySchema<T>(schema: StandardSchemaV1<T>): SearchParamCodec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // Coerce single string to array for array schemas\n let input: unknown = value;\n if (typeof value === 'string') {\n input = [value];\n } else if (value === undefined) {\n input = undefined;\n }\n\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // On failure, try undefined for default\n const defaultResult = validateSync(schema, undefined);\n if (!defaultResult.issues) {\n return defaultResult.value;\n }\n\n return undefined as T;\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (Array.isArray(value)) {\n return value.length === 0 ? null : value.join(',');\n }\n return String(value);\n },\n };\n}\n","/**\n * defineSearchParams — factory for SearchParamsDefinition<T>.\n *\n * Creates a typed, composable definition for a route's search parameters.\n * Accepts both SearchParamCodec values and Standard Schema objects (Zod,\n * Valibot, ArkType) with auto-detection. Supports URL key aliasing via\n * withUrlKey(), default-omission serialization, and composition via\n * .extend() / .pick().\n *\n * Design doc: design/23-search-params.md §\"defineSearchParams — The Factory\"\n */\n\nimport { useQueryStates as clientUseQueryStates } from '../client/use-query-states.js';\nimport { fromSchema } from './codecs.js';\nimport type { Codec } from '../codec.js';\n\n// Server-only reference for .load() — avoids pulling server ALS into client bundles.\n// In client environments, .load() throws before reaching this code path.\n//\n// IMPORTANT: This is set eagerly via _setRawSearchParamsFn() at server startup\n// (called from request-context.ts module initialization). It must NOT use\n// dynamic `await import()` at call time because the async microtask from the\n// dynamic import loses AsyncLocalStorage context in React's RSC Flight renderer,\n// breaking rawSearchParams() in parallel slot pages. See TIM-523.\nlet _rawSearchParams: (() => Promise<URLSearchParams>) | undefined;\n\n/**\n * Register the rawSearchParams function. Called once at module load time\n * from request-context.ts to avoid dynamic import at call time.\n * @internal\n */\nexport function _setRawSearchParamsFn(fn: () => Promise<URLSearchParams>): void {\n _rawSearchParams = fn;\n}\n\nfunction getRawSearchParams(): Promise<URLSearchParams> {\n if (!_rawSearchParams) {\n throw new Error(\n '[timber] searchParams.load() is only available on the server. ' +\n 'Use searchParams.useQueryStates() on the client.'\n );\n }\n return _rawSearchParams();\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * A codec that converts between URL string values and typed values.\n *\n * nuqs parsers implement this interface natively — no adapter needed.\n * Standard Schema objects (Zod, Valibot, ArkType) are auto-detected\n * by defineSearchParams and wrapped via fromSchema.\n */\nexport interface SearchParamCodec<T> extends Codec<T> {\n /** Optional URL key alias, set by withUrlKey(). */\n urlKey?: string;\n}\n\n/** A codec with a URL key alias attached via withUrlKey(). */\nexport interface SearchParamCodecWithUrlKey<T> extends SearchParamCodec<T> {\n urlKey: string;\n}\n\n/** Infer the output type of a codec. */\nexport type InferCodec<C> = C extends SearchParamCodec<infer T> ? T : never;\n\n/** Map of property names to codecs. */\nexport type CodecMap<T extends Record<string, unknown>> = {\n [K in keyof T]: SearchParamCodec<T[K]>;\n};\n\n/** Options for useQueryStates setter. */\nexport interface SetParamsOptions {\n /** Update URL without server roundtrip (default: false). */\n shallow?: boolean;\n /** Scroll to top after update (default: true). */\n scroll?: boolean;\n /** 'push' (default) or 'replace' for history state. */\n history?: 'push' | 'replace';\n}\n\n/** Setter function returned by useQueryStates. */\nexport type SetParams<T> = (values: Partial<T>, options?: SetParamsOptions) => void;\n\n/** Options for useQueryStates hook. */\nexport interface QueryStatesOptions {\n /** Update URL without server roundtrip (default: false). */\n shallow?: boolean;\n /** Scroll to top after update (default: true). */\n scroll?: boolean;\n /** 'push' (default) or 'replace' for history state. */\n history?: 'push' | 'replace';\n}\n\n/**\n * A fully typed, composable search params definition.\n *\n * Returned by defineSearchParams(). Carries a phantom _type property\n * for build-time type extraction.\n */\nexport interface SearchParamsDefinition<T extends Record<string, unknown>> {\n /** Parse raw URL search params into typed values. */\n parse(raw: URLSearchParams | Record<string, string | string[] | undefined>): T;\n /** Parse a Promise of URLSearchParams (e.g., from the ALS `searchParams()` API). */\n parse(raw: Promise<URLSearchParams | Record<string, string | string[] | undefined>>): Promise<T>;\n\n /**\n * Load typed search params from the current request context (ALS-backed).\n *\n * Server-only — reads rawSearchParams() from ALS and parses through codecs.\n * Throws on client. Eliminates the naming conflict between the definition\n * export and the server helper.\n *\n * ```tsx\n * // app/products/page.tsx\n * import { searchParams } from './params'\n * export default async function Page() {\n * const { page, category } = await searchParams.load()\n * }\n * ```\n */\n load(): Promise<T>;\n\n /** Client hook — reads current URL params and returns typed values + setter. */\n useQueryStates(options?: QueryStatesOptions): [T, SetParams<T>];\n\n /** Extend with additional codecs or Standard Schema objects. */\n extend<U extends Record<string, SearchParamCodec<unknown> | StandardSchemaV1<unknown>>>(\n codecs: U\n ): SearchParamsDefinition<T & { [K in keyof U]: InferField<U[K]> }>;\n\n /** Pick a subset of keys. Preserves codecs and aliases. */\n pick<K extends keyof T & string>(...keys: K[]): SearchParamsDefinition<Pick<T, K>>;\n\n /** Serialize values to a query string (no leading '?'), omitting defaults. */\n serialize(values: Partial<T>): string;\n\n /** Build a full path with query string, omitting defaults. */\n href(pathname: string, values: Partial<T>): string;\n\n /** Build a URLSearchParams instance, omitting defaults. */\n toSearchParams(values: Partial<T>): URLSearchParams;\n\n /** Read-only codec map for spreading into .extend(). */\n codecs: { [K in keyof T]: SearchParamCodec<T[K]> };\n\n /** Read-only URL key alias map. Maps property names to URL query parameter keys. */\n readonly urlKeys: Readonly<Record<string, string>>;\n\n /**\n * Phantom property for build-time type extraction.\n * Never set at runtime — exists only in the type system.\n */\n readonly _type?: T;\n}\n\n// ---------------------------------------------------------------------------\n// Standard Schema interface (subset)\n//\n// Standard Schema (https://github.com/standard-schema/standard-schema) defines\n// a minimal interface that Zod ≥3.24, Valibot ≥1.0, and ArkType all implement.\n// We check for the '~standard' property to auto-detect schemas.\n// ---------------------------------------------------------------------------\n\n/** Minimal Standard Schema interface for auto-detection. */\nexport interface StandardSchemaV1<Output = unknown> {\n '~standard': {\n validate(\n value: unknown\n ):\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> }\n | Promise<\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> }\n >;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Type-level helpers\n// ---------------------------------------------------------------------------\n\n/** Infer the output type from either a SearchParamCodec or a StandardSchemaV1. */\nexport type InferField<V> =\n V extends SearchParamCodec<infer T> ? T : V extends StandardSchemaV1<infer T> ? T : never;\n\n/** Acceptable field value for defineSearchParams: a codec or a Standard Schema. */\nexport type SearchParamField<T = unknown> = SearchParamCodec<T> | StandardSchemaV1<T>;\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Convert URLSearchParams or a plain record to a normalized record\n * where repeated keys produce arrays.\n */\nfunction normalizeRaw(\n raw: URLSearchParams | Record<string, string | string[] | undefined>\n): Record<string, string | string[] | undefined> {\n if (raw instanceof URLSearchParams) {\n const result: Record<string, string | string[] | undefined> = {};\n for (const key of new Set(raw.keys())) {\n const values = raw.getAll(key);\n result[key] = values.length === 1 ? values[0] : values;\n }\n return result;\n }\n return raw;\n}\n\n/**\n * Compute the serialized default value for a codec. Used for\n * default-omission: when serialize(value) === serialize(parse(undefined)),\n * the field is omitted from the URL.\n */\nfunction getDefaultSerialized<T>(codec: SearchParamCodec<T>): string | null {\n return codec.serialize(codec.parse(undefined));\n}\n\n/** Check if a value is a Standard Schema object. */\nfunction isStandardSchema(value: unknown): value is StandardSchemaV1 {\n return (\n typeof value === 'object' &&\n value !== null &&\n '~standard' in value &&\n typeof (value as StandardSchemaV1)['~standard']?.validate === 'function'\n );\n}\n\n/** Check if a value is a SearchParamCodec. */\nfunction isCodec(value: unknown): value is SearchParamCodec<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as SearchParamCodec<unknown>).parse === 'function' &&\n typeof (value as SearchParamCodec<unknown>).serialize === 'function'\n );\n}\n\n/**\n * Resolve a field value to a SearchParamCodec. Auto-detects Standard Schema\n * objects and wraps them with fromSchema. Reads .urlKey from codecs.\n */\nfunction resolveField(\n fieldName: string,\n value: SearchParamField\n): { codec: SearchParamCodec<unknown>; urlKey?: string } {\n // Check for codec first (codecs may also have '~standard' if they're nuqs parsers)\n if (isCodec(value)) {\n return { codec: value, urlKey: value.urlKey };\n }\n\n // Auto-detect Standard Schema\n if (isStandardSchema(value)) {\n return { codec: fromSchema(value) };\n }\n\n throw new Error(\n `[timber] defineSearchParams: field '${fieldName}' is not a valid codec or Standard Schema. ` +\n `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +\n `(Zod, Valibot, ArkType).`\n );\n}\n\n/**\n * Validate that all codecs handle absent params (parse(undefined) doesn't throw).\n * Catches schemas that throw on missing input. `undefined` and `null` are both\n * valid defaults — `undefined` is correct for optional fields (e.g., `z.string().optional()`).\n */\nfunction validateDefaults(codecMap: Record<string, SearchParamCodec<unknown>>): void {\n for (const [key, codec] of Object.entries(codecMap)) {\n try {\n codec.parse(undefined);\n } catch {\n throw new Error(\n `[timber] defineSearchParams: field '${key}' throws when the param is absent.\\n` +\n ` Search params are optional — the URL might not contain ?${key}=anything.\\n` +\n ` Add .default() or .optional() to your schema, or wrap with withDefault().`\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a SearchParamsDefinition from a map of codecs and/or Standard Schema\n * objects. Accepts both SearchParamCodec values and raw Zod/Valibot/ArkType\n * schemas with auto-detection.\n *\n * ```ts\n * import { defineSearchParams, withDefault, withUrlKey } from '@timber-js/app/search-params'\n * import { parseAsString, parseAsStringEnum } from 'nuqs'\n * import { z } from 'zod/v4'\n *\n * export const searchParams = defineSearchParams({\n * page: z.coerce.number().int().min(1).default(1), // Standard Schema — auto-wrapped\n * q: withUrlKey(parseAsString, 'search'), // nuqs codec with URL alias\n * sort: withDefault(parseAsStringEnum(['price', 'name']), 'price'),\n * })\n * ```\n */\nexport function defineSearchParams<C extends Record<string, SearchParamField>>(\n codecs: C\n): SearchParamsDefinition<{ [K in keyof C]: InferField<C[K]> }> {\n type T = { [K in keyof C]: InferField<C[K]> };\n\n const resolvedCodecs: Record<string, SearchParamCodec<unknown>> = {};\n const urlKeys: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(codecs)) {\n const resolved = resolveField(key, value as SearchParamField);\n resolvedCodecs[key] = resolved.codec;\n if (resolved.urlKey) {\n urlKeys[key] = resolved.urlKey;\n }\n }\n\n // Validate that all codecs handle absent params\n validateDefaults(resolvedCodecs);\n\n return buildDefinition<T>(resolvedCodecs as unknown as CodecMap<T>, urlKeys);\n}\n\n// ---------------------------------------------------------------------------\n// Internal: build the definition object\n// ---------------------------------------------------------------------------\n\n/**\n * Internal: build a SearchParamsDefinition from a typed codec map and url keys.\n */\nfunction buildDefinition<T extends Record<string, unknown>>(\n codecMap: CodecMap<T>,\n urlKeys: Record<string, string>\n): SearchParamsDefinition<T> {\n // Pre-compute default serialized values for omission check\n const defaultSerialized: Record<string, string | null> = {};\n for (const key of Object.keys(codecMap)) {\n defaultSerialized[key] = getDefaultSerialized(codecMap[key as keyof T]);\n }\n\n function getUrlKey(prop: string): string {\n return urlKeys[prop] ?? prop;\n }\n\n // ---- parse ----\n function parseSync(raw: URLSearchParams | Record<string, string | string[] | undefined>): T {\n const normalized = normalizeRaw(raw);\n const result: Record<string, unknown> = {};\n\n for (const prop of Object.keys(codecMap)) {\n const urlKey = getUrlKey(prop);\n const rawValue = normalized[urlKey];\n result[prop] = (codecMap[prop as keyof T] as SearchParamCodec<unknown>).parse(rawValue);\n }\n\n return result as T;\n }\n\n // Overloaded parse: sync when given raw params, async when given a Promise.\n // This enables the ergonomic pattern: await def.parse(searchParams())\n function parse(raw: URLSearchParams | Record<string, string | string[] | undefined>): T;\n function parse(\n raw: Promise<URLSearchParams | Record<string, string | string[] | undefined>>\n ): Promise<T>;\n function parse(\n raw:\n | URLSearchParams\n | Record<string, string | string[] | undefined>\n | Promise<URLSearchParams | Record<string, string | string[] | undefined>>\n ): T | Promise<T> {\n if (raw instanceof Promise) {\n return raw.then(parseSync);\n }\n return parseSync(raw);\n }\n\n // ---- serialize ----\n function serialize(values: Partial<T>): string {\n const parts: string[] = [];\n\n for (const prop of Object.keys(codecMap)) {\n if (!(prop in values)) continue;\n const codec = codecMap[prop as keyof T] as SearchParamCodec<unknown>;\n const serialized = codec.serialize(values[prop as keyof T] as unknown);\n\n // Omit if serialized value matches the default\n if (serialized === defaultSerialized[prop]) continue;\n if (serialized === null) continue;\n\n parts.push(`${encodeURIComponent(getUrlKey(prop))}=${encodeURIComponent(serialized)}`);\n }\n\n return parts.join('&');\n }\n\n // ---- href ----\n function href(pathname: string, values: Partial<T>): string {\n const qs = serialize(values);\n return qs ? `${pathname}?${qs}` : pathname;\n }\n\n // ---- toSearchParams ----\n function toSearchParams(values: Partial<T>): URLSearchParams {\n const usp = new URLSearchParams();\n\n for (const prop of Object.keys(codecMap)) {\n if (!(prop in values)) continue;\n const codec = codecMap[prop as keyof T] as SearchParamCodec<unknown>;\n const serialized = codec.serialize(values[prop as keyof T] as unknown);\n\n if (serialized === defaultSerialized[prop]) continue;\n if (serialized === null) continue;\n\n usp.set(getUrlKey(prop), serialized);\n }\n\n return usp;\n }\n\n // ---- extend ----\n function extend<U extends Record<string, SearchParamCodec<unknown> | StandardSchemaV1<unknown>>>(\n newCodecs: U\n ): SearchParamsDefinition<T & { [K in keyof U]: InferField<U[K]> }> {\n type Combined = T & { [K in keyof U]: InferField<U[K]> };\n\n // Resolve any Standard Schema objects in the extension\n const resolvedNewCodecs: Record<string, SearchParamCodec<unknown>> = {};\n const newUrlKeys: Record<string, string> = {};\n for (const [key, value] of Object.entries(newCodecs)) {\n const resolved = resolveField(key, value as SearchParamField);\n resolvedNewCodecs[key] = resolved.codec;\n if (resolved.urlKey) {\n newUrlKeys[key] = resolved.urlKey;\n }\n }\n\n const combinedCodecs = {\n ...codecMap,\n ...resolvedNewCodecs,\n } as unknown as CodecMap<Combined>;\n\n // Merge URL keys: base keys + new codec urlKeys from withUrlKey\n const combinedUrlKeys: Record<string, string> = { ...urlKeys, ...newUrlKeys };\n\n return buildDefinition<Combined>(combinedCodecs, combinedUrlKeys);\n }\n\n // ---- pick ----\n function pick<K extends keyof T & string>(...keys: K[]): SearchParamsDefinition<Pick<T, K>> {\n const pickedCodecs: Record<string, SearchParamCodec<unknown>> = {};\n const pickedUrlKeys: Record<string, string> = {};\n\n for (const key of keys) {\n pickedCodecs[key] = codecMap[key] as SearchParamCodec<unknown>;\n if (key in urlKeys) {\n pickedUrlKeys[key] = urlKeys[key];\n }\n }\n\n return buildDefinition<Pick<T, K>>(\n pickedCodecs as unknown as CodecMap<Pick<T, K>>,\n pickedUrlKeys\n );\n }\n\n // ---- useQueryStates ----\n // Delegates to the 'use client' implementation from use-query-states.ts.\n //\n // In the RSC environment: use-query-states.ts is transformed by the RSC\n // plugin into a client reference proxy. Calling it throws — correct,\n // because hooks can't run during server component rendering.\n // In SSR: use-query-states.ts is the real nuqs-backed function. Hooks\n // work during SSR's renderToReadableStream, so this works correctly.\n // On the client: same as SSR — the real function is available.\n function useQueryStates(options?: QueryStatesOptions): [T, SetParams<T>] {\n return clientUseQueryStates(codecMap, options, Object.freeze({ ...urlKeys })) as [\n T,\n SetParams<T>,\n ];\n }\n\n // ---- load ----\n // ALS-backed: reads rawSearchParams() from the current request context\n // and parses through codecs. Server-only — throws on client.\n async function load(): Promise<T> {\n if (typeof window !== 'undefined') {\n throw new Error(\n '[timber] searchParams.load() is server-only. ' +\n 'Use searchParams.useQueryStates() on the client.'\n );\n }\n const raw = await getRawSearchParams();\n return parseSync(raw);\n }\n\n const definition: SearchParamsDefinition<T> = {\n parse,\n load,\n useQueryStates,\n extend,\n pick,\n serialize,\n href,\n toSearchParams,\n codecs: codecMap,\n urlKeys: Object.freeze({ ...urlKeys }),\n };\n\n return definition;\n}\n"],"mappings":";;;;;;;;AAqCA,SAAS,aACP,QACA,OAC8B;CAC9B,MAAM,SAAS,OAAO,aAAa,SAAS,MAAM;AAClD,KAAI,kBAAkB,QACpB,OAAM,IAAI,MACR,sGACD;AAEH,QAAO;;;;;;;;;;;;;;;;;;;AAwBT,SAAgB,WAAc,QAAkD;AAC9E,QAAO;EACL,MAAM,OAAyC;GAK7C,MAAM,SAAS,aAAa,QAHd,MAAM,QAAQ,MAAM,GAAG,MAAM,MAAM,SAAS,KAAK,MAGrB;AAC1C,OAAI,CAAC,OAAO,OACV,QAAO,OAAO;GAIhB,MAAM,gBAAgB,aAAa,QAAQ,KAAA,EAAU;AACrD,OAAI,CAAC,cAAc,OACjB,QAAO,cAAc;;EAOzB,UAAU,OAAyB;AACjC,OAAI,UAAU,QAAQ,UAAU,KAAA,EAC9B,QAAO;AAET,UAAO,OAAO,MAAM;;EAEvB;;;;;;;;;;;;;AAkBH,SAAgB,gBAAmB,QAAkD;AACnF,QAAO;EACL,MAAM,OAAyC;GAE7C,IAAI,QAAiB;AACrB,OAAI,OAAO,UAAU,SACnB,SAAQ,CAAC,MAAM;YACN,UAAU,KAAA,EACnB,SAAQ,KAAA;GAGV,MAAM,SAAS,aAAa,QAAQ,MAAM;AAC1C,OAAI,CAAC,OAAO,OACV,QAAO,OAAO;GAIhB,MAAM,gBAAgB,aAAa,QAAQ,KAAA,EAAU;AACrD,OAAI,CAAC,cAAc,OACjB,QAAO,cAAc;;EAMzB,UAAU,OAAyB;AACjC,OAAI,UAAU,QAAQ,UAAU,KAAA,EAC9B,QAAO;AAET,OAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,WAAW,IAAI,OAAO,MAAM,KAAK,IAAI;AAEpD,UAAO,OAAO,MAAM;;EAEvB;;;;;;;;;;;;;;;AC/HH,IAAI;;;;;;AAOJ,SAAgB,sBAAsB,IAA0C;AAC9E,oBAAmB;;AAGrB,SAAS,qBAA+C;AACtD,KAAI,CAAC,iBACH,OAAM,IAAI,MACR,iHAED;AAEH,QAAO,kBAAkB;;;;;;AA+J3B,SAAS,aACP,KAC+C;AAC/C,KAAI,eAAe,iBAAiB;EAClC,MAAM,SAAwD,EAAE;AAChE,OAAK,MAAM,OAAO,IAAI,IAAI,IAAI,MAAM,CAAC,EAAE;GACrC,MAAM,SAAS,IAAI,OAAO,IAAI;AAC9B,UAAO,OAAO,OAAO,WAAW,IAAI,OAAO,KAAK;;AAElD,SAAO;;AAET,QAAO;;;;;;;AAQT,SAAS,qBAAwB,OAA2C;AAC1E,QAAO,MAAM,UAAU,MAAM,MAAM,KAAA,EAAU,CAAC;;;AAIhD,SAAS,iBAAiB,OAA2C;AACnE,QACE,OAAO,UAAU,YACjB,UAAU,QACV,eAAe,SACf,OAAQ,MAA2B,cAAc,aAAa;;;AAKlE,SAAS,QAAQ,OAAoD;AACnE,QACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAAoC,UAAU,cACtD,OAAQ,MAAoC,cAAc;;;;;;AAQ9D,SAAS,aACP,WACA,OACuD;AAEvD,KAAI,QAAQ,MAAM,CAChB,QAAO;EAAE,OAAO;EAAO,QAAQ,MAAM;EAAQ;AAI/C,KAAI,iBAAiB,MAAM,CACzB,QAAO,EAAE,OAAO,WAAW,MAAM,EAAE;AAGrC,OAAM,IAAI,MACR,uCAAuC,UAAU,uJAGlD;;;;;;;AAQH,SAAS,iBAAiB,UAA2D;AACnF,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,CACjD,KAAI;AACF,QAAM,MAAM,KAAA,EAAU;SAChB;AACN,QAAM,IAAI,MACR,uCAAuC,IAAI,gGACoB,IAAI,yFAEpE;;;;;;;;;;;;;;;;;;;;AA0BP,SAAgB,mBACd,QAC8D;CAG9D,MAAM,iBAA4D,EAAE;CACpE,MAAM,UAAkC,EAAE;AAE1C,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,EAAE;EACjD,MAAM,WAAW,aAAa,KAAK,MAA0B;AAC7D,iBAAe,OAAO,SAAS;AAC/B,MAAI,SAAS,OACX,SAAQ,OAAO,SAAS;;AAK5B,kBAAiB,eAAe;AAEhC,QAAO,gBAAmB,gBAA0C,QAAQ;;;;;AAU9E,SAAS,gBACP,UACA,SAC2B;CAE3B,MAAM,oBAAmD,EAAE;AAC3D,MAAK,MAAM,OAAO,OAAO,KAAK,SAAS,CACrC,mBAAkB,OAAO,qBAAqB,SAAS,KAAgB;CAGzE,SAAS,UAAU,MAAsB;AACvC,SAAO,QAAQ,SAAS;;CAI1B,SAAS,UAAU,KAAyE;EAC1F,MAAM,aAAa,aAAa,IAAI;EACpC,MAAM,SAAkC,EAAE;AAE1C,OAAK,MAAM,QAAQ,OAAO,KAAK,SAAS,EAAE;GAExC,MAAM,WAAW,WADF,UAAU,KAAK;AAE9B,UAAO,QAAS,SAAS,MAA+C,MAAM,SAAS;;AAGzF,SAAO;;CAST,SAAS,MACP,KAIgB;AAChB,MAAI,eAAe,QACjB,QAAO,IAAI,KAAK,UAAU;AAE5B,SAAO,UAAU,IAAI;;CAIvB,SAAS,UAAU,QAA4B;EAC7C,MAAM,QAAkB,EAAE;AAE1B,OAAK,MAAM,QAAQ,OAAO,KAAK,SAAS,EAAE;AACxC,OAAI,EAAE,QAAQ,QAAS;GAEvB,MAAM,aADQ,SAAS,MACE,UAAU,OAAO,MAA4B;AAGtE,OAAI,eAAe,kBAAkB,MAAO;AAC5C,OAAI,eAAe,KAAM;AAEzB,SAAM,KAAK,GAAG,mBAAmB,UAAU,KAAK,CAAC,CAAC,GAAG,mBAAmB,WAAW,GAAG;;AAGxF,SAAO,MAAM,KAAK,IAAI;;CAIxB,SAAS,KAAK,UAAkB,QAA4B;EAC1D,MAAM,KAAK,UAAU,OAAO;AAC5B,SAAO,KAAK,GAAG,SAAS,GAAG,OAAO;;CAIpC,SAAS,eAAe,QAAqC;EAC3D,MAAM,MAAM,IAAI,iBAAiB;AAEjC,OAAK,MAAM,QAAQ,OAAO,KAAK,SAAS,EAAE;AACxC,OAAI,EAAE,QAAQ,QAAS;GAEvB,MAAM,aADQ,SAAS,MACE,UAAU,OAAO,MAA4B;AAEtE,OAAI,eAAe,kBAAkB,MAAO;AAC5C,OAAI,eAAe,KAAM;AAEzB,OAAI,IAAI,UAAU,KAAK,EAAE,WAAW;;AAGtC,SAAO;;CAIT,SAAS,OACP,WACkE;EAIlE,MAAM,oBAA+D,EAAE;EACvE,MAAM,aAAqC,EAAE;AAC7C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,UAAU,EAAE;GACpD,MAAM,WAAW,aAAa,KAAK,MAA0B;AAC7D,qBAAkB,OAAO,SAAS;AAClC,OAAI,SAAS,OACX,YAAW,OAAO,SAAS;;AAY/B,SAAO,gBARgB;GACrB,GAAG;GACH,GAAG;GACJ,EAG+C;GAAE,GAAG;GAAS,GAAG;GAAY,CAEZ;;CAInE,SAAS,KAAiC,GAAG,MAA+C;EAC1F,MAAM,eAA0D,EAAE;EAClE,MAAM,gBAAwC,EAAE;AAEhD,OAAK,MAAM,OAAO,MAAM;AACtB,gBAAa,OAAO,SAAS;AAC7B,OAAI,OAAO,QACT,eAAc,OAAO,QAAQ;;AAIjC,SAAO,gBACL,cACA,cACD;;CAYH,SAAS,iBAAe,SAAiD;AACvE,SAAO,eAAqB,UAAU,SAAS,OAAO,OAAO,EAAE,GAAG,SAAS,CAAC,CAAC;;CAS/E,eAAe,OAAmB;AAChC,MAAI,OAAO,WAAW,YACpB,OAAM,IAAI,MACR,gGAED;AAGH,SAAO,UADK,MAAM,oBAAoB,CACjB;;AAgBvB,QAb8C;EAC5C;EACA;EACA,gBAAA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR,SAAS,OAAO,OAAO,EAAE,GAAG,SAAS,CAAC;EACvC"}
|
|
@@ -208,4 +208,4 @@ function statusMatches(boundaryStatus, errorStatus) {
|
|
|
208
208
|
//#endregion
|
|
209
209
|
export { _setCachedSearch as a, cachedSearch as c, globalRouter as d, setSsrData as i, cachedSearchParams as l, clearSsrData as n, _setCurrentParams as o, getSsrData as r, _setGlobalRouter as s, TimberErrorBoundary as t, currentParams as u };
|
|
210
210
|
|
|
211
|
-
//# sourceMappingURL=error-boundary-
|
|
211
|
+
//# sourceMappingURL=error-boundary-A_sgyyUP.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-boundary-DpZJBCqh.js","names":[],"sources":["../../src/client/state.ts","../../src/client/ssr-data.ts","../../src/client/error-boundary.tsx"],"sourcesContent":["/**\n * Centralized client singleton state registry.\n *\n * ALL mutable module-level state that must have singleton semantics across\n * the client bundle lives here. Individual modules (router-ref.ts, ssr-data.ts,\n * use-params.ts, use-search-params.ts, unload-guard.ts) import from this file\n * and re-export thin wrapper functions.\n *\n * Why: In Vite dev, a module is instantiated separately if reached via different\n * import paths (e.g., relative `./foo.js` vs barrel `@timber-js/app/client`).\n * By centralizing all mutable state in a single module that is always reached\n * through the same dependency chain (barrel → wrapper → state.ts), we guarantee\n * a single instance of every piece of shared state.\n *\n * DO NOT import this file from outside client/. Server code must never depend\n * on client state. The barrel (client/index.ts) is the public entry point.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\".\n */\n\nimport type { RouterInstance } from './router.js';\nimport type { SsrData } from './ssr-data.js';\n\n// ─── Router (from router-ref.ts) ──────────────────────────────────────────\n\n/** The global router singleton — set once during bootstrap. */\nexport let globalRouter: RouterInstance | null = null;\n\nexport function _setGlobalRouter(router: RouterInstance | null): void {\n globalRouter = router;\n}\n\n// ─── SSR Data Provider (from ssr-data.ts) ──────────────────────────────────\n\n/**\n * ALS-backed SSR data provider. When registered, getSsrData() reads from\n * this function (ALS store) instead of module-level currentSsrData.\n */\nexport let ssrDataProvider: (() => SsrData | undefined) | undefined;\n\nexport function _setSsrDataProvider(provider: (() => SsrData | undefined) | undefined): void {\n ssrDataProvider = provider;\n}\n\n/** Fallback SSR data for tests and environments without ALS. */\nexport let currentSsrData: SsrData | undefined;\n\nexport function _setCurrentSsrData(data: SsrData | undefined): void {\n currentSsrData = data;\n}\n\n// ─── Route Params (from use-params.ts) ──────────────────────────────────────\n\n/** Current route params snapshot — replaced (not mutated) on each navigation. */\nexport let currentParams: Record<string, string | string[]> = {};\n\nexport function _setCurrentParams(params: Record<string, string | string[]>): void {\n currentParams = params;\n}\n\n/** Listeners notified when currentParams changes. */\nexport const paramsListeners = new Set<() => void>();\n\n// ─── Search Params Cache (from use-search-params.ts) ────────────────────────\n\n/** Cached search string — avoids reparsing when URL hasn't changed. */\nexport let cachedSearch = '';\nexport let cachedSearchParams = new URLSearchParams();\n\nexport function _setCachedSearch(search: string, params: URLSearchParams): void {\n cachedSearch = search;\n cachedSearchParams = params;\n}\n\n// ─── Unload Guard (from unload-guard.ts) ─────────────────────────────────────\n\n/** Whether the page is currently being unloaded. */\nexport let unloading = false;\n\nexport function _setUnloading(value: boolean): void {\n unloading = value;\n}\n","/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n */\n\nimport {\n ssrDataProvider,\n currentSsrData,\n _setSsrDataProvider,\n _setCurrentSsrData,\n} from './state.js';\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: Record<string, string | string[]>;\n /**\n * Mutable reference to NavContext for error boundary → RSC communication.\n * When TimberErrorBoundary catches a DenySignal, it sets\n * `_navContext._denyHandledByBoundary = true` to prevent the RSC entry\n * from promoting the denial to page-level. See LOCAL-298.\n */\n _navContext?: { _denyHandledByBoundary?: boolean };\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n//\n// Module singleton guarantee: In Vite's SSR environment, both\n// ssr-entry.ts (via #/client/ssr-data.js) and client component hooks\n// (via @timber-js/app/client) must resolve to the SAME module instance\n// of this file. The timber-shims plugin ensures this by remapping\n// @timber-js/app/client → src/client/index.ts in the SSR environment.\n// Without this remap, @timber-js/app/client resolves to dist/ (via\n// package.json exports), creating a split where registerSsrDataProvider\n// writes to one instance but getSsrData reads from another.\n// See timber-shims plugin resolveId for details.\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _setSsrDataProvider(provider);\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n _setCurrentSsrData(data);\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n _setCurrentSsrData(undefined);\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (ssrDataProvider) {\n return ssrDataProvider();\n }\n return currentSsrData;\n}\n","'use client';\n\n/**\n * Framework-injected React error boundary.\n *\n * Catches errors thrown by children and renders a fallback component\n * with the appropriate props based on error type:\n * - DenySignal (4xx) → { status, dangerouslyPassData }\n * - RenderError (5xx) → { error, digest, reset }\n * - Unhandled error → { error, digest: null, reset }\n *\n * The `status` prop controls which errors this boundary catches:\n * - Specific code (e.g. 403) → only that status\n * - Category (400) → any 4xx\n * - Category (500) → any 5xx\n * - Omitted → catches everything (error.tsx behavior)\n *\n * See design/10-error-handling.md §\"Status-Code Files\"\n */\n\nimport { Component, createElement, type ReactNode } from 'react';\nimport { getSsrData } from './ssr-data.js';\n\n// ─── Page Unload Detection ───────────────────────────────────────────────────\n// Track whether the page is being unloaded (user refreshed or navigated away).\n// When this is true, error boundaries suppress activation — the error is from\n// the aborted connection, not an application error.\nlet _isUnloading = false;\nif (typeof window !== 'undefined') {\n window.addEventListener('beforeunload', () => {\n _isUnloading = true;\n });\n window.addEventListener('pagehide', () => {\n _isUnloading = true;\n });\n}\n\n// ─── Digest Types ────────────────────────────────────────────────────────────\n\n/** Structured digest returned by RSC onError for DenySignal. */\ninterface DenyDigest {\n type: 'deny';\n status: number;\n data: unknown;\n}\n\n/** Structured digest returned by RSC onError for RenderError. */\ninterface RenderErrorDigest {\n type: 'render-error';\n code: string;\n data: unknown;\n status: number;\n}\n\n/** Structured digest returned by RSC onError for RedirectSignal. */\ninterface RedirectDigest {\n type: 'redirect';\n location: string;\n status: number;\n}\n\ntype ParsedDigest = DenyDigest | RenderErrorDigest | RedirectDigest;\n\n// ─── Props & State ───────────────────────────────────────────────────────────\n\nexport interface TimberErrorBoundaryProps {\n /** The component to render when an error is caught. */\n fallbackComponent?: (...args: unknown[]) => ReactNode;\n /**\n * Pre-rendered fallback element. Used for MDX status files which are server\n * components and cannot be passed as function props across the RSC→client\n * boundary. When set, rendered directly instead of calling fallbackComponent.\n *\n * See design/10-error-handling.md §\"Status-Code File Variants\" — MDX status\n * files are server components by default (zero client JS).\n */\n fallbackElement?: ReactNode;\n /**\n * Status code filter. If set, only catches errors matching this status.\n * 400 = any 4xx, 500 = any 5xx, specific number = exact match.\n */\n status?: number;\n /**\n * When true, catching a DenySignal sets _denyHandledByBoundary on the\n * nav context to prevent page-level deny promotion. Only slot catch-all\n * boundaries should set this — segment boundaries (403.tsx, 4xx.tsx,\n * error.tsx) must NOT, otherwise normal page denies get swallowed.\n */\n isSlotBoundary?: boolean;\n children: ReactNode;\n}\n\ninterface TimberErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport class TimberErrorBoundary extends Component<\n TimberErrorBoundaryProps,\n TimberErrorBoundaryState\n> {\n constructor(props: TimberErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): TimberErrorBoundaryState {\n // Suppress error boundaries during page unload (refresh/navigate away).\n // The aborted connection causes React's streaming hydration to error,\n // but the page is about to be replaced — showing an error boundary\n // would be a jarring flash for the user.\n if (_isUnloading) {\n return { hasError: false, error: null };\n }\n\n return { hasError: true, error };\n }\n\n componentDidUpdate(prevProps: TimberErrorBoundaryProps): void {\n // Reset error state when children change (e.g. client-side navigation).\n // Without this, navigating from one error page to another keeps the\n // stale error — getDerivedStateFromError doesn't re-fire for new children.\n if (this.state.hasError && prevProps.children !== this.props.children) {\n this.setState({ hasError: false, error: null });\n }\n }\n\n /** Reset the error state so children re-render. */\n private reset = () => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n if (!this.state.hasError || !this.state.error) {\n return this.props.children;\n }\n\n const error = this.state.error;\n const parsed = parseDigest(error);\n\n // RedirectSignal errors must propagate through all error boundaries\n // so the SSR shell fails and the pipeline catch block can produce a\n // proper HTTP redirect response. See design/04-authorization.md.\n if (parsed?.type === 'redirect') {\n throw error;\n }\n\n // If this boundary has a status filter, check whether the error matches.\n // Non-matching errors re-throw so an outer boundary can catch them.\n if (this.props.status != null) {\n const errorStatus = getErrorStatus(parsed, error);\n if (errorStatus == null || !statusMatches(this.props.status, errorStatus)) {\n // Re-throw: this boundary doesn't handle this error.\n throw error;\n }\n }\n\n // Report DenySignal handling to prevent page-level promotion — but only\n // for slot boundaries. Segment boundaries (403.tsx, 4xx.tsx, error.tsx)\n // must NOT set this flag, otherwise normal page/hold-window denies get\n // swallowed as 200 with boundary HTML instead of the intended 4xx.\n // Runs here in render() (not getDerivedStateFromError) so the status\n // filter has already been applied — non-matching boundaries re-threw above.\n // See LOCAL-298.\n if (parsed?.type === 'deny' && this.props.isSlotBoundary) {\n const ssrData = getSsrData();\n if (ssrData?._navContext) {\n ssrData._navContext._denyHandledByBoundary = true;\n }\n }\n\n // Pre-rendered fallback element (MDX status files) — render directly.\n // MDX components are server components that cannot be passed as function\n // props across the RSC→client boundary. Instead, they are pre-rendered\n // as elements in the RSC environment and passed here as fallbackElement.\n if (this.props.fallbackElement != null) {\n return this.props.fallbackElement;\n }\n\n // Render the fallback component with the right props shape.\n if (parsed?.type === 'deny') {\n return createElement(this.props.fallbackComponent as never, {\n status: parsed.status,\n dangerouslyPassData: parsed.data,\n });\n }\n\n // 5xx / RenderError / unhandled error\n const digest =\n parsed?.type === 'render-error' ? { code: parsed.code, data: parsed.data } : null;\n\n return createElement(this.props.fallbackComponent as never, {\n error,\n digest,\n reset: this.reset,\n });\n }\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Parse the structured digest from the error.\n * React sets `error.digest` from the string returned by RSC's onError.\n */\nfunction parseDigest(error: Error): ParsedDigest | null {\n const raw = (error as { digest?: string }).digest;\n if (typeof raw !== 'string') return null;\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && typeof parsed.type === 'string') {\n return parsed as ParsedDigest;\n }\n } catch {\n // Not JSON — legacy or unknown digest format\n }\n return null;\n}\n\n/**\n * Extract the HTTP status code from a parsed digest or error message.\n * Falls back to message pattern matching for errors without a digest.\n */\nfunction getErrorStatus(parsed: ParsedDigest | null, error: Error): number | null {\n if (parsed?.type === 'deny') return parsed.status;\n if (parsed?.type === 'render-error') return parsed.status;\n if (parsed?.type === 'redirect') return parsed.status;\n\n // Fallback: parse DenySignal message pattern for errors that lost their digest\n const match = error.message.match(/^Access denied with status (\\d+)$/);\n if (match) return parseInt(match[1], 10);\n\n // Unhandled errors are implicitly 500\n return 500;\n}\n\n/**\n * Check whether an error's status matches the boundary's status filter.\n * Category markers (400, 500) match any status in that range.\n */\nfunction statusMatches(boundaryStatus: number, errorStatus: number): boolean {\n // Category catch-all: 400 matches any 4xx, 500 matches any 5xx\n if (boundaryStatus === 400) return errorStatus >= 400 && errorStatus <= 499;\n if (boundaryStatus === 500) return errorStatus >= 500 && errorStatus <= 599;\n // Exact match\n return boundaryStatus === errorStatus;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"error-boundary-A_sgyyUP.js","names":[],"sources":["../../src/client/state.ts","../../src/client/ssr-data.ts","../../src/client/error-boundary.tsx"],"sourcesContent":["/**\n * Centralized client singleton state registry.\n *\n * ALL mutable module-level state that must have singleton semantics across\n * the client bundle lives here. Individual modules (router-ref.ts, ssr-data.ts,\n * use-params.ts, use-search-params.ts, unload-guard.ts) import from this file\n * and re-export thin wrapper functions.\n *\n * Why: In Vite dev, a module is instantiated separately if reached via different\n * import paths (e.g., relative `./foo.js` vs barrel `@timber-js/app/client`).\n * By centralizing all mutable state in a single module that is always reached\n * through the same dependency chain (barrel → wrapper → state.ts), we guarantee\n * a single instance of every piece of shared state.\n *\n * DO NOT import this file from outside client/. Server code must never depend\n * on client state. The barrel (client/index.ts) is the public entry point.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\".\n */\n\nimport type { RouterInstance } from './router.js';\nimport type { SsrData } from './ssr-data.js';\n\n// ─── Router (from router-ref.ts) ──────────────────────────────────────────\n\n/** The global router singleton — set once during bootstrap. */\nexport let globalRouter: RouterInstance | null = null;\n\nexport function _setGlobalRouter(router: RouterInstance | null): void {\n globalRouter = router;\n}\n\n// ─── SSR Data Provider (from ssr-data.ts) ──────────────────────────────────\n\n/**\n * ALS-backed SSR data provider. When registered, getSsrData() reads from\n * this function (ALS store) instead of module-level currentSsrData.\n */\nexport let ssrDataProvider: (() => SsrData | undefined) | undefined;\n\nexport function _setSsrDataProvider(provider: (() => SsrData | undefined) | undefined): void {\n ssrDataProvider = provider;\n}\n\n/** Fallback SSR data for tests and environments without ALS. */\nexport let currentSsrData: SsrData | undefined;\n\nexport function _setCurrentSsrData(data: SsrData | undefined): void {\n currentSsrData = data;\n}\n\n// ─── Route Params (from use-params.ts) ──────────────────────────────────────\n\n/** Current route params snapshot — replaced (not mutated) on each navigation. */\nexport let currentParams: Record<string, string | string[]> = {};\n\nexport function _setCurrentParams(params: Record<string, string | string[]>): void {\n currentParams = params;\n}\n\n/** Listeners notified when currentParams changes. */\nexport const paramsListeners = new Set<() => void>();\n\n// ─── Search Params Cache (from use-search-params.ts) ────────────────────────\n\n/** Cached search string — avoids reparsing when URL hasn't changed. */\nexport let cachedSearch = '';\nexport let cachedSearchParams = new URLSearchParams();\n\nexport function _setCachedSearch(search: string, params: URLSearchParams): void {\n cachedSearch = search;\n cachedSearchParams = params;\n}\n\n// ─── Unload Guard (from unload-guard.ts) ─────────────────────────────────────\n\n/** Whether the page is currently being unloaded. */\nexport let unloading = false;\n\nexport function _setUnloading(value: boolean): void {\n unloading = value;\n}\n","/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n */\n\nimport {\n ssrDataProvider,\n currentSsrData,\n _setSsrDataProvider,\n _setCurrentSsrData,\n} from './state.js';\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: Record<string, string | string[]>;\n /**\n * Mutable reference to NavContext for error boundary → RSC communication.\n * When TimberErrorBoundary catches a DenySignal, it sets\n * `_navContext._denyHandledByBoundary = true` to prevent the RSC entry\n * from promoting the denial to page-level. See LOCAL-298.\n */\n _navContext?: { _denyHandledByBoundary?: boolean };\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n//\n// Module singleton guarantee: In Vite's SSR environment, both\n// ssr-entry.ts (via #/client/ssr-data.js) and client component hooks\n// (via @timber-js/app/client) must resolve to the SAME module instance\n// of this file. The timber-shims plugin ensures this by remapping\n// @timber-js/app/client → src/client/index.ts in the SSR environment.\n// Without this remap, @timber-js/app/client resolves to dist/ (via\n// package.json exports), creating a split where registerSsrDataProvider\n// writes to one instance but getSsrData reads from another.\n// See timber-shims plugin resolveId for details.\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _setSsrDataProvider(provider);\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n _setCurrentSsrData(data);\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n _setCurrentSsrData(undefined);\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (ssrDataProvider) {\n return ssrDataProvider();\n }\n return currentSsrData;\n}\n","'use client';\n\n/**\n * Framework-injected React error boundary.\n *\n * Catches errors thrown by children and renders a fallback component\n * with the appropriate props based on error type:\n * - DenySignal (4xx) → { status, dangerouslyPassData }\n * - RenderError (5xx) → { error, digest, reset }\n * - Unhandled error → { error, digest: null, reset }\n *\n * The `status` prop controls which errors this boundary catches:\n * - Specific code (e.g. 403) → only that status\n * - Category (400) → any 4xx\n * - Category (500) → any 5xx\n * - Omitted → catches everything (error.tsx behavior)\n *\n * See design/10-error-handling.md §\"Status-Code Files\"\n */\n\nimport { Component, createElement, type ReactNode } from 'react';\nimport { getSsrData } from './ssr-data.js';\n\n// ─── Page Unload Detection ───────────────────────────────────────────────────\n// Track whether the page is being unloaded (user refreshed or navigated away).\n// When this is true, error boundaries suppress activation — the error is from\n// the aborted connection, not an application error.\nlet _isUnloading = false;\nif (typeof window !== 'undefined') {\n window.addEventListener('beforeunload', () => {\n _isUnloading = true;\n });\n window.addEventListener('pagehide', () => {\n _isUnloading = true;\n });\n}\n\n// ─── Digest Types ────────────────────────────────────────────────────────────\n\n/** Structured digest returned by RSC onError for DenySignal. */\ninterface DenyDigest {\n type: 'deny';\n status: number;\n data: unknown;\n}\n\n/** Structured digest returned by RSC onError for RenderError. */\ninterface RenderErrorDigest {\n type: 'render-error';\n code: string;\n data: unknown;\n status: number;\n}\n\n/** Structured digest returned by RSC onError for RedirectSignal. */\ninterface RedirectDigest {\n type: 'redirect';\n location: string;\n status: number;\n}\n\ntype ParsedDigest = DenyDigest | RenderErrorDigest | RedirectDigest;\n\n// ─── Props & State ───────────────────────────────────────────────────────────\n\nexport interface TimberErrorBoundaryProps {\n /** The component to render when an error is caught. */\n fallbackComponent?: (...args: unknown[]) => ReactNode;\n /**\n * Pre-rendered fallback element. Used for MDX status files which are server\n * components and cannot be passed as function props across the RSC→client\n * boundary. When set, rendered directly instead of calling fallbackComponent.\n *\n * See design/10-error-handling.md §\"Status-Code File Variants\" — MDX status\n * files are server components by default (zero client JS).\n */\n fallbackElement?: ReactNode;\n /**\n * Status code filter. If set, only catches errors matching this status.\n * 400 = any 4xx, 500 = any 5xx, specific number = exact match.\n */\n status?: number;\n /**\n * When true, catching a DenySignal sets _denyHandledByBoundary on the\n * nav context to prevent page-level deny promotion. Only slot catch-all\n * boundaries should set this — segment boundaries (403.tsx, 4xx.tsx,\n * error.tsx) must NOT, otherwise normal page denies get swallowed.\n */\n isSlotBoundary?: boolean;\n children: ReactNode;\n}\n\ninterface TimberErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport class TimberErrorBoundary extends Component<\n TimberErrorBoundaryProps,\n TimberErrorBoundaryState\n> {\n constructor(props: TimberErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): TimberErrorBoundaryState {\n // Suppress error boundaries during page unload (refresh/navigate away).\n // The aborted connection causes React's streaming hydration to error,\n // but the page is about to be replaced — showing an error boundary\n // would be a jarring flash for the user.\n if (_isUnloading) {\n return { hasError: false, error: null };\n }\n\n return { hasError: true, error };\n }\n\n componentDidUpdate(prevProps: TimberErrorBoundaryProps): void {\n // Reset error state when children change (e.g. client-side navigation).\n // Without this, navigating from one error page to another keeps the\n // stale error — getDerivedStateFromError doesn't re-fire for new children.\n if (this.state.hasError && prevProps.children !== this.props.children) {\n this.setState({ hasError: false, error: null });\n }\n }\n\n /** Reset the error state so children re-render. */\n private reset = () => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n if (!this.state.hasError || !this.state.error) {\n return this.props.children;\n }\n\n const error = this.state.error;\n const parsed = parseDigest(error);\n\n // RedirectSignal errors must propagate through all error boundaries\n // so the SSR shell fails and the pipeline catch block can produce a\n // proper HTTP redirect response. See design/04-authorization.md.\n if (parsed?.type === 'redirect') {\n throw error;\n }\n\n // If this boundary has a status filter, check whether the error matches.\n // Non-matching errors re-throw so an outer boundary can catch them.\n if (this.props.status != null) {\n const errorStatus = getErrorStatus(parsed, error);\n if (errorStatus == null || !statusMatches(this.props.status, errorStatus)) {\n // Re-throw: this boundary doesn't handle this error.\n throw error;\n }\n }\n\n // Report DenySignal handling to prevent page-level promotion — but only\n // for slot boundaries. Segment boundaries (403.tsx, 4xx.tsx, error.tsx)\n // must NOT set this flag, otherwise normal page/hold-window denies get\n // swallowed as 200 with boundary HTML instead of the intended 4xx.\n // Runs here in render() (not getDerivedStateFromError) so the status\n // filter has already been applied — non-matching boundaries re-threw above.\n // See LOCAL-298.\n if (parsed?.type === 'deny' && this.props.isSlotBoundary) {\n const ssrData = getSsrData();\n if (ssrData?._navContext) {\n ssrData._navContext._denyHandledByBoundary = true;\n }\n }\n\n // Pre-rendered fallback element (MDX status files) — render directly.\n // MDX components are server components that cannot be passed as function\n // props across the RSC→client boundary. Instead, they are pre-rendered\n // as elements in the RSC environment and passed here as fallbackElement.\n if (this.props.fallbackElement != null) {\n return this.props.fallbackElement;\n }\n\n // Render the fallback component with the right props shape.\n if (parsed?.type === 'deny') {\n return createElement(this.props.fallbackComponent as never, {\n status: parsed.status,\n dangerouslyPassData: parsed.data,\n });\n }\n\n // 5xx / RenderError / unhandled error\n const digest =\n parsed?.type === 'render-error' ? { code: parsed.code, data: parsed.data } : null;\n\n return createElement(this.props.fallbackComponent as never, {\n error,\n digest,\n reset: this.reset,\n });\n }\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Parse the structured digest from the error.\n * React sets `error.digest` from the string returned by RSC's onError.\n */\nfunction parseDigest(error: Error): ParsedDigest | null {\n const raw = (error as { digest?: string }).digest;\n if (typeof raw !== 'string') return null;\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && typeof parsed.type === 'string') {\n return parsed as ParsedDigest;\n }\n } catch {\n // Not JSON — legacy or unknown digest format\n }\n return null;\n}\n\n/**\n * Extract the HTTP status code from a parsed digest or error message.\n * Falls back to message pattern matching for errors without a digest.\n */\nfunction getErrorStatus(parsed: ParsedDigest | null, error: Error): number | null {\n if (parsed?.type === 'deny') return parsed.status;\n if (parsed?.type === 'render-error') return parsed.status;\n if (parsed?.type === 'redirect') return parsed.status;\n\n // Fallback: parse DenySignal message pattern for errors that lost their digest\n const match = error.message.match(/^Access denied with status (\\d+)$/);\n if (match) return parseInt(match[1], 10);\n\n // Unhandled errors are implicitly 500\n return 500;\n}\n\n/**\n * Check whether an error's status matches the boundary's status filter.\n * Category markers (400, 500) match any status in that range.\n */\nfunction statusMatches(boundaryStatus: number, errorStatus: number): boolean {\n // Category catch-all: 400 matches any 4xx, 500 matches any 5xx\n if (boundaryStatus === 400) return errorStatus >= 400 && errorStatus <= 499;\n if (boundaryStatus === 500) return errorStatus >= 500 && errorStatus <= 599;\n // Exact match\n return boundaryStatus === errorStatus;\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { o as traceAls } from "./als-registry-
|
|
1
|
+
import { o as traceAls } from "./als-registry-BJARkOcu.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
//#region src/server/tracing.ts
|
|
4
4
|
/**
|
|
@@ -182,6 +182,57 @@ async function getOtelTraceId() {
|
|
|
182
182
|
};
|
|
183
183
|
}
|
|
184
184
|
//#endregion
|
|
185
|
-
|
|
185
|
+
//#region src/cache/handler-store.ts
|
|
186
|
+
var handler = null;
|
|
187
|
+
/** Replace the active cache handler. Called by the framework at boot. */
|
|
188
|
+
function setCacheHandler(h) {
|
|
189
|
+
handler = h;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Get the active cache handler. Creates a default MemoryCacheHandler on
|
|
193
|
+
* first access if none has been set via setCacheHandler().
|
|
194
|
+
*/
|
|
195
|
+
function getCacheHandler() {
|
|
196
|
+
if (!handler) handler = createDefaultHandler();
|
|
197
|
+
return handler;
|
|
198
|
+
}
|
|
199
|
+
function createDefaultHandler() {
|
|
200
|
+
const store = /* @__PURE__ */ new Map();
|
|
201
|
+
const maxSize = 1e3;
|
|
202
|
+
return {
|
|
203
|
+
async get(key) {
|
|
204
|
+
const entry = store.get(key);
|
|
205
|
+
if (!entry) return null;
|
|
206
|
+
store.delete(key);
|
|
207
|
+
store.set(key, entry);
|
|
208
|
+
const stale = Date.now() > entry.expiresAt;
|
|
209
|
+
return {
|
|
210
|
+
value: entry.value,
|
|
211
|
+
stale
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
async set(key, value, opts) {
|
|
215
|
+
if (store.has(key)) store.delete(key);
|
|
216
|
+
while (store.size >= maxSize) {
|
|
217
|
+
const oldest = store.keys().next().value;
|
|
218
|
+
if (oldest !== void 0) store.delete(oldest);
|
|
219
|
+
else break;
|
|
220
|
+
}
|
|
221
|
+
store.set(key, {
|
|
222
|
+
value,
|
|
223
|
+
expiresAt: Date.now() + opts.ttl * 1e3,
|
|
224
|
+
tags: opts.tags
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
async invalidate(opts) {
|
|
228
|
+
if (opts.key) store.delete(opts.key);
|
|
229
|
+
if (opts.tag) {
|
|
230
|
+
for (const [key, entry] of store) if (entry.tags.includes(opts.tag)) store.delete(key);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
//#endregion
|
|
236
|
+
export { generateTraceId as a, replaceTraceId as c, spanId as d, traceId as f, addSpanEventSync as i, runWithTraceId as l, setCacheHandler as n, getOtelTraceId as o, withSpan as p, addSpanEvent as r, getTraceStore as s, getCacheHandler as t, setSpanAttribute as u };
|
|
186
237
|
|
|
187
|
-
//# sourceMappingURL=
|
|
238
|
+
//# sourceMappingURL=handler-store-CaE0ZgVG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler-store-CaE0ZgVG.js","names":[],"sources":["../../src/server/tracing.ts","../../src/cache/handler-store.ts"],"sourcesContent":["/**\n * Tracing — per-request trace ID via AsyncLocalStorage, OTEL span helpers.\n *\n * traceId() is always available in server code (middleware, access, components, actions).\n * Returns a 32-char lowercase hex string — the OTEL trace ID when an SDK is active,\n * or a crypto.randomUUID()-derived fallback otherwise.\n *\n * See design/17-logging.md §\"trace_id is Always Set\"\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { traceAls, type TraceStore } from './als-registry.js';\n\n// Re-export the TraceStore type for public API consumers.\nexport type { TraceStore } from './als-registry.js';\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/**\n * Returns the current request's trace ID — always a 32-char lowercase hex string.\n *\n * With OTEL: the real OTEL trace ID (matches Jaeger/Honeycomb/Datadog).\n * Without OTEL: crypto.randomUUID() with hyphens stripped.\n *\n * Throws if called outside a request context (no ALS store).\n */\nexport function traceId(): string {\n const store = traceAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] traceId() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n return store.traceId;\n}\n\n/**\n * Returns the current OTEL span ID if available, undefined otherwise.\n */\nexport function spanId(): string | undefined {\n return traceAls.getStore()?.spanId;\n}\n\n// ─── Framework-Internal Helpers ───────────────────────────────────────────\n\n/**\n * Generate a 32-char lowercase hex ID from crypto.randomUUID().\n * Same format as OTEL trace IDs — zero-friction upgrade path.\n */\nexport function generateTraceId(): string {\n return randomUUID().replace(/-/g, '');\n}\n\n/**\n * Run a callback within a trace context. Used by the pipeline to establish\n * per-request ALS scope.\n */\nexport function runWithTraceId<T>(id: string, fn: () => T): T {\n return traceAls.run({ traceId: id }, fn);\n}\n\n/**\n * Replace the trace ID in the current ALS store. Used when OTEL creates\n * a root span and we want to switch from the UUID fallback to the real\n * OTEL trace ID.\n */\nexport function replaceTraceId(newTraceId: string, newSpanId?: string): void {\n const store = traceAls.getStore();\n if (store) {\n store.traceId = newTraceId;\n store.spanId = newSpanId;\n }\n}\n\n/**\n * Update the span ID in the current ALS store. Used when entering a new\n * OTEL span to keep log–trace correlation accurate.\n */\nexport function updateSpanId(newSpanId: string | undefined): void {\n const store = traceAls.getStore();\n if (store) {\n store.spanId = newSpanId;\n }\n}\n\n/**\n * Get the current trace store, or undefined if outside a request context.\n * Framework-internal — use traceId()/spanId() in user code.\n */\nexport function getTraceStore(): TraceStore | undefined {\n return traceAls.getStore();\n}\n\n// ─── Dev-Mode OTEL Auto-Init ─────────────────────────────────────────────\n\n/**\n * Initialize a minimal OTEL SDK in dev mode so spans are recorded and\n * fed to the DevSpanProcessor for dev log output.\n *\n * If the user already configured an OTEL SDK in register(), we add\n * our DevSpanProcessor alongside theirs. If no SDK is configured,\n * we create a BasicTracerProvider with our processor.\n *\n * Only called in dev mode — zero overhead in production.\n */\nexport async function initDevTracing(\n config: import('./dev-logger.js').DevLoggerConfig\n): Promise<void> {\n const api = await getOtelApi();\n if (!api) return;\n\n let DevSpanProcessor: typeof import('./dev-span-processor.js').DevSpanProcessor;\n let BasicTracerProvider: typeof import('@opentelemetry/sdk-trace-base').BasicTracerProvider;\n let AsyncLocalStorageContextManager: typeof import('@opentelemetry/context-async-hooks').AsyncLocalStorageContextManager;\n\n try {\n ({ DevSpanProcessor } = await import('./dev-span-processor.js'));\n ({ BasicTracerProvider } = await import('@opentelemetry/sdk-trace-base'));\n ({ AsyncLocalStorageContextManager } = await import('@opentelemetry/context-async-hooks'));\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[timber] Dev tracing disabled — failed to load OTEL packages:\\n ${msg}`);\n return;\n }\n\n const processor = new DevSpanProcessor(config);\n\n // Register a context manager so OTEL can propagate the active span\n // across async boundaries. Without this, startActiveSpan can't make\n // spans \"active\" — child spans get random trace IDs and getActiveSpan()\n // returns undefined.\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n api.context.setGlobalContextManager(contextManager);\n\n // Create a minimal TracerProvider with our DevSpanProcessor.\n // If the user also configures an SDK in register(), their provider\n // will coexist — the global provider set last wins for new tracers,\n // but our processor captures all spans from the timber.js tracer.\n const provider = new BasicTracerProvider({\n spanProcessors: [processor],\n });\n api.trace.setGlobalTracerProvider(provider);\n\n // Reset cached tracer so next getTracer() picks up the new provider\n _tracer = undefined;\n}\n\n// ─── OTEL Span Helpers ───────────────────────────────────────────────────\n\n/**\n * Attempt to get the @opentelemetry/api tracer. Returns undefined if the\n * package is not installed or no SDK is registered.\n *\n * timber.js depends on @opentelemetry/api as the vendor-neutral interface.\n * The API is a no-op by default — spans are only emitted when the developer\n * initializes an SDK in register().\n */\nlet _otelApi: typeof import('@opentelemetry/api') | null | undefined;\n\nasync function getOtelApi(): Promise<typeof import('@opentelemetry/api') | null> {\n if (_otelApi === undefined) {\n try {\n _otelApi = await import('@opentelemetry/api');\n } catch {\n _otelApi = null;\n }\n }\n return _otelApi;\n}\n\n/** OTEL tracer instance, lazily created. */\nlet _tracer: import('@opentelemetry/api').Tracer | null | undefined;\n\n/**\n * Get the timber.js OTEL tracer. Returns null if @opentelemetry/api is not available.\n */\nexport async function getTracer(): Promise<import('@opentelemetry/api').Tracer | null> {\n if (_tracer === undefined) {\n const api = await getOtelApi();\n if (api) {\n _tracer = api.trace.getTracer('timber.js');\n } else {\n _tracer = null;\n }\n }\n return _tracer;\n}\n\n/**\n * Run a function within an OTEL span. If OTEL is not available, runs the function\n * directly without any span overhead.\n *\n * Automatically:\n * - Creates the span as a child of the current context\n * - Updates the ALS span ID for log–trace correlation\n * - Ends the span when the function completes\n * - Records exceptions on error\n */\nexport async function withSpan<T>(\n name: string,\n attributes: Record<string, string | number | boolean>,\n fn: () => T | Promise<T>\n): Promise<T> {\n const tracer = await getTracer();\n if (!tracer) {\n return fn();\n }\n\n const api = (await getOtelApi())!;\n return tracer.startActiveSpan(name, { attributes }, async (span) => {\n const prevSpanId = spanId();\n updateSpanId(span.spanContext().spanId);\n try {\n const result = await fn();\n span.setStatus({ code: api.SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.setStatus({ code: api.SpanStatusCode.ERROR });\n if (error instanceof Error) {\n span.recordException(error);\n }\n throw error;\n } finally {\n span.end();\n updateSpanId(prevSpanId);\n }\n });\n}\n\n/**\n * Set an attribute on the current active span (if any).\n * Used for setting span attributes after span creation (e.g. timber.result on access spans).\n */\nexport async function setSpanAttribute(\n key: string,\n value: string | number | boolean\n): Promise<void> {\n const api = await getOtelApi();\n if (!api) return;\n\n const activeSpan = api.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.setAttribute(key, value);\n }\n}\n\n/**\n * Add a span event to the current active span (if any).\n * Used for timber.cache HIT/MISS events — recorded as span events, not child spans.\n */\nexport async function addSpanEvent(\n name: string,\n attributes?: Record<string, string | number | boolean>\n): Promise<void> {\n const api = await getOtelApi();\n if (!api) return;\n\n const activeSpan = api.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.addEvent(name, attributes);\n }\n}\n\n/**\n * Fire-and-forget span event — no await, no microtask overhead.\n *\n * Used on the cache hot path where awaiting addSpanEvent creates an\n * unnecessary microtask per cache operation. If OTEL is not loaded yet,\n * the event is silently dropped (acceptable for diagnostics).\n *\n * See TIM-370 for perf motivation.\n */\nexport function addSpanEventSync(\n name: string,\n attributes?: Record<string, string | number | boolean>\n): void {\n // Fast path: if OTEL API hasn't been loaded yet, skip entirely.\n // _otelApi is undefined (not yet loaded), null (failed to load), or the module.\n if (!_otelApi) return;\n\n const activeSpan = _otelApi.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.addEvent(name, attributes);\n }\n}\n\n/**\n * Try to extract the OTEL trace ID from the current active span context.\n * Returns undefined if OTEL is not active or no span exists.\n */\nexport async function getOtelTraceId(): Promise<{ traceId: string; spanId: string } | undefined> {\n const api = await getOtelApi();\n if (!api) return undefined;\n\n const activeSpan = api.trace.getActiveSpan();\n if (!activeSpan) return undefined;\n\n const ctx = activeSpan.spanContext();\n // OTEL uses \"0000000000000000\" as invalid trace IDs\n if (!ctx.traceId || ctx.traceId === '00000000000000000000000000000000') {\n return undefined;\n }\n\n return { traceId: ctx.traceId, spanId: ctx.spanId };\n}\n","/**\n * Module-level cache handler singleton.\n *\n * Lazily initialized to MemoryCacheHandler on first access. The framework\n * replaces this at boot from timber.config.ts via setCacheHandler().\n *\n * This module avoids importing from ./index to prevent circular dependencies.\n */\n\n// Inline the interface to avoid circular import with index.ts\ninterface CacheHandlerLike {\n get(key: string): Promise<{ value: unknown; stale: boolean } | null>;\n set(key: string, value: unknown, opts: { ttl: number; tags: string[] }): Promise<void>;\n invalidate(opts: { key?: string; tag?: string }): Promise<void>;\n}\n\nlet handler: CacheHandlerLike | null = null;\n\n/** Replace the active cache handler. Called by the framework at boot. */\nexport function setCacheHandler(h: CacheHandlerLike): void {\n handler = h;\n}\n\n/**\n * Get the active cache handler. Creates a default MemoryCacheHandler on\n * first access if none has been set via setCacheHandler().\n */\nexport function getCacheHandler(): CacheHandlerLike {\n if (!handler) {\n // Inline a minimal LRU cache to avoid circular dep with index.ts.\n // In production, the framework always calls setCacheHandler() at boot.\n handler = createDefaultHandler();\n }\n return handler;\n}\n\nfunction createDefaultHandler(): CacheHandlerLike {\n const store = new Map<string, { value: unknown; expiresAt: number; tags: string[] }>();\n const maxSize = 1000;\n\n return {\n async get(key) {\n const entry = store.get(key);\n if (!entry) return null;\n store.delete(key);\n store.set(key, entry);\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n },\n async set(key, value, opts) {\n if (store.has(key)) store.delete(key);\n while (store.size >= maxSize) {\n const oldest = store.keys().next().value;\n if (oldest !== undefined) store.delete(oldest);\n else break;\n }\n store.set(key, { value, expiresAt: Date.now() + opts.ttl * 1000, tags: opts.tags });\n },\n async invalidate(opts) {\n if (opts.key) store.delete(opts.key);\n if (opts.tag) {\n for (const [key, entry] of store) {\n if (entry.tags.includes(opts.tag)) store.delete(key);\n }\n }\n },\n };\n}\n"],"mappings":"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"}
|