@timber-js/app 0.1.21 → 0.1.22

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.
Files changed (135) hide show
  1. package/dist/_chunks/als-registry-c0AGnbqS.js +39 -0
  2. package/dist/_chunks/als-registry-c0AGnbqS.js.map +1 -0
  3. package/dist/_chunks/{interception-c-a3uODY.js → interception-DGDIjDbR.js} +10 -3
  4. package/dist/_chunks/interception-DGDIjDbR.js.map +1 -0
  5. package/dist/_chunks/{metadata-routes-BDnswgRO.js → metadata-routes-CQCnF4VK.js} +14 -2
  6. package/dist/_chunks/metadata-routes-CQCnF4VK.js.map +1 -0
  7. package/dist/_chunks/{request-context-BzES06i1.js → request-context-C69VW4xS.js} +2 -4
  8. package/dist/_chunks/request-context-C69VW4xS.js.map +1 -0
  9. package/dist/_chunks/ssr-data-B2yikEEB.js +90 -0
  10. package/dist/_chunks/ssr-data-B2yikEEB.js.map +1 -0
  11. package/dist/_chunks/{tracing-BtOwb8O6.js → tracing-tIvqStk8.js} +2 -3
  12. package/dist/_chunks/tracing-tIvqStk8.js.map +1 -0
  13. package/dist/_chunks/{use-cookie-D2cZu0jK.js → use-cookie-D5aS4slY.js} +2 -2
  14. package/dist/_chunks/{use-cookie-D2cZu0jK.js.map → use-cookie-D5aS4slY.js.map} +1 -1
  15. package/dist/_chunks/{use-query-states-wEXY2JQB.js → use-query-states-DAhgj8Gx.js} +1 -1
  16. package/dist/_chunks/{use-query-states-wEXY2JQB.js.map → use-query-states-DAhgj8Gx.js.map} +1 -1
  17. package/dist/cache/index.js +2 -1
  18. package/dist/cache/index.js.map +1 -1
  19. package/dist/client/error-boundary.js +1 -1
  20. package/dist/client/index.d.ts +1 -1
  21. package/dist/client/index.d.ts.map +1 -1
  22. package/dist/client/index.js +18 -17
  23. package/dist/client/index.js.map +1 -1
  24. package/dist/client/router-ref.d.ts.map +1 -1
  25. package/dist/client/ssr-data.d.ts +3 -0
  26. package/dist/client/ssr-data.d.ts.map +1 -1
  27. package/dist/client/state.d.ts +47 -0
  28. package/dist/client/state.d.ts.map +1 -0
  29. package/dist/client/types.d.ts +10 -1
  30. package/dist/client/types.d.ts.map +1 -1
  31. package/dist/client/unload-guard.d.ts +3 -0
  32. package/dist/client/unload-guard.d.ts.map +1 -1
  33. package/dist/client/use-params.d.ts +3 -0
  34. package/dist/client/use-params.d.ts.map +1 -1
  35. package/dist/client/use-search-params.d.ts +3 -0
  36. package/dist/client/use-search-params.d.ts.map +1 -1
  37. package/dist/cookies/index.js +4 -2
  38. package/dist/cookies/index.js.map +1 -1
  39. package/dist/index.js +4 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/plugins/shims.d.ts.map +1 -1
  42. package/dist/routing/index.js +1 -1
  43. package/dist/routing/scanner.d.ts.map +1 -1
  44. package/dist/rsc-runtime/browser.d.ts +13 -0
  45. package/dist/rsc-runtime/browser.d.ts.map +1 -0
  46. package/dist/rsc-runtime/rsc.d.ts +14 -0
  47. package/dist/rsc-runtime/rsc.d.ts.map +1 -0
  48. package/dist/rsc-runtime/ssr.d.ts +13 -0
  49. package/dist/rsc-runtime/ssr.d.ts.map +1 -0
  50. package/dist/search-params/builtin-codecs.d.ts +105 -0
  51. package/dist/search-params/builtin-codecs.d.ts.map +1 -0
  52. package/dist/search-params/index.d.ts +1 -0
  53. package/dist/search-params/index.d.ts.map +1 -1
  54. package/dist/search-params/index.js +167 -2
  55. package/dist/search-params/index.js.map +1 -1
  56. package/dist/server/actions.d.ts +2 -7
  57. package/dist/server/actions.d.ts.map +1 -1
  58. package/dist/server/als-registry.d.ts +80 -0
  59. package/dist/server/als-registry.d.ts.map +1 -0
  60. package/dist/server/early-hints-sender.d.ts.map +1 -1
  61. package/dist/server/form-flash.d.ts.map +1 -1
  62. package/dist/server/index.d.ts +1 -0
  63. package/dist/server/index.d.ts.map +1 -1
  64. package/dist/server/index.js +242 -76
  65. package/dist/server/index.js.map +1 -1
  66. package/dist/server/metadata-routes.d.ts +27 -0
  67. package/dist/server/metadata-routes.d.ts.map +1 -1
  68. package/dist/server/pipeline.d.ts +7 -0
  69. package/dist/server/pipeline.d.ts.map +1 -1
  70. package/dist/server/primitives.d.ts +14 -6
  71. package/dist/server/primitives.d.ts.map +1 -1
  72. package/dist/server/request-context.d.ts +2 -32
  73. package/dist/server/request-context.d.ts.map +1 -1
  74. package/dist/server/route-matcher.d.ts +5 -0
  75. package/dist/server/route-matcher.d.ts.map +1 -1
  76. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  77. package/dist/server/rsc-entry/rsc-payload.d.ts +25 -0
  78. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -0
  79. package/dist/server/rsc-entry/rsc-stream.d.ts +43 -0
  80. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -0
  81. package/dist/server/rsc-entry/ssr-renderer.d.ts +52 -0
  82. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -0
  83. package/dist/server/rsc-prop-warnings.d.ts +53 -0
  84. package/dist/server/rsc-prop-warnings.d.ts.map +1 -0
  85. package/dist/server/server-timing.d.ts +49 -0
  86. package/dist/server/server-timing.d.ts.map +1 -0
  87. package/dist/server/tracing.d.ts +2 -6
  88. package/dist/server/tracing.d.ts.map +1 -1
  89. package/dist/server/types.d.ts +11 -0
  90. package/dist/server/types.d.ts.map +1 -1
  91. package/package.json +1 -1
  92. package/src/client/browser-entry.ts +1 -1
  93. package/src/client/index.ts +1 -1
  94. package/src/client/router-ref.ts +6 -12
  95. package/src/client/ssr-data.ts +25 -9
  96. package/src/client/state.ts +83 -0
  97. package/src/client/types.ts +18 -1
  98. package/src/client/unload-guard.ts +6 -3
  99. package/src/client/use-params.ts +10 -13
  100. package/src/client/use-search-params.ts +9 -5
  101. package/src/plugins/shims.ts +26 -2
  102. package/src/routing/scanner.ts +18 -2
  103. package/src/rsc-runtime/browser.ts +18 -0
  104. package/src/rsc-runtime/rsc.ts +19 -0
  105. package/src/rsc-runtime/ssr.ts +13 -0
  106. package/src/search-params/builtin-codecs.ts +228 -0
  107. package/src/search-params/index.ts +11 -0
  108. package/src/server/action-handler.ts +1 -1
  109. package/src/server/actions.ts +4 -10
  110. package/src/server/als-registry.ts +116 -0
  111. package/src/server/deny-renderer.ts +1 -1
  112. package/src/server/early-hints-sender.ts +1 -3
  113. package/src/server/form-flash.ts +1 -5
  114. package/src/server/index.ts +1 -0
  115. package/src/server/metadata-routes.ts +61 -0
  116. package/src/server/pipeline.ts +164 -38
  117. package/src/server/primitives.ts +110 -6
  118. package/src/server/request-context.ts +8 -36
  119. package/src/server/route-matcher.ts +25 -2
  120. package/src/server/rsc-entry/error-renderer.ts +1 -1
  121. package/src/server/rsc-entry/index.ts +42 -380
  122. package/src/server/rsc-entry/rsc-payload.ts +126 -0
  123. package/src/server/rsc-entry/rsc-stream.ts +162 -0
  124. package/src/server/rsc-entry/ssr-renderer.ts +228 -0
  125. package/src/server/rsc-prop-warnings.ts +187 -0
  126. package/src/server/server-timing.ts +132 -0
  127. package/src/server/ssr-entry.ts +1 -1
  128. package/src/server/tracing.ts +3 -11
  129. package/src/server/types.ts +16 -0
  130. package/dist/_chunks/interception-c-a3uODY.js.map +0 -1
  131. package/dist/_chunks/metadata-routes-BDnswgRO.js.map +0 -1
  132. package/dist/_chunks/request-context-BzES06i1.js.map +0 -1
  133. package/dist/_chunks/ssr-data-BgSwMbN9.js +0 -38
  134. package/dist/_chunks/ssr-data-BgSwMbN9.js.map +0 -1
  135. package/dist/_chunks/tracing-BtOwb8O6.js.map +0 -1
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Built-in search param codecs for common types.
3
+ *
4
+ * These provide zero-dependency alternatives to nuqs parsers for the most
5
+ * common cases: strings, integers, floats, booleans, and string enums.
6
+ *
7
+ * All codecs implement SearchParamCodec<T | null> — returning null when the
8
+ * param is absent or unparseable. Use withDefault() to replace null with a
9
+ * concrete fallback value.
10
+ *
11
+ * Design doc: design/23-search-params.md §"Identified Gaps" #1
12
+ * Task: TIM-362
13
+ */
14
+
15
+ import type { SearchParamCodec } from './create.js';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Helpers
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Normalize array inputs to a single string (last value wins, matching
23
+ * URLSearchParams.get() semantics). Returns undefined if absent or empty.
24
+ */
25
+ function normalizeInput(value: string | string[] | undefined): string | undefined {
26
+ if (Array.isArray(value)) {
27
+ return value.length > 0 ? value[value.length - 1] : undefined;
28
+ }
29
+ return value;
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // parseAsString
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * String codec. Returns the raw string value, or null if absent.
38
+ *
39
+ * ```ts
40
+ * import { parseAsString } from '@timber-js/app/search-params'
41
+ *
42
+ * const def = createSearchParams({ q: parseAsString })
43
+ * // ?q=shoes → { q: 'shoes' }
44
+ * // (absent) → { q: null }
45
+ * ```
46
+ */
47
+ export const parseAsString: SearchParamCodec<string | null> = {
48
+ parse(value: string | string[] | undefined): string | null {
49
+ const v = normalizeInput(value);
50
+ return v !== undefined ? v : null;
51
+ },
52
+ serialize(value: string | null): string | null {
53
+ return value;
54
+ },
55
+ };
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // parseAsInteger
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * Integer codec. Parses a base-10 integer, or returns null if absent or
63
+ * not a valid integer. Rejects floats, NaN, Infinity, and non-numeric strings.
64
+ *
65
+ * ```ts
66
+ * import { parseAsInteger, withDefault } from '@timber-js/app/search-params'
67
+ *
68
+ * const def = createSearchParams({ page: withDefault(parseAsInteger, 1) })
69
+ * // ?page=2 → { page: 2 }
70
+ * // ?page=abc → { page: 1 }
71
+ * // (absent) → { page: 1 }
72
+ * ```
73
+ */
74
+ export const parseAsInteger: SearchParamCodec<number | null> = {
75
+ parse(value: string | string[] | undefined): number | null {
76
+ const v = normalizeInput(value);
77
+ if (v === undefined || v === '') return null;
78
+ const n = Number(v);
79
+ if (!Number.isFinite(n) || !Number.isInteger(n)) return null;
80
+ return n;
81
+ },
82
+ serialize(value: number | null): string | null {
83
+ return value === null ? null : String(value);
84
+ },
85
+ };
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // parseAsFloat
89
+ // ---------------------------------------------------------------------------
90
+
91
+ /**
92
+ * Float codec. Parses a finite number, or returns null if absent or invalid.
93
+ * Rejects NaN and Infinity.
94
+ *
95
+ * ```ts
96
+ * import { parseAsFloat, withDefault } from '@timber-js/app/search-params'
97
+ *
98
+ * const def = createSearchParams({ price: withDefault(parseAsFloat, 0) })
99
+ * ```
100
+ */
101
+ export const parseAsFloat: SearchParamCodec<number | null> = {
102
+ parse(value: string | string[] | undefined): number | null {
103
+ const v = normalizeInput(value);
104
+ if (v === undefined || v === '') return null;
105
+ const n = Number(v);
106
+ if (!Number.isFinite(n)) return null;
107
+ return n;
108
+ },
109
+ serialize(value: number | null): string | null {
110
+ return value === null ? null : String(value);
111
+ },
112
+ };
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // parseAsBoolean
116
+ // ---------------------------------------------------------------------------
117
+
118
+ /**
119
+ * Boolean codec. Accepts "true"/"1" as true, "false"/"0" as false.
120
+ * Returns null for absent or unrecognized values.
121
+ *
122
+ * ```ts
123
+ * import { parseAsBoolean, withDefault } from '@timber-js/app/search-params'
124
+ *
125
+ * const def = createSearchParams({ debug: withDefault(parseAsBoolean, false) })
126
+ * // ?debug=true → { debug: true }
127
+ * // ?debug=0 → { debug: false }
128
+ * ```
129
+ */
130
+ export const parseAsBoolean: SearchParamCodec<boolean | null> = {
131
+ parse(value: string | string[] | undefined): boolean | null {
132
+ const v = normalizeInput(value);
133
+ if (v === undefined) return null;
134
+ if (v === 'true' || v === '1') return true;
135
+ if (v === 'false' || v === '0') return false;
136
+ return null;
137
+ },
138
+ serialize(value: boolean | null): string | null {
139
+ return value === null ? null : String(value);
140
+ },
141
+ };
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // parseAsStringEnum
145
+ // ---------------------------------------------------------------------------
146
+
147
+ /**
148
+ * String enum codec. Accepts only values in the provided list.
149
+ * Returns null for absent or invalid values.
150
+ *
151
+ * ```ts
152
+ * import { parseAsStringEnum, withDefault } from '@timber-js/app/search-params'
153
+ *
154
+ * const sortCodec = withDefault(
155
+ * parseAsStringEnum(['price', 'name', 'date']),
156
+ * 'date'
157
+ * )
158
+ * ```
159
+ */
160
+ export function parseAsStringEnum<T extends string>(
161
+ values: readonly T[]
162
+ ): SearchParamCodec<T | null> {
163
+ const allowed = new Set<string>(values);
164
+ return {
165
+ parse(value: string | string[] | undefined): T | null {
166
+ const v = normalizeInput(value);
167
+ if (v === undefined) return null;
168
+ return allowed.has(v) ? (v as T) : null;
169
+ },
170
+ serialize(value: T | null): string | null {
171
+ return value;
172
+ },
173
+ };
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // parseAsStringLiteral
178
+ // ---------------------------------------------------------------------------
179
+
180
+ /**
181
+ * String literal codec. Functionally identical to parseAsStringEnum but
182
+ * accepts `as const` tuples for narrower type inference.
183
+ *
184
+ * ```ts
185
+ * import { parseAsStringLiteral } from '@timber-js/app/search-params'
186
+ *
187
+ * const sizes = ['sm', 'md', 'lg', 'xl'] as const
188
+ * const codec = parseAsStringLiteral(sizes)
189
+ * // Type: SearchParamCodec<'sm' | 'md' | 'lg' | 'xl' | null>
190
+ * ```
191
+ */
192
+ export function parseAsStringLiteral<const T extends readonly string[]>(
193
+ values: T
194
+ ): SearchParamCodec<T[number] | null> {
195
+ // Delegates to parseAsStringEnum — same runtime behavior, different type
196
+ return parseAsStringEnum<T[number]>(values);
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // withDefault
201
+ // ---------------------------------------------------------------------------
202
+
203
+ /**
204
+ * Wrap a nullable codec with a default value. When the inner codec returns
205
+ * null, the default is used instead. The output type becomes non-nullable.
206
+ *
207
+ * ```ts
208
+ * import { parseAsInteger, withDefault } from '@timber-js/app/search-params'
209
+ *
210
+ * const page = withDefault(parseAsInteger, 1)
211
+ * // page.parse(undefined) → 1 (not null)
212
+ * // page.parse('5') → 5
213
+ * ```
214
+ */
215
+ export function withDefault<T>(
216
+ codec: SearchParamCodec<T | null>,
217
+ defaultValue: T
218
+ ): SearchParamCodec<T> {
219
+ return {
220
+ parse(value: string | string[] | undefined): T {
221
+ const result = codec.parse(value);
222
+ return result === null ? defaultValue : result;
223
+ },
224
+ serialize(value: T): string | null {
225
+ return codec.serialize(value);
226
+ },
227
+ };
228
+ }
@@ -15,6 +15,17 @@ export { createSearchParams } from './create.js';
15
15
  // Codec bridges
16
16
  export { fromSchema, fromArraySchema } from './codecs.js';
17
17
 
18
+ // Built-in codecs
19
+ export {
20
+ parseAsString,
21
+ parseAsInteger,
22
+ parseAsFloat,
23
+ parseAsBoolean,
24
+ parseAsStringEnum,
25
+ parseAsStringLiteral,
26
+ withDefault,
27
+ } from './builtin-codecs.js';
28
+
18
29
  // Runtime registry (route-scoped useQueryStates)
19
30
  export { registerSearchParams, getSearchParams } from './registry.js';
20
31
 
@@ -18,7 +18,7 @@ import {
18
18
  decodeReply,
19
19
  decodeAction,
20
20
  renderToReadableStream,
21
- } from '@vitejs/plugin-rsc/rsc';
21
+ } from '#/rsc-runtime/rsc.js';
22
22
 
23
23
  import { validateCsrf, type CsrfConfig } from './csrf.js';
24
24
  import { executeAction, type RevalidateRenderer } from './actions.js';
@@ -14,10 +14,10 @@
14
14
  * See design/08-forms-and-actions.md
15
15
  */
16
16
 
17
- import { AsyncLocalStorage } from 'node:async_hooks';
18
17
  import type { CacheHandler } from '#/cache/index';
19
18
  import { RedirectSignal } from './primitives';
20
19
  import { withSpan } from './tracing';
20
+ import { revalidationAls, type RevalidationState } from './als-registry.js';
21
21
 
22
22
  // ─── Types ───────────────────────────────────────────────────────────────
23
23
 
@@ -32,13 +32,8 @@ export interface RevalidationResult {
32
32
  /** Renderer function that builds a React element tree for a given path. */
33
33
  export type RevalidateRenderer = (path: string) => Promise<RevalidationResult>;
34
34
 
35
- /** Per-request revalidation state tracks revalidatePath/Tag calls within an action. */
36
- export interface RevalidationState {
37
- /** Paths to re-render (populated by revalidatePath calls). */
38
- paths: string[];
39
- /** Tags to invalidate (populated by revalidateTag calls). */
40
- tags: string[];
41
- }
35
+ // Re-export the type from the registry for public API consumers.
36
+ export type { RevalidationState } from './als-registry.js';
42
37
 
43
38
  /** Options for creating the action handler. */
44
39
  export interface ActionHandlerConfig {
@@ -62,10 +57,9 @@ export interface ActionHandlerResult {
62
57
 
63
58
  // ─── Revalidation State ──────────────────────────────────────────────────
64
59
 
65
- // Per-request revalidation state stored in AsyncLocalStorage.
60
+ // Per-request revalidation state stored in AsyncLocalStorage (from als-registry.ts).
66
61
  // This ensures concurrent requests never share or overwrite each other's state
67
62
  // (the previous module-level global was vulnerable to cross-request pollution).
68
- const revalidationAls = new AsyncLocalStorage<RevalidationState>();
69
63
 
70
64
  /**
71
65
  * Set the revalidation state for the current action execution.
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Centralized AsyncLocalStorage registry for server-side per-request state.
3
+ *
4
+ * ALL ALS instances used by the server framework live here. Individual
5
+ * modules (request-context.ts, tracing.ts, actions.ts, etc.) import from
6
+ * this registry and re-export public accessor functions.
7
+ *
8
+ * Why: ALS instances require singleton semantics — if two copies of the
9
+ * same ALS exist (one from a relative import, one from a barrel import),
10
+ * one module writes to its copy and another reads from an empty copy.
11
+ * Centralizing ALS creation in a single module eliminates this class of bug.
12
+ *
13
+ * The `timber-shims` plugin ensures `@timber-js/app/server` resolves to
14
+ * src/ in RSC and SSR environments, so all import paths converge here.
15
+ *
16
+ * DO NOT create ALS instances outside this file. If you need a new ALS,
17
+ * add it here and import from `./als-registry.js` in the consuming module.
18
+ *
19
+ * See design/18-build-system.md §"Module Singleton Strategy" and
20
+ * §"Singleton State Registry".
21
+ */
22
+
23
+ import { AsyncLocalStorage } from 'node:async_hooks';
24
+
25
+ // ─── Request Context ──────────────────────────────────────────────────────
26
+ // Used by: request-context.ts (headers(), cookies(), searchParams())
27
+ // Design doc: design/04-authorization.md
28
+
29
+ /** @internal — import via request-context.ts public API */
30
+ export const requestContextAls = new AsyncLocalStorage<RequestContextStore>();
31
+
32
+ export interface RequestContextStore {
33
+ /** Incoming request headers (read-only view). */
34
+ headers: Headers;
35
+ /** Raw cookie header string, parsed lazily into a Map on first access. */
36
+ cookieHeader: string;
37
+ /** Lazily-parsed cookie map (mutable — reflects write-overlay from set()). */
38
+ parsedCookies?: Map<string, string>;
39
+ /** Original (pre-overlay) frozen headers, kept for overlay merging. */
40
+ originalHeaders: Headers;
41
+ /**
42
+ * Promise resolving to the route's typed search params (when search-params.ts
43
+ * exists) or to the raw URLSearchParams. Stored as a Promise so the framework
44
+ * can later support partial pre-rendering where param resolution is deferred.
45
+ */
46
+ searchParamsPromise: Promise<URLSearchParams | Record<string, unknown>>;
47
+ /** Outgoing Set-Cookie entries (name → serialized value + options). Last write wins. */
48
+ cookieJar: Map<string, CookieEntry>;
49
+ /** Whether the response has flushed (headers committed). */
50
+ flushed: boolean;
51
+ /** Whether the current context allows cookie mutation. */
52
+ mutableContext: boolean;
53
+ }
54
+
55
+ /** A single outgoing cookie entry in the cookie jar. */
56
+ export interface CookieEntry {
57
+ name: string;
58
+ value: string;
59
+ options: import('./request-context.js').CookieOptions;
60
+ }
61
+
62
+ // ─── Tracing ──────────────────────────────────────────────────────────────
63
+ // Used by: tracing.ts (traceId(), spanId())
64
+ // Design doc: design/17-logging.md
65
+
66
+ export interface TraceStore {
67
+ /** 32-char lowercase hex trace ID (OTEL or UUID fallback). */
68
+ traceId: string;
69
+ /** OTEL span ID if available, undefined otherwise. */
70
+ spanId?: string;
71
+ }
72
+
73
+ /** @internal — import via tracing.ts public API */
74
+ export const traceAls = new AsyncLocalStorage<TraceStore>();
75
+
76
+ // ─── Server-Timing ────────────────────────────────────────────────────────
77
+ // Used by: server-timing.ts (recordTiming(), withTiming())
78
+ // Design doc: (dev-only performance instrumentation)
79
+
80
+ export interface TimingStore {
81
+ entries: import('./server-timing.js').TimingEntry[];
82
+ }
83
+
84
+ /** @internal — import via server-timing.ts public API */
85
+ export const timingAls = new AsyncLocalStorage<TimingStore>();
86
+
87
+ // ─── Revalidation ─────────────────────────────────────────────────────────
88
+ // Used by: actions.ts (revalidatePath(), revalidateTag())
89
+ // Design doc: design/08-forms-and-actions.md
90
+
91
+ export interface RevalidationState {
92
+ /** Paths to re-render (populated by revalidatePath calls). */
93
+ paths: string[];
94
+ /** Tags to invalidate (populated by revalidateTag calls). */
95
+ tags: string[];
96
+ }
97
+
98
+ /** @internal — import via actions.ts public API */
99
+ export const revalidationAls = new AsyncLocalStorage<RevalidationState>();
100
+
101
+ // ─── Form Flash ───────────────────────────────────────────────────────────
102
+ // Used by: form-flash.ts (getFormFlash())
103
+ // Design doc: design/08-forms-and-actions.md §"No-JS Error Round-Trip"
104
+
105
+ /** @internal — import via form-flash.ts public API */
106
+ export const formFlashAls = new AsyncLocalStorage<import('./form-flash.js').FormFlashData>();
107
+
108
+ // ─── Early Hints Sender ──────────────────────────────────────────────────
109
+ // Used by: early-hints-sender.ts (sendEarlyHints103())
110
+ // Design doc: design/02-rendering-pipeline.md §"Early Hints (103)"
111
+
112
+ /** Function that sends Link header values as a 103 Early Hints response. */
113
+ export type EarlyHintsSenderFn = (links: string[]) => void;
114
+
115
+ /** @internal — import via early-hints-sender.ts public API */
116
+ export const earlyHintsSenderAls = new AsyncLocalStorage<EarlyHintsSenderFn>();
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { createElement } from 'react';
19
- import { renderToReadableStream } from '@vitejs/plugin-rsc/rsc';
19
+ import { renderToReadableStream } from '#/rsc-runtime/rsc.js';
20
20
 
21
21
  import { DenySignal } from './primitives.js';
22
22
  import { logRenderError } from './logger.js';
@@ -18,13 +18,11 @@
18
18
  * Design doc: 02-rendering-pipeline.md §"Early Hints (103)"
19
19
  */
20
20
 
21
- import { AsyncLocalStorage } from 'node:async_hooks';
21
+ import { earlyHintsSenderAls } from './als-registry.js';
22
22
 
23
23
  /** Function that sends Link header values as a 103 Early Hints response. */
24
24
  export type EarlyHintsSenderFn = (links: string[]) => void;
25
25
 
26
- const earlyHintsSenderAls = new AsyncLocalStorage<EarlyHintsSenderFn>();
27
-
28
26
  /**
29
27
  * Run a function with a per-request early hints sender installed.
30
28
  *
@@ -14,8 +14,8 @@
14
14
  * See design/08-forms-and-actions.md §"No-JS Error Round-Trip"
15
15
  */
16
16
 
17
- import { AsyncLocalStorage } from 'node:async_hooks';
18
17
  import type { ValidationErrors } from './action-client.js';
18
+ import { formFlashAls } from './als-registry.js';
19
19
 
20
20
  // ─── Types ───────────────────────────────────────────────────────────────
21
21
 
@@ -43,10 +43,6 @@ export interface FormFlashData {
43
43
  serverError?: { code: string; data?: Record<string, unknown> };
44
44
  }
45
45
 
46
- // ─── ALS Store ───────────────────────────────────────────────────────────
47
-
48
- const formFlashAls = new AsyncLocalStorage<FormFlashData>();
49
-
50
46
  // ─── Public API ──────────────────────────────────────────────────────────
51
47
 
52
48
  /**
@@ -36,6 +36,7 @@ export {
36
36
  RedirectSignal,
37
37
  } from './primitives';
38
38
  export type { RenderErrorDigest, WaitUntilAdapter } from './primitives';
39
+ export type { JsonSerializable } from './types';
39
40
 
40
41
  // Pipeline
41
42
  export { createPipeline } from './pipeline';
@@ -121,6 +121,67 @@ export const METADATA_ROUTE_CONVENTIONS: Record<
121
121
  },
122
122
  };
123
123
 
124
+ // ─── MIME Type Resolution ─────────────────────────────────────────────────────
125
+
126
+ /**
127
+ * Map of file extensions to MIME types for static metadata route files.
128
+ * Used to resolve the generic `image/*` content type for static image files.
129
+ */
130
+ const EXTENSION_MIME_TYPES: Record<string, string> = {
131
+ xml: 'application/xml',
132
+ txt: 'text/plain',
133
+ json: 'application/json',
134
+ ico: 'image/x-icon',
135
+ png: 'image/png',
136
+ jpg: 'image/jpeg',
137
+ jpeg: 'image/jpeg',
138
+ svg: 'image/svg+xml',
139
+ webp: 'image/webp',
140
+ };
141
+
142
+ /**
143
+ * Resolve the concrete MIME type for a static metadata route file.
144
+ *
145
+ * For generic content types like `image/*`, this resolves to the actual
146
+ * MIME type based on the file extension (e.g. `image/png` for `.png`).
147
+ *
148
+ * @param conventionContentType - The content type from the convention table (may be generic like `image/*`)
149
+ * @param extension - The file extension without leading dot (e.g. "png", "xml")
150
+ * @returns The resolved MIME type
151
+ */
152
+ export function resolveStaticContentType(conventionContentType: string, extension: string): string {
153
+ if (conventionContentType.includes('*')) {
154
+ return EXTENSION_MIME_TYPES[extension] ?? 'application/octet-stream';
155
+ }
156
+ return conventionContentType;
157
+ }
158
+
159
+ /**
160
+ * Check if a file extension represents a static (non-code) metadata route file.
161
+ *
162
+ * @param baseName - The base file name without extension (e.g. "sitemap", "icon")
163
+ * @param extension - The file extension without leading dot (e.g. "xml", "png", "ts")
164
+ * @returns true if this is a static file, false if dynamic or unrecognized
165
+ */
166
+ export function isStaticMetadataExtension(baseName: string, extension: string): boolean {
167
+ const convention = METADATA_ROUTE_CONVENTIONS[baseName];
168
+ if (!convention) return false;
169
+ return convention.staticExtensions.includes(extension);
170
+ }
171
+
172
+ /**
173
+ * Check if a file extension represents a dynamic (code) metadata route file.
174
+ *
175
+ * @param baseName - The base file name without extension (e.g. "sitemap", "icon")
176
+ * @param extension - The file extension without leading dot (e.g. "ts", "tsx")
177
+ * @returns true if this is a dynamic file, false if static or unrecognized
178
+ */
179
+ export function isDynamicMetadataExtension(baseName: string, extension: string): boolean {
180
+ const convention = METADATA_ROUTE_CONVENTIONS[baseName];
181
+ if (!convention) return false;
182
+ return convention.dynamicExtensions.includes(extension);
183
+ }
184
+
124
185
  // ─── Classification ──────────────────────────────────────────────────────────
125
186
 
126
187
  /**