@timber-js/app 0.2.0-alpha.71 → 0.2.0-alpha.72
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/actions-Dg-ANYHb.js +421 -0
- package/dist/_chunks/actions-Dg-ANYHb.js.map +1 -0
- package/dist/_chunks/{als-registry-BJARkOcu.js → als-registry-HS0LGUl2.js} +1 -1
- package/dist/_chunks/als-registry-HS0LGUl2.js.map +1 -0
- package/dist/_chunks/{define-Dz1bqwaS.js → define-C77ScO0m.js} +14 -14
- package/dist/_chunks/define-C77ScO0m.js.map +1 -0
- package/dist/_chunks/{define-CGuYoRHU.js → define-CZqDwhSu.js} +15 -15
- package/dist/_chunks/define-CZqDwhSu.js.map +1 -0
- package/dist/_chunks/{define-cookie-B5mewxwM.js → define-cookie-C2IkoFGN.js} +9 -8
- package/dist/_chunks/{define-cookie-B5mewxwM.js.map → define-cookie-C2IkoFGN.js.map} +1 -1
- package/dist/_chunks/{format-Rn922VH2.js → dev-warnings-DpGRGoDi.js} +4 -26
- package/dist/_chunks/dev-warnings-DpGRGoDi.js.map +1 -0
- package/dist/_chunks/format-CYBGxKtc.js +14 -0
- package/dist/_chunks/format-CYBGxKtc.js.map +1 -0
- package/dist/_chunks/{interception-CEdHHviP.js → interception-Dpn_UfAD.js} +2 -2
- package/dist/_chunks/{interception-CEdHHviP.js.map → interception-Dpn_UfAD.js.map} +1 -1
- package/dist/_chunks/{segment-context-hzuJ048X.js → merge-search-params-Cm_KIWDX.js} +2 -33
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js.map +1 -0
- package/dist/_chunks/{request-context-CywiO4jV.js → request-context-qMsWgy9C.js} +72 -36
- package/dist/_chunks/request-context-qMsWgy9C.js.map +1 -0
- package/dist/_chunks/{schema-bridge-C4SwjCQD.js → schema-bridge-C3xl_vfb.js} +1 -1
- package/dist/_chunks/{schema-bridge-C4SwjCQD.js.map → schema-bridge-C3xl_vfb.js.map} +1 -1
- package/dist/_chunks/segment-context-fHFLF1PE.js +34 -0
- package/dist/_chunks/segment-context-fHFLF1PE.js.map +1 -0
- package/dist/_chunks/ssr-data-DzuI0bIV.js +88 -0
- package/dist/_chunks/ssr-data-DzuI0bIV.js.map +1 -0
- package/dist/_chunks/{stale-reload-BLUC_Pl_.js → stale-reload-C2plcNtG.js} +1 -1
- package/dist/_chunks/{stale-reload-BLUC_Pl_.js.map → stale-reload-C2plcNtG.js.map} +1 -1
- package/dist/_chunks/{handler-store-BVePM7hp.js → tracing-CCYbKn5n.js} +60 -60
- package/dist/_chunks/tracing-CCYbKn5n.js.map +1 -0
- package/dist/_chunks/use-params-B1AuhI1p.js +307 -0
- package/dist/_chunks/use-params-B1AuhI1p.js.map +1 -0
- package/dist/_chunks/{use-query-states-DAhgj8Gx.js → use-query-states-Lo_s_pw2.js} +4 -4
- package/dist/_chunks/use-query-states-Lo_s_pw2.js.map +1 -0
- package/dist/_chunks/{wrappers-LZbghvn0.js → wrappers-_DTmImGt.js} +1 -1
- package/dist/_chunks/{wrappers-LZbghvn0.js.map → wrappers-_DTmImGt.js.map} +1 -1
- package/dist/adapters/cloudflare-kv-cache.d.ts +64 -0
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -0
- package/dist/adapters/cloudflare-kv-cache.js +95 -0
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -0
- package/dist/cache/index.d.ts +18 -4
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +78 -12
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/sizeof.d.ts +22 -0
- package/dist/cache/sizeof.d.ts.map +1 -0
- package/dist/cli.d.ts +6 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-dev.d.ts +27 -1
- package/dist/client/browser-dev.d.ts.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts +17 -0
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -0
- package/dist/client/browser-entry/hmr.d.ts +21 -0
- package/dist/client/browser-entry/hmr.d.ts.map +1 -0
- package/dist/client/browser-entry/hydrate.d.ts +46 -0
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -0
- package/dist/client/browser-entry/index.d.ts +30 -0
- package/dist/client/browser-entry/index.d.ts.map +1 -0
- package/dist/client/browser-entry/post-hydration.d.ts +26 -0
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -0
- package/dist/client/browser-entry/router-init.d.ts +23 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -0
- package/dist/client/browser-entry/rsc-stream.d.ts +24 -0
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -0
- package/dist/client/browser-entry/scroll.d.ts +19 -0
- package/dist/client/browser-entry/scroll.d.ts.map +1 -0
- package/dist/client/error-boundary.js +131 -1
- package/dist/client/error-boundary.js.map +1 -0
- package/dist/client/index.d.ts +4 -19
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +14 -1191
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +18 -0
- package/dist/client/internal.d.ts.map +1 -0
- package/dist/client/internal.js +890 -0
- package/dist/client/internal.js.map +1 -0
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/router-ref.d.ts +1 -1
- package/dist/client/top-loader.d.ts +2 -2
- package/dist/client/use-link-status.d.ts +1 -1
- package/dist/client/{use-navigation-pending.d.ts → use-pending-navigation.d.ts} +4 -4
- package/dist/client/use-pending-navigation.d.ts.map +1 -0
- package/dist/client/use-router.d.ts +1 -1
- package/dist/codec.d.ts +10 -0
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/config-types.d.ts +210 -0
- package/dist/config-types.d.ts.map +1 -0
- package/dist/content/index.d.ts +1 -10
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +0 -2
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.d.ts +0 -2
- package/dist/cookies/index.d.ts.map +1 -1
- package/dist/cookies/index.js +2 -3
- package/dist/index.d.ts +25 -288
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +261 -43
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +84 -0
- package/dist/plugin-context.d.ts.map +1 -0
- package/dist/plugins/adapter-build.d.ts +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +1 -1
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/content.d.ts +1 -1
- package/dist/plugins/content.d.ts.map +1 -1
- package/dist/plugins/dev-browser-logs.d.ts +1 -1
- package/dist/plugins/dev-browser-logs.d.ts.map +1 -1
- package/dist/plugins/dev-logs.d.ts +1 -1
- package/dist/plugins/dev-logs.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/mdx.d.ts +1 -1
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +4 -4
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/search-params/define.d.ts +6 -6
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.d.ts +1 -2
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +4 -4
- package/dist/search-params/registry.d.ts +1 -1
- package/dist/search-params/registry.d.ts.map +1 -1
- package/dist/segment-params/define.d.ts +6 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.d.ts +0 -1
- package/dist/segment-params/index.d.ts.map +1 -1
- package/dist/segment-params/index.js +3 -3
- package/dist/server/als-registry.d.ts +1 -1
- package/dist/server/dev-holding-server.d.ts +52 -0
- package/dist/server/dev-holding-server.d.ts.map +1 -0
- package/dist/server/dev-warnings.d.ts +1 -7
- package/dist/server/dev-warnings.d.ts.map +1 -1
- package/dist/server/index.d.ts +6 -45
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +7 -3272
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +46 -0
- package/dist/server/internal.d.ts.map +1 -0
- package/dist/server/internal.js +2865 -0
- package/dist/server/internal.js.map +1 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +41 -17
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +45 -15
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +4 -4
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/shims/headers.d.ts +2 -1
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +2 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/package.json +19 -13
- package/src/adapters/cloudflare-kv-cache.ts +142 -0
- package/src/cache/handler-store.ts +2 -2
- package/src/cache/index.ts +74 -15
- package/src/cache/sizeof.ts +31 -0
- package/src/cli.ts +6 -1
- package/src/client/browser-dev.ts +128 -1
- package/src/client/browser-entry/action-dispatch.ts +116 -0
- package/src/client/browser-entry/hmr.ts +81 -0
- package/src/client/browser-entry/hydrate.ts +145 -0
- package/src/client/browser-entry/index.ts +138 -0
- package/src/client/browser-entry/post-hydration.ts +119 -0
- package/src/client/browser-entry/router-init.ts +184 -0
- package/src/client/browser-entry/rsc-stream.ts +157 -0
- package/src/client/browser-entry/scroll.ts +27 -0
- package/src/client/index.ts +10 -38
- package/src/client/internal.ts +57 -0
- package/src/client/navigation-context.ts +6 -2
- package/src/client/navigation-root.tsx +1 -1
- package/src/client/router-ref.ts +1 -1
- package/src/client/top-loader.tsx +2 -2
- package/src/client/use-link-status.ts +1 -1
- package/src/client/{use-navigation-pending.ts → use-pending-navigation.ts} +5 -5
- package/src/client/use-query-states.ts +2 -2
- package/src/client/use-router.ts +1 -1
- package/src/codec.ts +15 -0
- package/src/config-types.ts +208 -0
- package/src/content/index.ts +5 -13
- package/src/cookies/define-cookie.ts +9 -7
- package/src/cookies/index.ts +6 -5
- package/src/index.ts +84 -416
- package/src/plugin-context.ts +200 -0
- package/src/plugins/adapter-build.ts +1 -1
- package/src/plugins/build-manifest.ts +1 -1
- package/src/plugins/build-report.ts +1 -1
- package/src/plugins/content.ts +1 -1
- package/src/plugins/dev-browser-logs.ts +1 -1
- package/src/plugins/dev-logs.ts +1 -1
- package/src/plugins/dev-server.ts +16 -1
- package/src/plugins/entries.ts +2 -2
- package/src/plugins/fonts.ts +4 -3
- package/src/plugins/mdx.ts +1 -1
- package/src/plugins/routing.ts +1 -1
- package/src/plugins/shims.ts +53 -5
- package/src/plugins/static-build.ts +8 -6
- package/src/search-params/define.ts +22 -22
- package/src/search-params/index.ts +3 -3
- package/src/search-params/registry.ts +1 -1
- package/src/segment-params/define.ts +18 -18
- package/src/segment-params/index.ts +2 -1
- package/src/server/action-handler.ts +1 -1
- package/src/server/als-registry.ts +3 -3
- package/src/server/dev-holding-server.ts +185 -0
- package/src/server/dev-warnings.ts +2 -21
- package/src/server/html-injectors.ts +3 -3
- package/src/server/index.ts +25 -180
- package/src/server/internal.ts +169 -0
- package/src/server/pipeline.ts +12 -7
- package/src/server/primitives.ts +71 -30
- package/src/server/request-context.ts +77 -39
- package/src/server/route-element-builder.ts +1 -1
- package/src/server/rsc-entry/index.ts +2 -2
- package/src/server/rsc-entry/ssr-renderer.ts +1 -1
- package/src/server/slot-resolver.ts +1 -1
- package/src/server/tracing.ts +6 -6
- package/src/server/tree-builder.ts +1 -1
- package/src/shims/headers.ts +5 -1
- package/src/shims/navigation.ts +5 -1
- package/dist/_chunks/als-registry-BJARkOcu.js.map +0 -1
- package/dist/_chunks/define-CGuYoRHU.js.map +0 -1
- package/dist/_chunks/define-Dz1bqwaS.js.map +0 -1
- package/dist/_chunks/error-boundary-D9hzsveV.js +0 -216
- package/dist/_chunks/error-boundary-D9hzsveV.js.map +0 -1
- package/dist/_chunks/format-Rn922VH2.js.map +0 -1
- package/dist/_chunks/handler-store-BVePM7hp.js.map +0 -1
- package/dist/_chunks/request-context-CywiO4jV.js.map +0 -1
- package/dist/_chunks/segment-context-hzuJ048X.js.map +0 -1
- package/dist/_chunks/use-query-states-DAhgj8Gx.js.map +0 -1
- package/dist/client/browser-entry.d.ts +0 -21
- package/dist/client/browser-entry.d.ts.map +0 -1
- package/dist/client/use-navigation-pending.d.ts.map +0 -1
- package/src/client/browser-entry.ts +0 -846
|
@@ -158,7 +158,7 @@ function rejectLegacyGenerateMetadata(mod: Record<string, unknown>, filePath: st
|
|
|
158
158
|
* Returns the resolved Metadata, or null if none exported.
|
|
159
159
|
*
|
|
160
160
|
* Metadata functions no longer receive { params } — they access params
|
|
161
|
-
* via
|
|
161
|
+
* via getSegmentParams() from ALS, same as page/layout components.
|
|
162
162
|
*/
|
|
163
163
|
async function extractMetadata(
|
|
164
164
|
mod: Record<string, unknown>,
|
|
@@ -320,9 +320,9 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
320
320
|
// Coerce segment params (params.ts) before building the element tree.
|
|
321
321
|
// Without this, components receive raw strings instead of typed values.
|
|
322
322
|
await coerceSegmentParams(revalidateMatch);
|
|
323
|
-
// Set coerced params in ALS so
|
|
323
|
+
// Set coerced params in ALS so getSegmentParams() works during
|
|
324
324
|
// the revalidation render (AccessGate reads params via ALS).
|
|
325
|
-
// Without this, AccessGate →
|
|
325
|
+
// Without this, AccessGate → getSegmentParams() throws because
|
|
326
326
|
// segmentParamsPromise is never set. See TIM-667.
|
|
327
327
|
setSegmentParams(revalidateMatch.segmentParams);
|
|
328
328
|
const routeResult = await buildRouteElement(revalidateReq, revalidateMatch);
|
|
@@ -288,7 +288,7 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
288
288
|
//
|
|
289
289
|
// Error pages now use SSR-only rendering (Fizz directly, no RSC Flight),
|
|
290
290
|
// which imports layouts in the SSR environment where ALS is active. This
|
|
291
|
-
// means we CAN include layouts — they'll call
|
|
291
|
+
// means we CAN include layouts — they'll call getHeaders()/getCookies() correctly.
|
|
292
292
|
//
|
|
293
293
|
// Deny pages still go through RSC → SSR (plain props), so they pass empty
|
|
294
294
|
// layouts to avoid re-executing server components in the RSC environment's
|
|
@@ -238,7 +238,7 @@ export async function resolveSlotElement(
|
|
|
238
238
|
// Catch-all error boundary: ensures slot errors NEVER propagate to the
|
|
239
239
|
// parent layout. Without this, a slot without error.tsx that throws
|
|
240
240
|
// causes SSR's renderToReadableStream to reject, triggering renderDenyPage
|
|
241
|
-
// which re-executes all layout server components (including
|
|
241
|
+
// which re-executes all layout server components (including getHeaders() calls
|
|
242
242
|
// that fail in the SSR environment). The null fallback means the slot
|
|
243
243
|
// degrades to nothing — consistent with the slot access denial behavior.
|
|
244
244
|
// See design/02-rendering-pipeline.md §"Slot Access Failure = Graceful Degradation"
|
package/src/server/tracing.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tracing — per-request trace ID via AsyncLocalStorage, OTEL span helpers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* getTraceId() is always available in server code (middleware, access, components, actions).
|
|
5
5
|
* Returns a 32-char lowercase hex string — the OTEL trace ID when an SDK is active,
|
|
6
6
|
* or a crypto.randomUUID()-derived fallback otherwise.
|
|
7
7
|
*
|
|
@@ -24,11 +24,11 @@ export type { TraceStore } from './als-registry.js';
|
|
|
24
24
|
*
|
|
25
25
|
* Throws if called outside a request context (no ALS store).
|
|
26
26
|
*/
|
|
27
|
-
export function
|
|
27
|
+
export function getTraceId(): string {
|
|
28
28
|
const store = traceAls.getStore();
|
|
29
29
|
if (!store) {
|
|
30
30
|
throw new Error(
|
|
31
|
-
'[timber]
|
|
31
|
+
'[timber] getTraceId() called outside of a request context. ' +
|
|
32
32
|
'It can only be used in middleware, access checks, server components, and server actions.'
|
|
33
33
|
);
|
|
34
34
|
}
|
|
@@ -38,7 +38,7 @@ export function traceId(): string {
|
|
|
38
38
|
/**
|
|
39
39
|
* Returns the current OTEL span ID if available, undefined otherwise.
|
|
40
40
|
*/
|
|
41
|
-
export function
|
|
41
|
+
export function getSpanId(): string | undefined {
|
|
42
42
|
return traceAls.getStore()?.spanId;
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -86,7 +86,7 @@ export function updateSpanId(newSpanId: string | undefined): void {
|
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* Get the current trace store, or undefined if outside a request context.
|
|
89
|
-
* Framework-internal — use
|
|
89
|
+
* Framework-internal — use getTraceId()/getSpanId() in user code.
|
|
90
90
|
*/
|
|
91
91
|
export function getTraceStore(): TraceStore | undefined {
|
|
92
92
|
return traceAls.getStore();
|
|
@@ -210,7 +210,7 @@ export async function withSpan<T>(
|
|
|
210
210
|
|
|
211
211
|
const api = (await getOtelApi())!;
|
|
212
212
|
return tracer.startActiveSpan(name, { attributes }, async (span) => {
|
|
213
|
-
const prevSpanId =
|
|
213
|
+
const prevSpanId = getSpanId();
|
|
214
214
|
updateSpanId(span.spanContext().spanId);
|
|
215
215
|
try {
|
|
216
216
|
const result = await fn();
|
|
@@ -176,7 +176,7 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
|
|
|
176
176
|
);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
// Build the page element — params are accessed via
|
|
179
|
+
// Build the page element — params are accessed via getSegmentParams() from ALS
|
|
180
180
|
let element: ReactElement = createElement(PageComponent, {});
|
|
181
181
|
|
|
182
182
|
// Build tree bottom-up: wrap page, then walk segments from leaf to root
|
package/src/shims/headers.ts
CHANGED
|
@@ -6,4 +6,8 @@
|
|
|
6
6
|
* pipeline (both import from the same shared request-context chunk in dist/).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
// Re-export timber's new names for the primary API
|
|
10
|
+
export { getHeaders, getCookies } from '@timber-js/app/server';
|
|
11
|
+
|
|
12
|
+
// Next.js compat aliases — libraries importing from 'next/headers' expect these names.
|
|
13
|
+
export { getHeaders as headers, getCookies as cookies } from '@timber-js/app/server';
|
package/src/shims/navigation.ts
CHANGED
|
@@ -18,4 +18,8 @@ export {
|
|
|
18
18
|
} from '@timber-js/app/client';
|
|
19
19
|
|
|
20
20
|
// Functions (server-side)
|
|
21
|
-
export { redirect,
|
|
21
|
+
export { redirect, redirectExternal, RedirectType } from '@timber-js/app/server';
|
|
22
|
+
|
|
23
|
+
// Next.js compat aliases — these are not part of timber's primary API.
|
|
24
|
+
// They exist so that libraries importing from 'next/navigation' still work.
|
|
25
|
+
export { permanentRedirect, notFound } from '../server/primitives.js';
|
|
@@ -1 +0,0 @@
|
|
|
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 * Set by AccessGate or PageDenyBoundary when a DenySignal is caught\n * server-side (inside the React tree, before React Flight sees it).\n * The pipeline reads this after render to set the HTTP status code.\n * See TIM-666.\n */\n denyStatus?: number;\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;;AAuE7E,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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"define-CGuYoRHU.js","names":[],"sources":["../../src/search-params/define.ts"],"sourcesContent":["/**\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, isStandardSchema, isCodec } from '../schema-bridge.js';\nimport type { StandardSchemaV1 } from '../schema-bridge.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// StandardSchemaV1 is imported from schema-bridge.ts — single source of truth.\n// Re-export for consumers that import it from this module.\nexport type { StandardSchemaV1 } from '../schema-bridge.js';\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// isStandardSchema and isCodec are imported from schema-bridge.ts.\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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"define-Dz1bqwaS.js","names":[],"sources":["../../src/segment-params/define.ts"],"sourcesContent":["/**\n * defineSegmentParams — 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 params.ts (segment-level)\n * convention file.\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';\nimport {\n type StandardSchemaV1,\n validateSync,\n isStandardSchema,\n isCodec,\n} from '../schema-bridge.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\nexport type { StandardSchemaV1 };\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// Internal helpers\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] defineSegmentParams: 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] defineSegmentParams: 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 { defineSegmentParams } from '@timber-js/app/segment-params'\n * import { z } from 'zod/v4'\n *\n * export const segmentParams = defineSegmentParams({\n * id: z.coerce.number().int().positive(),\n * })\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":";;AA6BA,IAAI;;;;;;AAOJ,SAAgB,uBAAuB,IAA4D;AACjG,qBAAoB;;AAGtB,SAAS,sBAAkE;AACzE,KAAI,CAAC,kBACH,OAAM,IAAI,MACR,uGAED;AAEH,QAAO,mBAAmB;;;;;;;;;AA+D5B,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,wCAAwC,UAAU,uJAGnD;;;;;;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,wCAAwC,IAAI,+IAG7C;UAEI,GAAG;AAIV,MAAI,aAAa,SAAS,EAAE,QAAQ,SAAS,gBAAgB,CAC3D,OAAM;;;;;;;;;;;;;;;;AAuBd,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"}
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import { Component, createElement } from "react";
|
|
2
|
-
//#region src/client/state.ts
|
|
3
|
-
/** The global router singleton — set once during bootstrap. */
|
|
4
|
-
var globalRouter = null;
|
|
5
|
-
function _setGlobalRouter(router) {
|
|
6
|
-
globalRouter = router;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* ALS-backed SSR data provider. When registered, getSsrData() reads from
|
|
10
|
-
* this function (ALS store) instead of module-level currentSsrData.
|
|
11
|
-
*/
|
|
12
|
-
var ssrDataProvider;
|
|
13
|
-
/** Fallback SSR data for tests and environments without ALS. */
|
|
14
|
-
var currentSsrData;
|
|
15
|
-
function _setCurrentSsrData(data) {
|
|
16
|
-
currentSsrData = data;
|
|
17
|
-
}
|
|
18
|
-
/** Current route params snapshot — replaced (not mutated) on each navigation. */
|
|
19
|
-
var currentParams = {};
|
|
20
|
-
function _setCurrentParams(params) {
|
|
21
|
-
currentParams = params;
|
|
22
|
-
}
|
|
23
|
-
/** Cached search string — avoids reparsing when URL hasn't changed. */
|
|
24
|
-
var cachedSearch = "";
|
|
25
|
-
var cachedSearchParams = new URLSearchParams();
|
|
26
|
-
function _setCachedSearch(search, params) {
|
|
27
|
-
cachedSearch = search;
|
|
28
|
-
cachedSearchParams = params;
|
|
29
|
-
}
|
|
30
|
-
//#endregion
|
|
31
|
-
//#region src/client/ssr-data.ts
|
|
32
|
-
/**
|
|
33
|
-
* SSR Data — per-request state for client hooks during server-side rendering.
|
|
34
|
-
*
|
|
35
|
-
* RSC and SSR are separate Vite module graphs (see design/18-build-system.md),
|
|
36
|
-
* so the RSC environment's request-context ALS is not visible to SSR modules.
|
|
37
|
-
* This module provides getter/setter functions that ssr-entry.ts uses to
|
|
38
|
-
* populate per-request data for React's render.
|
|
39
|
-
*
|
|
40
|
-
* Request isolation: On the server, ssr-entry.ts registers an ALS-backed
|
|
41
|
-
* provider via registerSsrDataProvider(). getSsrData() reads from the ALS
|
|
42
|
-
* store, ensuring correct per-request data even when Suspense boundaries
|
|
43
|
-
* resolve asynchronously across concurrent requests. The module-level
|
|
44
|
-
* setSsrData/clearSsrData functions are kept as a fallback for tests
|
|
45
|
-
* and environments without ALS.
|
|
46
|
-
*
|
|
47
|
-
* IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only
|
|
48
|
-
* APIs, as it's imported by 'use client' hooks that are bundled for the browser.
|
|
49
|
-
* The ALS instance lives in ssr-entry.ts (server-only); this module only holds
|
|
50
|
-
* a reference to the provider function.
|
|
51
|
-
*
|
|
52
|
-
* All mutable state is delegated to client/state.ts for singleton guarantees.
|
|
53
|
-
* See design/18-build-system.md §"Singleton State Registry"
|
|
54
|
-
*/
|
|
55
|
-
/**
|
|
56
|
-
* Set the SSR data for the current request via module-level state.
|
|
57
|
-
*
|
|
58
|
-
* In production, ssr-entry.ts uses ALS (runWithSsrData) instead.
|
|
59
|
-
* This function is retained for tests and as a fallback.
|
|
60
|
-
*/
|
|
61
|
-
function setSsrData(data) {
|
|
62
|
-
_setCurrentSsrData(data);
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Clear the SSR data after rendering completes.
|
|
66
|
-
*
|
|
67
|
-
* In production, ALS scope handles cleanup automatically.
|
|
68
|
-
* This function is retained for tests and as a fallback.
|
|
69
|
-
*/
|
|
70
|
-
function clearSsrData() {
|
|
71
|
-
_setCurrentSsrData(void 0);
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Read the current request's SSR data. Returns undefined when called
|
|
75
|
-
* outside an SSR render (i.e. on the client after hydration).
|
|
76
|
-
*
|
|
77
|
-
* Prefers the ALS-backed provider when registered (server-side),
|
|
78
|
-
* falling back to module-level state (tests, legacy).
|
|
79
|
-
*
|
|
80
|
-
* Used by client hooks' server snapshot functions.
|
|
81
|
-
*/
|
|
82
|
-
function getSsrData() {
|
|
83
|
-
if (ssrDataProvider) return ssrDataProvider();
|
|
84
|
-
return currentSsrData;
|
|
85
|
-
}
|
|
86
|
-
//#endregion
|
|
87
|
-
//#region src/client/error-boundary.tsx
|
|
88
|
-
/**
|
|
89
|
-
* Framework-injected React error boundary.
|
|
90
|
-
*
|
|
91
|
-
* Catches errors thrown by children and renders a fallback component
|
|
92
|
-
* with the appropriate props based on error type:
|
|
93
|
-
* - DenySignal (4xx) → { status, dangerouslyPassData }
|
|
94
|
-
* - RenderError (5xx) → { error, digest, reset }
|
|
95
|
-
* - Unhandled error → { error, digest: null, reset }
|
|
96
|
-
*
|
|
97
|
-
* The `status` prop controls which errors this boundary catches:
|
|
98
|
-
* - Specific code (e.g. 403) → only that status
|
|
99
|
-
* - Category (400) → any 4xx
|
|
100
|
-
* - Category (500) → any 5xx
|
|
101
|
-
* - Omitted → catches everything (error.tsx behavior)
|
|
102
|
-
*
|
|
103
|
-
* See design/10-error-handling.md §"Status-Code Files"
|
|
104
|
-
*/
|
|
105
|
-
var _isUnloading = false;
|
|
106
|
-
if (typeof window !== "undefined") {
|
|
107
|
-
window.addEventListener("beforeunload", () => {
|
|
108
|
-
_isUnloading = true;
|
|
109
|
-
});
|
|
110
|
-
window.addEventListener("pagehide", () => {
|
|
111
|
-
_isUnloading = true;
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
var TimberErrorBoundary = class extends Component {
|
|
115
|
-
constructor(props) {
|
|
116
|
-
super(props);
|
|
117
|
-
this.state = {
|
|
118
|
-
hasError: false,
|
|
119
|
-
error: null
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
static getDerivedStateFromError(error) {
|
|
123
|
-
if (_isUnloading) return {
|
|
124
|
-
hasError: false,
|
|
125
|
-
error: null
|
|
126
|
-
};
|
|
127
|
-
return {
|
|
128
|
-
hasError: true,
|
|
129
|
-
error
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
componentDidUpdate(prevProps) {
|
|
133
|
-
if (this.state.hasError && prevProps.children !== this.props.children) this.setState({
|
|
134
|
-
hasError: false,
|
|
135
|
-
error: null
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
/** Reset the error state so children re-render. */
|
|
139
|
-
reset = () => {
|
|
140
|
-
this.setState({
|
|
141
|
-
hasError: false,
|
|
142
|
-
error: null
|
|
143
|
-
});
|
|
144
|
-
};
|
|
145
|
-
render() {
|
|
146
|
-
if (!this.state.hasError || !this.state.error) return this.props.children;
|
|
147
|
-
const error = this.state.error;
|
|
148
|
-
const parsed = parseDigest(error);
|
|
149
|
-
if (parsed?.type === "redirect") throw error;
|
|
150
|
-
if (this.props.status != null) {
|
|
151
|
-
const errorStatus = getErrorStatus(parsed, error);
|
|
152
|
-
if (errorStatus == null || !statusMatches(this.props.status, errorStatus)) throw error;
|
|
153
|
-
}
|
|
154
|
-
if (parsed?.type === "deny") {
|
|
155
|
-
if (this.props.fallbackElement == null || this.props.isSlotBoundary) {
|
|
156
|
-
const ssrData = getSsrData();
|
|
157
|
-
if (ssrData?._navContext) {
|
|
158
|
-
ssrData._navContext._denyHandledByBoundary = true;
|
|
159
|
-
if (!this.props.isSlotBoundary) ssrData._navContext.statusCode = parsed.status;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (this.props.fallbackElement != null) return this.props.fallbackElement;
|
|
164
|
-
if (parsed?.type === "deny") return createElement(this.props.fallbackComponent, {
|
|
165
|
-
status: parsed.status,
|
|
166
|
-
dangerouslyPassData: parsed.data
|
|
167
|
-
});
|
|
168
|
-
const digest = parsed?.type === "render-error" ? {
|
|
169
|
-
code: parsed.code,
|
|
170
|
-
data: parsed.data
|
|
171
|
-
} : null;
|
|
172
|
-
return createElement(this.props.fallbackComponent, {
|
|
173
|
-
error,
|
|
174
|
-
digest,
|
|
175
|
-
reset: this.reset
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
/**
|
|
180
|
-
* Parse the structured digest from the error.
|
|
181
|
-
* React sets `error.digest` from the string returned by RSC's onError.
|
|
182
|
-
*/
|
|
183
|
-
function parseDigest(error) {
|
|
184
|
-
const raw = error.digest;
|
|
185
|
-
if (typeof raw !== "string") return null;
|
|
186
|
-
try {
|
|
187
|
-
const parsed = JSON.parse(raw);
|
|
188
|
-
if (parsed && typeof parsed === "object" && typeof parsed.type === "string") return parsed;
|
|
189
|
-
} catch {}
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Extract the HTTP status code from a parsed digest or error message.
|
|
194
|
-
* Falls back to message pattern matching for errors without a digest.
|
|
195
|
-
*/
|
|
196
|
-
function getErrorStatus(parsed, error) {
|
|
197
|
-
if (parsed?.type === "deny") return parsed.status;
|
|
198
|
-
if (parsed?.type === "render-error") return parsed.status;
|
|
199
|
-
if (parsed?.type === "redirect") return parsed.status;
|
|
200
|
-
const match = error.message.match(/^Access denied with status (\d+)$/);
|
|
201
|
-
if (match) return parseInt(match[1], 10);
|
|
202
|
-
return 500;
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Check whether an error's status matches the boundary's status filter.
|
|
206
|
-
* Category markers (400, 500) match any status in that range.
|
|
207
|
-
*/
|
|
208
|
-
function statusMatches(boundaryStatus, errorStatus) {
|
|
209
|
-
if (boundaryStatus === 400) return errorStatus >= 400 && errorStatus <= 499;
|
|
210
|
-
if (boundaryStatus === 500) return errorStatus >= 500 && errorStatus <= 599;
|
|
211
|
-
return boundaryStatus === errorStatus;
|
|
212
|
-
}
|
|
213
|
-
//#endregion
|
|
214
|
-
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 };
|
|
215
|
-
|
|
216
|
-
//# sourceMappingURL=error-boundary-D9hzsveV.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"error-boundary-D9hzsveV.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 → pipeline communication.\n *\n * When TimberErrorBoundary catches a DenySignal during SSR, it:\n * 1. Sets `statusCode` to the deny status (e.g., 403) — so the HTTP\n * Response has the correct status code without a re-render.\n * 2. Sets `_denyHandledByBoundary = true` — so the pipeline skips\n * the redundant renderDenyPage() re-render.\n *\n * This runs synchronously during Fizz rendering, BEFORE onShellReady,\n * so the status code is committed before any bytes are sent.\n *\n * See TIM-664, design/04-authorization.md §\"React.cache Scope in Deny/Error Re-renders\"\n */\n _navContext?: { statusCode?: number; _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, this boundary wraps a parallel slot. Slot denials are\n * graceful degradation — they must NOT change the HTTP status code.\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 so the pipeline skips the redundant\n // renderDenyPage() re-render — but ONLY when this boundary can fully\n // handle the deny in-tree:\n //\n // ✅ TSX boundaries (fallbackComponent) — can receive runtime props\n // (status, dangerouslyPassData) dynamically in render()\n // ❌ MDX boundaries (fallbackElement) — pre-rendered at tree-build time,\n // cannot receive runtime deny props. Must fall through to re-render.\n //\n // For qualifying segment boundaries: also set the HTTP status code from\n // the DenySignal so the Response has the correct 4xx status. This runs\n // synchronously during Fizz rendering, BEFORE onShellReady.\n //\n // For slot boundaries: set _denyHandledByBoundary but do NOT change\n // statusCode — slot denials are graceful degradation.\n //\n // See TIM-664, LOCAL-298.\n if (parsed?.type === 'deny') {\n const canHandleInTree = this.props.fallbackElement == null;\n\n if (canHandleInTree || this.props.isSlotBoundary) {\n const ssrData = getSsrData();\n if (ssrData?._navContext) {\n ssrData._navContext._denyHandledByBoundary = true;\n if (!this.props.isSlotBoundary) {\n ssrData._navContext.statusCode = parsed.status;\n }\n }\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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"format-Rn922VH2.js","names":[],"sources":["../../src/server/dev-warnings.ts","../../src/utils/format.ts"],"sourcesContent":["/**\n * Dev-mode warnings for common timber.js misuse patterns.\n *\n * These fire in development only and are stripped from production builds.\n * Each warning targets a specific misuse identified during design review.\n *\n * Warnings are deduplicated by warningId:filePath:line so the same warning\n * is only emitted once per dev session (per unique source location).\n *\n * Warnings are written to stderr and, when a Vite dev server is available,\n * forwarded to the browser console via Vite's WebSocket.\n *\n * See design/21-dev-server.md §\"Dev-Mode Warnings\"\n * See design/11-platform.md §\"Dev Mode\"\n */\n\nimport type { ViteDevServer } from 'vite';\nimport { isDebug } from './debug.js';\n\n// ─── Warning IDs ───────────────────────────────────────────────────────────\n\nexport const WarningId = {\n SUSPENSE_WRAPS_CHILDREN: 'SUSPENSE_WRAPS_CHILDREN',\n DENY_IN_SUSPENSE: 'DENY_IN_SUSPENSE',\n REDIRECT_IN_SUSPENSE: 'REDIRECT_IN_SUSPENSE',\n REDIRECT_IN_ACCESS: 'REDIRECT_IN_ACCESS',\n STATIC_REQUEST_API: 'STATIC_REQUEST_API',\n SLOW_SLOT_NO_SUSPENSE: 'SLOW_SLOT_NO_SUSPENSE',\n} as const;\n\nexport type WarningId = (typeof WarningId)[keyof typeof WarningId];\n\n// ─── Configuration ──────────────────────────────────────────────────────────\n\n/** Configuration for dev warning behavior. */\nexport interface DevWarningConfig {\n /** Threshold in ms for \"slow slot\" warnings. Default: 200. */\n slowSlotThresholdMs?: number;\n}\n\n// ─── Deduplication & Server ─────────────────────────────────────────────────\n\nconst _emitted = new Set<string>();\n\n/** Vite dev server for forwarding warnings to browser console. */\nlet _viteServer: ViteDevServer | null = null;\n\n/**\n * Register the Vite dev server for browser console forwarding.\n * Called by timber-dev-server during configureServer.\n */\nexport function setViteServer(server: ViteDevServer | null): void {\n _viteServer = server;\n}\n\nfunction isDev(): boolean {\n return isDebug();\n}\n\n/**\n * Emit a warning only once per dedup key.\n *\n * Writes to stderr and forwards to browser console via Vite WebSocket.\n * Returns true if emitted (not deduplicated).\n */\nfunction emitOnce(\n warningId: WarningId,\n location: string,\n level: 'warn' | 'error',\n message: string\n): boolean {\n if (!isDev()) return false;\n\n const dedupKey = `${warningId}:${location}`;\n if (_emitted.has(dedupKey)) return false;\n _emitted.add(dedupKey);\n\n // Write to stderr\n const prefix = level === 'error' ? '\\x1b[31m[timber]\\x1b[0m' : '\\x1b[33m[timber]\\x1b[0m';\n process.stderr.write(`${prefix} ${message}\\n`);\n\n // Forward to browser console via Vite WebSocket\n if (_viteServer?.hot) {\n _viteServer.hot.send('timber:dev-warning', {\n warningId,\n level,\n message: `[timber] ${message}`,\n });\n }\n\n return true;\n}\n\n// ─── Warning Functions ──────────────────────────────────────────────────────\n\n/**\n * Warn when a layout wraps {children} in <Suspense>.\n *\n * This defers the page content — the primary resource — behind a fallback.\n * The page's data fetches won't affect the HTTP status code because they\n * resolve after onShellReady. If the page calls deny(404), the status code\n * is already committed as 200.\n *\n * @param layoutFile - Relative path to the layout file (e.g., \"app/(dashboard)/layout.tsx\")\n */\nexport function warnSuspenseWrappingChildren(layoutFile: string): void {\n emitOnce(\n WarningId.SUSPENSE_WRAPS_CHILDREN,\n layoutFile,\n 'warn',\n `Layout at ${layoutFile} wraps {children} in <Suspense>. ` +\n 'This prevents child pages from setting HTTP status codes. ' +\n 'Use useNavigationPending() for loading states instead.'\n );\n}\n\n/**\n * Warn when deny() is called inside a Suspense boundary.\n *\n * After the shell has flushed and the status code is committed, deny()\n * cannot change the HTTP response. The signal will be caught by the nearest\n * error boundary instead of producing a correct status code.\n *\n * @param file - Relative path to the file\n * @param line - Line number where deny() was called\n */\nexport function warnDenyInSuspense(file: string, line?: number): void {\n const location = line ? `${file}:${line}` : file;\n emitOnce(\n WarningId.DENY_IN_SUSPENSE,\n location,\n 'error',\n `deny() called inside <Suspense> at ${location}. ` +\n 'The HTTP status is already committed — this will trigger an error boundary with a 200 status. ' +\n 'Move deny() outside <Suspense> for correct HTTP semantics.'\n );\n}\n\n/**\n * Warn when redirect() is called inside a Suspense boundary.\n *\n * This will perform a client-side navigation instead of an HTTP redirect.\n *\n * @param file - Relative path to the file\n * @param line - Line number where redirect() was called\n */\nexport function warnRedirectInSuspense(file: string, line?: number): void {\n const location = line ? `${file}:${line}` : file;\n emitOnce(\n WarningId.REDIRECT_IN_SUSPENSE,\n location,\n 'error',\n `redirect() called inside <Suspense> at ${location}. ` +\n 'This will perform a client-side navigation instead of an HTTP redirect.'\n );\n}\n\n/**\n * Warn when redirect() is called in a slot's access.ts.\n *\n * Slots use deny() for graceful degradation. Redirecting from a slot would\n * redirect the entire page, breaking the contract that slot failure is\n * isolated to the slot.\n *\n * @param accessFile - Relative path to the access.ts file\n * @param line - Line number where redirect() was called\n */\nexport function warnRedirectInAccess(accessFile: string, line?: number): void {\n const location = line ? `${accessFile}:${line}` : accessFile;\n emitOnce(\n WarningId.REDIRECT_IN_ACCESS,\n location,\n 'error',\n `redirect() called in access.ts at ${location}. ` +\n 'Only deny() is valid in slot access checks. ' +\n 'Use deny() to block access or move redirect() to middleware.ts.'\n );\n}\n\n/**\n * Warn when cookies() or headers() is called during a static build.\n *\n * In output: 'static' mode, there is no per-request context — these APIs\n * read build-time values only. This is almost always a mistake.\n *\n * @param api - The dynamic API name (\"cookies\" or \"headers\")\n * @param file - Relative path to the file calling the API\n */\nexport function warnStaticRequestApi(api: 'cookies' | 'headers', file: string): void {\n emitOnce(\n WarningId.STATIC_REQUEST_API,\n `${api}:${file}`,\n 'error',\n `${api}() called during static generation of ${file}. ` +\n 'Dynamic request APIs are not available during prerendering.'\n );\n}\n\n// NOTE: warnCacheRequestProps removed — 'use cache' directive is a future feature.\n// See design/06-caching.md.\n\n/**\n * Warn when a parallel slot resolves slowly without a <Suspense> wrapper.\n *\n * A slow slot without Suspense blocks onShellReady — and therefore the\n * status code commit — for the entire page. Wrapping it in <Suspense>\n * lets the shell flush without waiting for the slot.\n *\n * @param slotName - The slot name (e.g., \"@admin\")\n * @param durationMs - How long the slot took to resolve\n */\nexport function warnSlowSlotWithoutSuspense(slotName: string, durationMs: number): void {\n emitOnce(\n WarningId.SLOW_SLOT_NO_SUSPENSE,\n slotName,\n 'warn',\n `Slot ${slotName} resolved in ${durationMs}ms and is not wrapped in <Suspense>. ` +\n 'Consider wrapping to avoid blocking the flush.'\n );\n}\n\n// ─── Legacy aliases ─────────────────────────────────────────────────────────\n\n/** @deprecated Use warnStaticRequestApi instead */\nexport const warnDynamicApiInStaticBuild = warnStaticRequestApi;\n\n/** @deprecated Use warnRedirectInAccess instead */\nexport function warnRedirectInSlotAccess(slotName: string): void {\n warnRedirectInAccess(`${slotName}/access.ts`);\n}\n\n/** @deprecated Use warnDenyInSuspense / warnRedirectInSuspense instead */\nexport function warnDenyAfterFlush(signal: 'deny' | 'redirect'): void {\n if (signal === 'deny') {\n warnDenyInSuspense('unknown');\n } else {\n warnRedirectInSuspense('unknown');\n }\n}\n\n// ─── Testing ────────────────────────────────────────────────────────────────\n\n/**\n * Reset emitted warnings. For testing only.\n * @internal\n */\nexport function _resetWarnings(): void {\n _emitted.clear();\n}\n\n/**\n * Get the set of emitted dedup keys. For testing only.\n * @internal\n */\nexport function _getEmitted(): ReadonlySet<string> {\n return _emitted;\n}\n","/**\n * Shared formatting utilities.\n */\n\n/** Format a byte count as a human-readable string (e.g. \"1.50 kB\"). */\nexport function formatSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} kB`;\n return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;\n}\n"],"mappings":";;AAqBA,IAAa,YAAY;CACvB,yBAAyB;CACzB,kBAAkB;CAClB,sBAAsB;CACtB,oBAAoB;CACpB,oBAAoB;CACpB,uBAAuB;CACxB;AAcD,IAAM,2BAAW,IAAI,KAAa;;AAGlC,IAAI,cAAoC;;;;;AAMxC,SAAgB,cAAc,QAAoC;AAChE,eAAc;;AAGhB,SAAS,QAAiB;AACxB,QAAO,SAAS;;;;;;;;AASlB,SAAS,SACP,WACA,UACA,OACA,SACS;AACT,KAAI,CAAC,OAAO,CAAE,QAAO;CAErB,MAAM,WAAW,GAAG,UAAU,GAAG;AACjC,KAAI,SAAS,IAAI,SAAS,CAAE,QAAO;AACnC,UAAS,IAAI,SAAS;CAGtB,MAAM,SAAS,UAAU,UAAU,4BAA4B;AAC/D,SAAQ,OAAO,MAAM,GAAG,OAAO,GAAG,QAAQ,IAAI;AAG9C,KAAI,aAAa,IACf,aAAY,IAAI,KAAK,sBAAsB;EACzC;EACA;EACA,SAAS,YAAY;EACtB,CAAC;AAGJ,QAAO;;;;;;;;;;;;AAeT,SAAgB,6BAA6B,YAA0B;AACrE,UACE,UAAU,yBACV,YACA,QACA,aAAa,WAAW,mJAGzB;;;;;;;;;;;;AAaH,SAAgB,mBAAmB,MAAc,MAAqB;CACpE,MAAM,WAAW,OAAO,GAAG,KAAK,GAAG,SAAS;AAC5C,UACE,UAAU,kBACV,UACA,SACA,sCAAsC,SAAS,4JAGhD;;;;;;;;;;AAWH,SAAgB,uBAAuB,MAAc,MAAqB;CACxE,MAAM,WAAW,OAAO,GAAG,KAAK,GAAG,SAAS;AAC5C,UACE,UAAU,sBACV,UACA,SACA,0CAA0C,SAAS,2EAEpD;;;;;;;;;;;;AAaH,SAAgB,qBAAqB,YAAoB,MAAqB;CAC5E,MAAM,WAAW,OAAO,GAAG,WAAW,GAAG,SAAS;AAClD,UACE,UAAU,oBACV,UACA,SACA,qCAAqC,SAAS,+GAG/C;;;;;;;;;;;AAYH,SAAgB,qBAAqB,KAA4B,MAAoB;AACnF,UACE,UAAU,oBACV,GAAG,IAAI,GAAG,QACV,SACA,GAAG,IAAI,wCAAwC,KAAK,+DAErD;;;;;;;;;;;;AAgBH,SAAgB,4BAA4B,UAAkB,YAA0B;AACtF,UACE,UAAU,uBACV,UACA,QACA,QAAQ,SAAS,eAAe,WAAW,qFAE5C;;;AAMH,IAAa,8BAA8B;;AAG3C,SAAgB,yBAAyB,UAAwB;AAC/D,sBAAqB,GAAG,SAAS,YAAY;;;AAI/C,SAAgB,mBAAmB,QAAmC;AACpE,KAAI,WAAW,OACb,oBAAmB,UAAU;KAE7B,wBAAuB,UAAU;;;;;;;;ACvOrC,SAAgB,WAAW,OAAuB;AAChD,KAAI,QAAQ,KAAM,QAAO,GAAG,MAAM;AAClC,KAAI,QAAQ,OAAO,KAAM,QAAO,IAAI,QAAQ,MAAM,QAAQ,EAAE,CAAC;AAC7D,QAAO,IAAI,SAAS,OAAO,OAAO,QAAQ,EAAE,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"handler-store-BVePM7hp.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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"}
|