@timber-js/app 0.2.0-alpha.4 → 0.2.0-alpha.40
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/LICENSE +8 -0
- package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
- package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
- package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
- package/dist/_chunks/debug-ECi_61pb.js +108 -0
- package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
- package/dist/_chunks/define-cookie-BmKbSyp0.js +93 -0
- package/dist/_chunks/define-cookie-BmKbSyp0.js.map +1 -0
- package/dist/_chunks/error-boundary-BAN3751q.js +211 -0
- package/dist/_chunks/error-boundary-BAN3751q.js.map +1 -0
- package/dist/_chunks/{format-CwdaB0_2.js → format-cX7wzEp2.js} +2 -2
- package/dist/_chunks/{format-CwdaB0_2.js.map → format-cX7wzEp2.js.map} +1 -1
- package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
- package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/{request-context-CZJi4CuK.js → request-context-BxYIJM24.js} +93 -69
- package/dist/_chunks/request-context-BxYIJM24.js.map +1 -0
- package/dist/_chunks/segment-context-C6byCyZU.js +69 -0
- package/dist/_chunks/segment-context-C6byCyZU.js.map +1 -0
- package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
- package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
- package/dist/_chunks/{tracing-Cwn7697K.js → tracing-CuXiCP5p.js} +17 -3
- package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-CuXiCP5p.js.map} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
- package/dist/_chunks/wrappers-C6J0nNji.js +331 -0
- package/dist/_chunks/wrappers-C6J0nNji.js.map +1 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +17 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +56 -13
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/fast-hash.d.ts +22 -0
- package/dist/cache/fast-hash.d.ts.map +1 -0
- package/dist/cache/index.d.ts +5 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +88 -18
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/register-cached-function.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts +18 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +10 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -125
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +213 -93
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts +22 -8
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +2 -2
- package/dist/client/router.d.ts +25 -3
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +23 -2
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +1 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts +1 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-merger.d.ts.map +1 -1
- package/dist/client/stale-reload.d.ts +15 -0
- package/dist/client/stale-reload.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +1 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/transition-root.d.ts +1 -1
- package/dist/client/transition-root.d.ts.map +1 -1
- package/dist/client/use-params.d.ts +2 -2
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +1 -1
- package/dist/codec.d.ts +21 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/cookies/define-cookie.d.ts +33 -12
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -83
- package/dist/fonts/css.d.ts +1 -0
- package/dist/fonts/css.d.ts.map +1 -1
- package/dist/fonts/local.d.ts +4 -2
- package/dist/fonts/local.d.ts.map +1 -1
- package/dist/index.d.ts +112 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +635 -233
- package/dist/index.js.map +1 -1
- package/dist/params/define.d.ts +76 -0
- package/dist/params/define.d.ts.map +1 -0
- package/dist/params/index.d.ts +8 -0
- package/dist/params/index.d.ts.map +1 -0
- package/dist/params/index.js +104 -0
- package/dist/params/index.js.map +1 -0
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/client-chunks.d.ts +32 -0
- package/dist/plugins/client-chunks.d.ts.map +1 -0
- package/dist/plugins/dev-error-overlay.d.ts +26 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +7 -0
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +9 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/mdx.d.ts +6 -0
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts +2 -2
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/status-file-lint.d.ts +2 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/routing/types.d.ts +6 -4
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/rsc-runtime/rsc.d.ts +1 -1
- package/dist/rsc-runtime/rsc.d.ts.map +1 -1
- package/dist/rsc-runtime/ssr.d.ts +12 -0
- package/dist/rsc-runtime/ssr.d.ts.map +1 -1
- package/dist/search-params/codecs.d.ts +1 -1
- package/dist/search-params/define.d.ts +153 -0
- package/dist/search-params/define.d.ts.map +1 -0
- package/dist/search-params/index.d.ts +4 -5
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +3 -474
- package/dist/search-params/registry.d.ts +1 -1
- package/dist/search-params/wrappers.d.ts +53 -0
- package/dist/search-params/wrappers.d.ts.map +1 -0
- package/dist/server/access-gate.d.ts +4 -0
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/action-encryption.d.ts +76 -0
- package/dist/server/action-encryption.d.ts.map +1 -0
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +18 -4
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +2 -2
- package/dist/server/debug.d.ts +46 -15
- package/dist/server/debug.d.ts.map +1 -1
- package/dist/server/default-logger.d.ts +22 -0
- package/dist/server/default-logger.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/early-hints.d.ts +13 -5
- package/dist/server/early-hints.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +4 -0
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/flight-injection-state.d.ts +78 -0
- package/dist/server/flight-injection-state.d.ts.map +1 -0
- package/dist/server/flight-scripts.d.ts +39 -0
- package/dist/server/flight-scripts.d.ts.map +1 -0
- package/dist/server/flush.d.ts.map +1 -1
- package/dist/server/form-data.d.ts +29 -0
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +5 -11
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1975 -1649
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +24 -7
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +77 -0
- package/dist/server/node-stream-transforms.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts +7 -4
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +30 -3
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-timeout.d.ts +51 -0
- package/dist/server/render-timeout.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +65 -38
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +7 -0
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-handler.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +2 -2
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +46 -3
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +6 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +22 -0
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +39 -21
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +10 -0
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +19 -12
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -3
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/version-skew.d.ts +61 -0
- package/dist/server/version-skew.d.ts.map +1 -0
- package/dist/server/waituntil-bridge.d.ts.map +1 -1
- package/dist/shared/merge-search-params.d.ts +22 -0
- package/dist/shared/merge-search-params.d.ts.map +1 -0
- package/dist/shims/navigation-client.d.ts +1 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +1 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/utils/state-machine.d.ts +80 -0
- package/dist/utils/state-machine.d.ts.map +1 -0
- package/package.json +17 -14
- package/src/adapters/compress-module.ts +24 -4
- package/src/adapters/nitro.ts +58 -9
- package/src/cache/fast-hash.ts +34 -0
- package/src/cache/index.ts +5 -2
- package/src/cache/register-cached-function.ts +7 -3
- package/src/cache/singleflight.ts +62 -4
- package/src/cache/timber-cache.ts +34 -26
- package/src/cli.ts +0 -0
- package/src/client/browser-entry.ts +94 -90
- package/src/client/error-boundary.tsx +18 -1
- package/src/client/index.ts +10 -1
- package/src/client/link.tsx +78 -19
- package/src/client/navigation-context.ts +2 -2
- package/src/client/router.ts +105 -60
- package/src/client/rsc-fetch.ts +63 -2
- package/src/client/segment-cache.ts +1 -1
- package/src/client/segment-context.ts +6 -1
- package/src/client/segment-merger.ts +2 -8
- package/src/client/stale-reload.ts +32 -6
- package/src/client/top-loader.tsx +10 -9
- package/src/client/transition-root.tsx +7 -1
- package/src/client/use-params.ts +3 -3
- package/src/client/use-query-states.ts +1 -1
- package/src/codec.ts +21 -0
- package/src/cookies/define-cookie.ts +69 -18
- package/src/fonts/css.ts +2 -1
- package/src/fonts/local.ts +7 -3
- package/src/index.ts +280 -85
- package/src/params/define.ts +260 -0
- package/src/params/index.ts +28 -0
- package/src/plugins/adapter-build.ts +6 -0
- package/src/plugins/build-manifest.ts +11 -0
- package/src/plugins/client-chunks.ts +65 -0
- package/src/plugins/dev-error-overlay.ts +70 -1
- package/src/plugins/dev-server.ts +38 -4
- package/src/plugins/entries.ts +12 -11
- package/src/plugins/fonts.ts +171 -19
- package/src/plugins/mdx.ts +9 -5
- package/src/plugins/routing.ts +40 -14
- package/src/plugins/server-bundle.ts +32 -1
- package/src/plugins/shims.ts +1 -1
- package/src/plugins/static-build.ts +8 -4
- package/src/routing/codegen.ts +109 -88
- package/src/routing/scanner.ts +55 -6
- package/src/routing/status-file-lint.ts +2 -1
- package/src/routing/types.ts +7 -4
- package/src/rsc-runtime/rsc.ts +2 -0
- package/src/rsc-runtime/ssr.ts +50 -0
- package/src/rsc-runtime/vendor-types.d.ts +7 -0
- package/src/search-params/codecs.ts +1 -1
- package/src/search-params/define.ts +504 -0
- package/src/search-params/index.ts +12 -18
- package/src/search-params/registry.ts +1 -1
- package/src/search-params/wrappers.ts +85 -0
- package/src/server/access-gate.tsx +40 -9
- package/src/server/action-client.ts +14 -5
- package/src/server/action-encryption.ts +144 -0
- package/src/server/action-handler.ts +19 -2
- package/src/server/als-registry.ts +18 -4
- package/src/server/build-manifest.ts +4 -4
- package/src/server/compress.ts +25 -7
- package/src/server/debug.ts +55 -17
- package/src/server/default-logger.ts +98 -0
- package/src/server/deny-renderer.ts +2 -1
- package/src/server/early-hints.ts +36 -15
- package/src/server/error-boundary-wrapper.ts +57 -14
- package/src/server/flight-injection-state.ts +152 -0
- package/src/server/flight-scripts.ts +59 -0
- package/src/server/flush.ts +2 -1
- package/src/server/form-data.ts +76 -0
- package/src/server/html-injectors.ts +103 -66
- package/src/server/index.ts +9 -4
- package/src/server/logger.ts +38 -35
- package/src/server/node-stream-transforms.ts +381 -0
- package/src/server/pipeline.ts +131 -39
- package/src/server/primitives.ts +47 -5
- package/src/server/render-timeout.ts +108 -0
- package/src/server/request-context.ts +112 -119
- package/src/server/route-element-builder.ts +106 -114
- package/src/server/route-handler.ts +2 -1
- package/src/server/route-matcher.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +5 -3
- package/src/server/rsc-entry/helpers.ts +122 -3
- package/src/server/rsc-entry/index.ts +125 -49
- package/src/server/rsc-entry/rsc-payload.ts +52 -12
- package/src/server/rsc-entry/rsc-stream.ts +33 -8
- package/src/server/rsc-entry/ssr-renderer.ts +40 -13
- package/src/server/slot-resolver.ts +199 -210
- package/src/server/ssr-entry.ts +169 -17
- package/src/server/ssr-render.ts +266 -67
- package/src/server/tracing.ts +23 -0
- package/src/server/tree-builder.ts +91 -57
- package/src/server/types.ts +1 -3
- package/src/server/version-skew.ts +104 -0
- package/src/server/waituntil-bridge.ts +4 -1
- package/src/shared/merge-search-params.ts +48 -0
- package/src/shims/navigation-client.ts +1 -1
- package/src/shims/navigation.ts +1 -1
- package/src/utils/state-machine.ts +111 -0
- package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
- package/dist/_chunks/debug-B4WUeqJ-.js +0 -75
- package/dist/_chunks/debug-B4WUeqJ-.js.map +0 -1
- package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
- package/dist/_chunks/request-context-CZJi4CuK.js.map +0 -1
- package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
- package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
- package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
- package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/cookies/index.js.map +0 -1
- package/dist/plugins/dynamic-transform.d.ts +0 -72
- package/dist/plugins/dynamic-transform.d.ts.map +0 -1
- package/dist/search-params/analyze.d.ts +0 -54
- package/dist/search-params/analyze.d.ts.map +0 -1
- package/dist/search-params/builtin-codecs.d.ts +0 -105
- package/dist/search-params/builtin-codecs.d.ts.map +0 -1
- package/dist/search-params/create.d.ts +0 -106
- package/dist/search-params/create.d.ts.map +0 -1
- package/dist/search-params/index.js.map +0 -1
- package/dist/server/prerender.d.ts +0 -77
- package/dist/server/prerender.d.ts.map +0 -1
- package/dist/server/response-cache.d.ts +0 -53
- package/dist/server/response-cache.d.ts.map +0 -1
- package/src/plugins/dynamic-transform.ts +0 -161
- package/src/search-params/analyze.ts +0 -192
- package/src/search-params/builtin-codecs.ts +0 -228
- package/src/search-params/create.ts +0 -321
- package/src/server/prerender.ts +0 -139
- package/src/server/response-cache.ts +0 -277
package/src/server/tracing.ts
CHANGED
|
@@ -252,6 +252,29 @@ export async function addSpanEvent(
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Fire-and-forget span event — no await, no microtask overhead.
|
|
257
|
+
*
|
|
258
|
+
* Used on the cache hot path where awaiting addSpanEvent creates an
|
|
259
|
+
* unnecessary microtask per cache operation. If OTEL is not loaded yet,
|
|
260
|
+
* the event is silently dropped (acceptable for diagnostics).
|
|
261
|
+
*
|
|
262
|
+
* See TIM-370 for perf motivation.
|
|
263
|
+
*/
|
|
264
|
+
export function addSpanEventSync(
|
|
265
|
+
name: string,
|
|
266
|
+
attributes?: Record<string, string | number | boolean>
|
|
267
|
+
): void {
|
|
268
|
+
// Fast path: if OTEL API hasn't been loaded yet, skip entirely.
|
|
269
|
+
// _otelApi is undefined (not yet loaded), null (failed to load), or the module.
|
|
270
|
+
if (!_otelApi) return;
|
|
271
|
+
|
|
272
|
+
const activeSpan = _otelApi.trace.getActiveSpan();
|
|
273
|
+
if (activeSpan) {
|
|
274
|
+
activeSpan.addEvent(name, attributes);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
255
278
|
/**
|
|
256
279
|
* Try to extract the OTEL trace ID from the current active span context.
|
|
257
280
|
* Returns undefined if OTEL is not active or no span exists.
|
|
@@ -46,10 +46,13 @@ export type SlotElements = Map<string, ReactElement>;
|
|
|
46
46
|
export interface TreeBuilderConfig {
|
|
47
47
|
/** The matched segment chain from root to leaf. */
|
|
48
48
|
segments: SegmentNode[];
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Route params extracted by the matcher (catch-all segments produce string[]).
|
|
51
|
+
* @deprecated Params are now accessed via rawSegmentParams() from ALS.
|
|
52
|
+
* This field is kept for backward compatibility but is no longer used
|
|
53
|
+
* by the tree builder itself.
|
|
54
|
+
*/
|
|
55
|
+
params?: Record<string, string | string[]>;
|
|
53
56
|
/** Loads a route file's module. */
|
|
54
57
|
loadModule: ModuleLoader;
|
|
55
58
|
/** React.createElement or equivalent. */
|
|
@@ -77,9 +80,7 @@ export interface TreeBuilderConfig {
|
|
|
77
80
|
* (backward compat for tree-builder.ts which doesn't run a pre-render pass).
|
|
78
81
|
*/
|
|
79
82
|
export interface AccessGateProps {
|
|
80
|
-
accessFn: (ctx: { params: Record<string, string | string[]
|
|
81
|
-
params: Record<string, string | string[]>;
|
|
82
|
-
searchParams: unknown;
|
|
83
|
+
accessFn: (ctx: { params: Record<string, string | string[]> }) => unknown;
|
|
83
84
|
/** Segment name for dev logging (e.g. "authenticated", "dashboard"). */
|
|
84
85
|
segmentName?: string;
|
|
85
86
|
/**
|
|
@@ -98,12 +99,19 @@ export interface AccessGateProps {
|
|
|
98
99
|
/**
|
|
99
100
|
* Framework-injected slot access gate component.
|
|
100
101
|
* On denial, renders denied.tsx → default.tsx → null instead of failing the page.
|
|
102
|
+
*
|
|
103
|
+
* DeniedComponent is passed instead of a pre-built element so that
|
|
104
|
+
* SlotAccessGate can forward DenySignal.data as dangerouslyPassData
|
|
105
|
+
* and slotName as the slot prop after catching the signal.
|
|
101
106
|
*/
|
|
102
107
|
export interface SlotAccessGateProps {
|
|
103
|
-
accessFn: (ctx: { params: Record<string, string | string[]
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
accessFn: (ctx: { params: Record<string, string | string[]> }) => unknown;
|
|
109
|
+
/** The denied.tsx component (not a pre-built element). null if no denied.tsx exists. */
|
|
110
|
+
DeniedComponent: ((...args: unknown[]) => unknown) | null;
|
|
111
|
+
/** Slot directory name without @ prefix (e.g. "admin", "sidebar"). */
|
|
112
|
+
slotName: string;
|
|
113
|
+
/** createElement function for building elements dynamically. */
|
|
114
|
+
createElement: CreateElement;
|
|
107
115
|
defaultFallback: ReactElement | null;
|
|
108
116
|
children: ReactElement;
|
|
109
117
|
}
|
|
@@ -113,7 +121,8 @@ export interface SlotAccessGateProps {
|
|
|
113
121
|
* Wraps content with status-code error boundary handling.
|
|
114
122
|
*/
|
|
115
123
|
export interface ErrorBoundaryProps {
|
|
116
|
-
fallbackComponent
|
|
124
|
+
fallbackComponent?: ReactElement | null;
|
|
125
|
+
fallbackElement?: ReactElement | null;
|
|
117
126
|
status?: number;
|
|
118
127
|
children: ReactElement;
|
|
119
128
|
}
|
|
@@ -143,8 +152,7 @@ export interface TreeBuildResult {
|
|
|
143
152
|
* Parallel slots are resolved at each layout level and composed as named props.
|
|
144
153
|
*/
|
|
145
154
|
export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeBuildResult> {
|
|
146
|
-
const { segments,
|
|
147
|
-
config;
|
|
155
|
+
const { segments, loadModule, createElement, errorBoundaryComponent } = config;
|
|
148
156
|
|
|
149
157
|
if (segments.length === 0) {
|
|
150
158
|
throw new Error('[timber] buildElementTree: empty segment chain');
|
|
@@ -168,8 +176,8 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
|
|
|
168
176
|
);
|
|
169
177
|
}
|
|
170
178
|
|
|
171
|
-
// Build the page element
|
|
172
|
-
let element: ReactElement = createElement(PageComponent, {
|
|
179
|
+
// Build the page element — params are accessed via rawSegmentParams() from ALS
|
|
180
|
+
let element: ReactElement = createElement(PageComponent, {});
|
|
173
181
|
|
|
174
182
|
// Build tree bottom-up: wrap page, then walk segments from leaf to root
|
|
175
183
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
@@ -190,8 +198,6 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
|
|
|
190
198
|
const accessFn = accessModule.default as AccessGateProps['accessFn'];
|
|
191
199
|
element = createElement('timber:access-gate', {
|
|
192
200
|
accessFn,
|
|
193
|
-
params,
|
|
194
|
-
searchParams,
|
|
195
201
|
segmentName: segment.segmentName,
|
|
196
202
|
children: element,
|
|
197
203
|
} satisfies AccessGateProps);
|
|
@@ -211,8 +217,6 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
|
|
|
211
217
|
for (const [slotName, slotNode] of segment.slots) {
|
|
212
218
|
slotProps[slotName] = await buildSlotElement(
|
|
213
219
|
slotNode,
|
|
214
|
-
params,
|
|
215
|
-
searchParams,
|
|
216
220
|
loadModule,
|
|
217
221
|
createElement,
|
|
218
222
|
errorBoundaryComponent
|
|
@@ -222,8 +226,6 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
|
|
|
222
226
|
|
|
223
227
|
element = createElement(LayoutComponent, {
|
|
224
228
|
...slotProps,
|
|
225
|
-
params,
|
|
226
|
-
searchParams,
|
|
227
229
|
children: element,
|
|
228
230
|
});
|
|
229
231
|
}
|
|
@@ -243,8 +245,6 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
|
|
|
243
245
|
*/
|
|
244
246
|
async function buildSlotElement(
|
|
245
247
|
slotNode: SegmentNode,
|
|
246
|
-
params: Record<string, string | string[]>,
|
|
247
|
-
searchParams: unknown,
|
|
248
248
|
loadModule: ModuleLoader,
|
|
249
249
|
createElement: CreateElement,
|
|
250
250
|
errorBoundaryComponent: unknown
|
|
@@ -261,10 +261,10 @@ async function buildSlotElement(
|
|
|
261
261
|
|
|
262
262
|
// If no page, render default.tsx or null
|
|
263
263
|
if (!PageComponent) {
|
|
264
|
-
return DefaultComponent ? createElement(DefaultComponent, {
|
|
264
|
+
return DefaultComponent ? createElement(DefaultComponent, {}) : null;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
let element: ReactElement = createElement(PageComponent, {
|
|
267
|
+
let element: ReactElement = createElement(PageComponent, {});
|
|
268
268
|
|
|
269
269
|
// Wrap in error boundaries
|
|
270
270
|
element = await wrapWithErrorBoundaries(
|
|
@@ -280,27 +280,19 @@ async function buildSlotElement(
|
|
|
280
280
|
const accessModule = await loadModule(slotNode.access);
|
|
281
281
|
const accessFn = accessModule.default as SlotAccessGateProps['accessFn'];
|
|
282
282
|
|
|
283
|
-
// Load denied.tsx
|
|
283
|
+
// Load denied.tsx — pass component (not pre-built element) so
|
|
284
|
+
// SlotAccessGate can forward DenySignal.data dynamically. See TIM-488.
|
|
284
285
|
const deniedModule = slotNode.denied ? await loadModule(slotNode.denied) : null;
|
|
285
|
-
const DeniedComponent =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const deniedFallback = DeniedComponent
|
|
290
|
-
? createElement(DeniedComponent, {
|
|
291
|
-
slot: slotNode.segmentName.replace(/^@/, ''),
|
|
292
|
-
dangerouslyPassData: undefined,
|
|
293
|
-
})
|
|
294
|
-
: null;
|
|
295
|
-
const defaultFallback = DefaultComponent
|
|
296
|
-
? createElement(DefaultComponent, { params, searchParams })
|
|
297
|
-
: null;
|
|
286
|
+
const DeniedComponent =
|
|
287
|
+
(deniedModule?.default as ((...args: unknown[]) => ReactElement) | undefined) ?? null;
|
|
288
|
+
|
|
289
|
+
const defaultFallback = DefaultComponent ? createElement(DefaultComponent, {}) : null;
|
|
298
290
|
|
|
299
291
|
element = createElement('timber:slot-access-gate', {
|
|
300
292
|
accessFn,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
293
|
+
DeniedComponent,
|
|
294
|
+
slotName: slotNode.segmentName.replace(/^@/, ''),
|
|
295
|
+
createElement,
|
|
304
296
|
defaultFallback,
|
|
305
297
|
children: element,
|
|
306
298
|
} satisfies SlotAccessGateProps);
|
|
@@ -311,6 +303,19 @@ async function buildSlotElement(
|
|
|
311
303
|
|
|
312
304
|
// ─── Error Boundary Wrapping ─────────────────────────────────────────────────
|
|
313
305
|
|
|
306
|
+
/** MDX/markdown extensions — these are server components that cannot be passed as function props. */
|
|
307
|
+
const MDX_EXTENSIONS = new Set(['mdx', 'md']);
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Check if a route file is an MDX/markdown file based on its extension.
|
|
311
|
+
* MDX components are server components by default and cannot cross the
|
|
312
|
+
* RSC→client boundary as function props. They must be pre-rendered as
|
|
313
|
+
* elements and passed as fallbackElement instead of fallbackComponent.
|
|
314
|
+
*/
|
|
315
|
+
function isMdxFile(file: RouteFile): boolean {
|
|
316
|
+
return MDX_EXTENSIONS.has(file.extension);
|
|
317
|
+
}
|
|
318
|
+
|
|
314
319
|
/**
|
|
315
320
|
* Wrap an element with error boundaries from a segment's status-code files.
|
|
316
321
|
*
|
|
@@ -320,6 +325,12 @@ async function buildSlotElement(
|
|
|
320
325
|
* 3. error.tsx (general error boundary)
|
|
321
326
|
*
|
|
322
327
|
* This creates the fallback chain described in design/10-error-handling.md.
|
|
328
|
+
*
|
|
329
|
+
* MDX status files are server components and cannot be passed as function
|
|
330
|
+
* props to TimberErrorBoundary (a 'use client' component). Instead, they
|
|
331
|
+
* are pre-rendered as elements and passed as fallbackElement. The error
|
|
332
|
+
* boundary renders the element directly when an error is caught.
|
|
333
|
+
* See TIM-503.
|
|
323
334
|
*/
|
|
324
335
|
async function wrapWithErrorBoundaries(
|
|
325
336
|
segment: SegmentNode,
|
|
@@ -340,11 +351,18 @@ async function wrapWithErrorBoundaries(
|
|
|
340
351
|
const mod = await loadModule(file);
|
|
341
352
|
const Component = mod.default;
|
|
342
353
|
if (Component) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
354
|
+
const boundaryProps = isMdxFile(file)
|
|
355
|
+
? ({
|
|
356
|
+
fallbackElement: createElement(Component, { status }),
|
|
357
|
+
status,
|
|
358
|
+
children: element,
|
|
359
|
+
} satisfies ErrorBoundaryProps)
|
|
360
|
+
: ({
|
|
361
|
+
fallbackComponent: Component,
|
|
362
|
+
status,
|
|
363
|
+
children: element,
|
|
364
|
+
} satisfies ErrorBoundaryProps);
|
|
365
|
+
element = createElement(errorBoundaryComponent, boundaryProps);
|
|
348
366
|
}
|
|
349
367
|
}
|
|
350
368
|
}
|
|
@@ -356,25 +374,41 @@ async function wrapWithErrorBoundaries(
|
|
|
356
374
|
const mod = await loadModule(file);
|
|
357
375
|
const Component = mod.default;
|
|
358
376
|
if (Component) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
377
|
+
const categoryStatus = key === '4xx' ? 400 : 500;
|
|
378
|
+
const boundaryProps = isMdxFile(file)
|
|
379
|
+
? ({
|
|
380
|
+
fallbackElement: createElement(Component, {}),
|
|
381
|
+
status: categoryStatus,
|
|
382
|
+
children: element,
|
|
383
|
+
} satisfies ErrorBoundaryProps)
|
|
384
|
+
: ({
|
|
385
|
+
fallbackComponent: Component,
|
|
386
|
+
status: categoryStatus,
|
|
387
|
+
children: element,
|
|
388
|
+
} satisfies ErrorBoundaryProps);
|
|
389
|
+
element = createElement(errorBoundaryComponent, boundaryProps);
|
|
364
390
|
}
|
|
365
391
|
}
|
|
366
392
|
}
|
|
367
393
|
}
|
|
368
394
|
|
|
369
395
|
// Wrap with error.tsx (outermost — catches anything not matched by status files)
|
|
396
|
+
// Note: error.tsx/error.mdx receives { error, digest, reset } props.
|
|
397
|
+
// MDX error files are pre-rendered without those props (they're static content).
|
|
370
398
|
if (segment.error) {
|
|
371
399
|
const errorModule = await loadModule(segment.error);
|
|
372
400
|
const ErrorComponent = errorModule.default;
|
|
373
401
|
if (ErrorComponent) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
402
|
+
const boundaryProps = isMdxFile(segment.error)
|
|
403
|
+
? ({
|
|
404
|
+
fallbackElement: createElement(ErrorComponent, {}),
|
|
405
|
+
children: element,
|
|
406
|
+
} satisfies ErrorBoundaryProps)
|
|
407
|
+
: ({
|
|
408
|
+
fallbackComponent: ErrorComponent,
|
|
409
|
+
children: element,
|
|
410
|
+
} satisfies ErrorBoundaryProps);
|
|
411
|
+
element = createElement(errorBoundaryComponent, boundaryProps);
|
|
378
412
|
}
|
|
379
413
|
}
|
|
380
414
|
|
package/src/server/types.ts
CHANGED
|
@@ -22,8 +22,7 @@ export interface MiddlewareContext {
|
|
|
22
22
|
req: Request;
|
|
23
23
|
requestHeaders: Headers;
|
|
24
24
|
headers: Headers;
|
|
25
|
-
|
|
26
|
-
searchParams: unknown;
|
|
25
|
+
segmentParams: Record<string, string | string[]>;
|
|
27
26
|
/** Declare early hints for critical resources. Appends Link headers. */
|
|
28
27
|
earlyHints: (hints: EarlyHint[]) => void;
|
|
29
28
|
}
|
|
@@ -37,7 +36,6 @@ export interface RouteContext {
|
|
|
37
36
|
|
|
38
37
|
export interface AccessContext {
|
|
39
38
|
params: Record<string, string | string[]>;
|
|
40
|
-
searchParams: unknown;
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
export interface Metadata {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version Skew Detection — graceful recovery when stale clients hit new deployments.
|
|
3
|
+
*
|
|
4
|
+
* When a new version of the app is deployed, clients with open tabs still have
|
|
5
|
+
* the old JavaScript bundle. Without version skew handling, these stale clients
|
|
6
|
+
* will experience:
|
|
7
|
+
*
|
|
8
|
+
* 1. Server action calls that crash (action IDs are content-hashed)
|
|
9
|
+
* 2. Chunk load failures (old filenames gone from CDN)
|
|
10
|
+
* 3. RSC payload mismatches (component references differ between builds)
|
|
11
|
+
*
|
|
12
|
+
* This module implements deployment ID comparison:
|
|
13
|
+
* - A per-build deployment ID is generated at build time (see build-manifest.ts)
|
|
14
|
+
* - The client sends it via `X-Timber-Deployment-Id` header on every RSC/action request
|
|
15
|
+
* - The server compares it against the current build's ID
|
|
16
|
+
* - On mismatch: signal the client to reload (not crash)
|
|
17
|
+
*
|
|
18
|
+
* The deployment ID is always-on in production. Dev mode skips the check
|
|
19
|
+
* (HMR handles code updates without full reloads).
|
|
20
|
+
*
|
|
21
|
+
* See design/25-production-deployments.md, TIM-446
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// ─── Constants ───────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Header sent by the client with every RSC/action request. */
|
|
27
|
+
export const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';
|
|
28
|
+
|
|
29
|
+
/** Response header that signals the client to do a full page reload. */
|
|
30
|
+
export const RELOAD_HEADER = 'X-Timber-Reload';
|
|
31
|
+
|
|
32
|
+
// ─── Deployment ID ───────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The current build's deployment ID. Set at startup from the manifest init
|
|
36
|
+
* module (globalThis.__TIMBER_DEPLOYMENT_ID__). Null in dev mode.
|
|
37
|
+
*/
|
|
38
|
+
let currentDeploymentId: string | null = null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Set the current deployment ID. Called once at server startup from the
|
|
42
|
+
* manifest init module. In dev mode this is never called (deployment ID
|
|
43
|
+
* checks are skipped).
|
|
44
|
+
*/
|
|
45
|
+
export function setDeploymentId(id: string): void {
|
|
46
|
+
currentDeploymentId = id;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the current deployment ID. Returns null in dev mode.
|
|
51
|
+
*/
|
|
52
|
+
export function getDeploymentId(): string | null {
|
|
53
|
+
return currentDeploymentId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Skew Detection ──────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/** Result of a version skew check. */
|
|
59
|
+
export interface SkewCheckResult {
|
|
60
|
+
/** Whether the client's deployment ID matches the server's. */
|
|
61
|
+
ok: boolean;
|
|
62
|
+
/** The client's deployment ID (null if header not sent — e.g., initial page load). */
|
|
63
|
+
clientId: string | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a request's deployment ID matches the current build.
|
|
68
|
+
*
|
|
69
|
+
* Returns `{ ok: true }` when:
|
|
70
|
+
* - Dev mode (no deployment ID set — HMR handles updates)
|
|
71
|
+
* - No deployment ID header (initial page load, non-RSC request)
|
|
72
|
+
* - Deployment IDs match
|
|
73
|
+
*
|
|
74
|
+
* Returns `{ ok: false }` when:
|
|
75
|
+
* - Client sends a deployment ID that differs from the current build
|
|
76
|
+
*/
|
|
77
|
+
export function checkVersionSkew(req: Request): SkewCheckResult {
|
|
78
|
+
// Dev mode — no deployment ID checks (HMR handles updates)
|
|
79
|
+
if (!currentDeploymentId) {
|
|
80
|
+
return { ok: true, clientId: null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const clientId = req.headers.get(DEPLOYMENT_ID_HEADER);
|
|
84
|
+
|
|
85
|
+
// No header — initial page load or non-RSC request. Always OK.
|
|
86
|
+
if (!clientId) {
|
|
87
|
+
return { ok: true, clientId: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Compare deployment IDs
|
|
91
|
+
if (clientId === currentDeploymentId) {
|
|
92
|
+
return { ok: true, clientId };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { ok: false, clientId };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Apply version skew reload headers to a response.
|
|
100
|
+
* Sets X-Timber-Reload: 1 to signal the client to do a full page reload.
|
|
101
|
+
*/
|
|
102
|
+
export function applyReloadHeaders(headers: Headers): void {
|
|
103
|
+
headers.set(RELOAD_HEADER, '1');
|
|
104
|
+
}
|
|
@@ -18,7 +18,10 @@ import { waitUntilAls } from './als-registry.js';
|
|
|
18
18
|
* Called by generated entry points (Nitro node-server/bun, Cloudflare)
|
|
19
19
|
* to bind the platform's lifecycle extension for the request duration.
|
|
20
20
|
*/
|
|
21
|
-
export function runWithWaitUntil<T>(
|
|
21
|
+
export function runWithWaitUntil<T>(
|
|
22
|
+
waitUntilFn: (promise: Promise<unknown>) => void,
|
|
23
|
+
fn: () => T
|
|
24
|
+
): T {
|
|
22
25
|
return waitUntilAls.run(waitUntilFn, fn);
|
|
23
26
|
}
|
|
24
27
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility for merging preserved search params into a target URL.
|
|
3
|
+
*
|
|
4
|
+
* Used by both <Link> (client) and redirect() (server). Extracted to a shared
|
|
5
|
+
* module to avoid importing client code ('use client') from server modules.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Merge preserved search params from the current URL into a target href.
|
|
10
|
+
*
|
|
11
|
+
* When `preserve` is `true`, all current search params are merged.
|
|
12
|
+
* When `preserve` is a `string[]`, only the named params are merged.
|
|
13
|
+
*
|
|
14
|
+
* The target href's own search params take precedence — preserved params
|
|
15
|
+
* are only added if the target doesn't already define them.
|
|
16
|
+
*
|
|
17
|
+
* @param targetHref - The resolved target href (may already contain query string)
|
|
18
|
+
* @param currentSearch - The current URL's search string (e.g. "?private=access&page=2")
|
|
19
|
+
* @param preserve - `true` to preserve all, or `string[]` to preserve specific params
|
|
20
|
+
* @returns The target href with preserved search params merged in
|
|
21
|
+
*/
|
|
22
|
+
export function mergePreservedSearchParams(
|
|
23
|
+
targetHref: string,
|
|
24
|
+
currentSearch: string,
|
|
25
|
+
preserve: true | string[]
|
|
26
|
+
): string {
|
|
27
|
+
const currentParams = new URLSearchParams(currentSearch);
|
|
28
|
+
if (currentParams.size === 0) return targetHref;
|
|
29
|
+
|
|
30
|
+
// Split target into path and existing query
|
|
31
|
+
const qIdx = targetHref.indexOf('?');
|
|
32
|
+
const targetPath = qIdx >= 0 ? targetHref.slice(0, qIdx) : targetHref;
|
|
33
|
+
const targetParams = new URLSearchParams(qIdx >= 0 ? targetHref.slice(qIdx + 1) : '');
|
|
34
|
+
|
|
35
|
+
// Collect params to preserve (that aren't already in the target)
|
|
36
|
+
const merged = new URLSearchParams(targetParams);
|
|
37
|
+
for (const [key, value] of currentParams) {
|
|
38
|
+
// Only preserve if: (a) not already in target, and (b) included in preserve list
|
|
39
|
+
if (!targetParams.has(key)) {
|
|
40
|
+
if (preserve === true || preserve.includes(key)) {
|
|
41
|
+
merged.append(key, value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const qs = merged.toString();
|
|
47
|
+
return qs ? `${targetPath}?${qs}` : targetPath;
|
|
48
|
+
}
|
package/src/shims/navigation.ts
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny typed state machine utility.
|
|
3
|
+
*
|
|
4
|
+
* Enforces discriminated-union states, typed transitions with runtime
|
|
5
|
+
* guards, subscribe for external store integration, and assertPhase
|
|
6
|
+
* for function entry guards.
|
|
7
|
+
*
|
|
8
|
+
* Designed for 3–5 state consumers (stream transforms, client navigation,
|
|
9
|
+
* build phase sequencing). No async, no hierarchy, no history.
|
|
10
|
+
*
|
|
11
|
+
* Performance: send() is one object lookup + one function call.
|
|
12
|
+
* Equivalent cost to a boolean check after V8 inlining.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** A state machine instance with typed state and events. */
|
|
16
|
+
export interface Machine<TState extends { phase: string }, TEvent extends { type: string }> {
|
|
17
|
+
/** Current state (discriminated union — narrowed by phase). */
|
|
18
|
+
readonly state: TState;
|
|
19
|
+
|
|
20
|
+
/** Transition with runtime guard. Throws on invalid source+event pair. */
|
|
21
|
+
send(event: TEvent): void;
|
|
22
|
+
|
|
23
|
+
/** Subscribe to state changes. Returns unsubscribe function. */
|
|
24
|
+
subscribe(listener: (state: TState) => void): () => void;
|
|
25
|
+
|
|
26
|
+
/** Throw if not in the expected phase. Entry guard for functions. */
|
|
27
|
+
assertPhase<P extends TState['phase']>(phase: P): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Transition map: `transitions[phase][eventType]` returns the next state.
|
|
32
|
+
*
|
|
33
|
+
* Each handler receives the current state (narrowed by phase context)
|
|
34
|
+
* and the event, returning the new state.
|
|
35
|
+
*/
|
|
36
|
+
export type TransitionMap<TState extends { phase: string }, TEvent extends { type: string }> = {
|
|
37
|
+
[P in TState['phase']]?: {
|
|
38
|
+
[E in TEvent['type']]?: (
|
|
39
|
+
state: Extract<TState, { phase: P }>,
|
|
40
|
+
event: Extract<TEvent, { type: E }>
|
|
41
|
+
) => TState;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface MachineConfig<TState extends { phase: string }, TEvent extends { type: string }> {
|
|
46
|
+
initial: TState;
|
|
47
|
+
transitions: TransitionMap<TState, TEvent>;
|
|
48
|
+
/** Fires after every valid transition. Use for side effects. */
|
|
49
|
+
onTransition?: (prev: TState, next: TState, event: TEvent) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a state machine from a config.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* type State = { phase: 'idle' } | { phase: 'running'; pid: number };
|
|
58
|
+
* type Event = { type: 'START'; pid: number } | { type: 'STOP' };
|
|
59
|
+
*
|
|
60
|
+
* const m = createMachine<State, Event>({
|
|
61
|
+
* initial: { phase: 'idle' },
|
|
62
|
+
* transitions: {
|
|
63
|
+
* idle: { START: (_s, e) => ({ phase: 'running', pid: e.pid }) },
|
|
64
|
+
* running: { STOP: () => ({ phase: 'idle' }) },
|
|
65
|
+
* },
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function createMachine<TState extends { phase: string }, TEvent extends { type: string }>(
|
|
70
|
+
config: MachineConfig<TState, TEvent>
|
|
71
|
+
): Machine<TState, TEvent> {
|
|
72
|
+
let current: TState = config.initial;
|
|
73
|
+
const listeners = new Set<(state: TState) => void>();
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
get state() {
|
|
77
|
+
return current;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
send(event: TEvent): void {
|
|
81
|
+
const phaseHandlers = config.transitions[current.phase as TState['phase']];
|
|
82
|
+
const handler = phaseHandlers?.[event.type as TEvent['type']] as
|
|
83
|
+
| ((state: TState, event: TEvent) => TState)
|
|
84
|
+
| undefined;
|
|
85
|
+
|
|
86
|
+
if (!handler) {
|
|
87
|
+
throw new Error(`[state-machine] Invalid transition: ${current.phase} + ${event.type}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const prev = current;
|
|
91
|
+
current = handler(prev, event);
|
|
92
|
+
|
|
93
|
+
config.onTransition?.(prev, current, event);
|
|
94
|
+
|
|
95
|
+
for (const listener of listeners) {
|
|
96
|
+
listener(current);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
subscribe(listener: (state: TState) => void): () => void {
|
|
101
|
+
listeners.add(listener);
|
|
102
|
+
return () => listeners.delete(listener);
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
assertPhase<P extends TState['phase']>(phase: P): void {
|
|
106
|
+
if (current.phase !== phase) {
|
|
107
|
+
throw new Error(`[state-machine] Expected phase "${phase}", got "${current.phase}"`);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"als-registry-B7DbZ2hS.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';\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 route's typed search params (when search-params.ts\n * exists) or to the raw URLSearchParams. Stored as a Promise so the framework\n * can later support partial pre-rendering where param resolution is deferred.\n */\n searchParamsPromise: Promise<URLSearchParams | Record<string, unknown>>;\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\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":";;;;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAa,oBAAoB,IAAI,mBAAwC;;AA4C7E,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"}
|