@timber-js/app 0.2.0-alpha.34 → 0.2.0-alpha.35

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 (230) hide show
  1. package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
  2. package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
  3. package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
  4. package/dist/_chunks/{debug-B3Gypr3D.js → debug-ECi_61pb.js} +1 -1
  5. package/dist/_chunks/{debug-B3Gypr3D.js.map → debug-ECi_61pb.js.map} +1 -1
  6. package/dist/_chunks/define-cookie-w5GWm_bL.js +93 -0
  7. package/dist/_chunks/define-cookie-w5GWm_bL.js.map +1 -0
  8. package/dist/_chunks/error-boundary-TYEQJZ1-.js +211 -0
  9. package/dist/_chunks/error-boundary-TYEQJZ1-.js.map +1 -0
  10. package/dist/_chunks/{format-RyoGQL74.js → format-cX7wzEp2.js} +2 -2
  11. package/dist/_chunks/{format-RyoGQL74.js.map → format-cX7wzEp2.js.map} +1 -1
  12. package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
  13. package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
  14. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
  15. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  16. package/dist/_chunks/{request-context-BQUC8PHn.js → request-context-CZz_T0Bc.js} +40 -71
  17. package/dist/_chunks/request-context-CZz_T0Bc.js.map +1 -0
  18. package/dist/_chunks/segment-context-Dpq2XOKg.js +34 -0
  19. package/dist/_chunks/segment-context-Dpq2XOKg.js.map +1 -0
  20. package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
  21. package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
  22. package/dist/_chunks/{tracing-CemImE6h.js → tracing-BPyIzIdu.js} +2 -2
  23. package/dist/_chunks/{tracing-CemImE6h.js.map → tracing-BPyIzIdu.js.map} +1 -1
  24. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
  25. package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
  26. package/dist/_chunks/wrappers-C1SN725w.js +331 -0
  27. package/dist/_chunks/wrappers-C1SN725w.js.map +1 -0
  28. package/dist/cache/index.js +1 -1
  29. package/dist/client/error-boundary.d.ts +10 -1
  30. package/dist/client/error-boundary.d.ts.map +1 -1
  31. package/dist/client/error-boundary.js +1 -125
  32. package/dist/client/index.d.ts +2 -2
  33. package/dist/client/index.d.ts.map +1 -1
  34. package/dist/client/index.js +193 -90
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/link.d.ts +8 -8
  37. package/dist/client/link.d.ts.map +1 -1
  38. package/dist/client/navigation-context.d.ts +2 -2
  39. package/dist/client/router.d.ts +25 -3
  40. package/dist/client/router.d.ts.map +1 -1
  41. package/dist/client/rsc-fetch.d.ts +23 -2
  42. package/dist/client/rsc-fetch.d.ts.map +1 -1
  43. package/dist/client/segment-cache.d.ts +1 -1
  44. package/dist/client/segment-cache.d.ts.map +1 -1
  45. package/dist/client/stale-reload.d.ts +15 -0
  46. package/dist/client/stale-reload.d.ts.map +1 -1
  47. package/dist/client/top-loader.d.ts +1 -1
  48. package/dist/client/top-loader.d.ts.map +1 -1
  49. package/dist/client/use-params.d.ts +2 -2
  50. package/dist/client/use-params.d.ts.map +1 -1
  51. package/dist/client/use-query-states.d.ts +1 -1
  52. package/dist/codec.d.ts +21 -0
  53. package/dist/codec.d.ts.map +1 -0
  54. package/dist/cookies/define-cookie.d.ts +33 -12
  55. package/dist/cookies/define-cookie.d.ts.map +1 -1
  56. package/dist/cookies/index.js +1 -81
  57. package/dist/index.d.ts +87 -12
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +346 -210
  60. package/dist/index.js.map +1 -1
  61. package/dist/params/define.d.ts +76 -0
  62. package/dist/params/define.d.ts.map +1 -0
  63. package/dist/params/index.d.ts +8 -0
  64. package/dist/params/index.d.ts.map +1 -0
  65. package/dist/params/index.js +104 -0
  66. package/dist/params/index.js.map +1 -0
  67. package/dist/plugins/adapter-build.d.ts.map +1 -1
  68. package/dist/plugins/build-manifest.d.ts.map +1 -1
  69. package/dist/plugins/client-chunks.d.ts +32 -0
  70. package/dist/plugins/client-chunks.d.ts.map +1 -0
  71. package/dist/plugins/entries.d.ts.map +1 -1
  72. package/dist/plugins/routing.d.ts.map +1 -1
  73. package/dist/plugins/server-bundle.d.ts.map +1 -1
  74. package/dist/plugins/static-build.d.ts.map +1 -1
  75. package/dist/routing/codegen.d.ts +2 -2
  76. package/dist/routing/codegen.d.ts.map +1 -1
  77. package/dist/routing/index.js +1 -1
  78. package/dist/routing/scanner.d.ts.map +1 -1
  79. package/dist/routing/status-file-lint.d.ts +2 -1
  80. package/dist/routing/status-file-lint.d.ts.map +1 -1
  81. package/dist/routing/types.d.ts +6 -4
  82. package/dist/routing/types.d.ts.map +1 -1
  83. package/dist/rsc-runtime/rsc.d.ts +1 -1
  84. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  85. package/dist/search-params/codecs.d.ts +1 -1
  86. package/dist/search-params/define.d.ts +153 -0
  87. package/dist/search-params/define.d.ts.map +1 -0
  88. package/dist/search-params/index.d.ts +4 -5
  89. package/dist/search-params/index.d.ts.map +1 -1
  90. package/dist/search-params/index.js +3 -474
  91. package/dist/search-params/registry.d.ts +1 -1
  92. package/dist/search-params/wrappers.d.ts +53 -0
  93. package/dist/search-params/wrappers.d.ts.map +1 -0
  94. package/dist/server/access-gate.d.ts +4 -0
  95. package/dist/server/access-gate.d.ts.map +1 -1
  96. package/dist/server/action-encryption.d.ts +76 -0
  97. package/dist/server/action-encryption.d.ts.map +1 -0
  98. package/dist/server/action-handler.d.ts.map +1 -1
  99. package/dist/server/als-registry.d.ts +4 -4
  100. package/dist/server/als-registry.d.ts.map +1 -1
  101. package/dist/server/build-manifest.d.ts +2 -2
  102. package/dist/server/early-hints.d.ts +13 -5
  103. package/dist/server/early-hints.d.ts.map +1 -1
  104. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  105. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  106. package/dist/server/flight-injection-state.d.ts +78 -0
  107. package/dist/server/flight-injection-state.d.ts.map +1 -0
  108. package/dist/server/form-data.d.ts +29 -0
  109. package/dist/server/form-data.d.ts.map +1 -1
  110. package/dist/server/html-injectors.d.ts.map +1 -1
  111. package/dist/server/index.d.ts +1 -1
  112. package/dist/server/index.d.ts.map +1 -1
  113. package/dist/server/index.js +1819 -1629
  114. package/dist/server/index.js.map +1 -1
  115. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  116. package/dist/server/pipeline.d.ts.map +1 -1
  117. package/dist/server/request-context.d.ts +28 -40
  118. package/dist/server/request-context.d.ts.map +1 -1
  119. package/dist/server/route-element-builder.d.ts +7 -0
  120. package/dist/server/route-element-builder.d.ts.map +1 -1
  121. package/dist/server/route-matcher.d.ts +2 -2
  122. package/dist/server/route-matcher.d.ts.map +1 -1
  123. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  124. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  125. package/dist/server/slot-resolver.d.ts.map +1 -1
  126. package/dist/server/ssr-entry.d.ts.map +1 -1
  127. package/dist/server/ssr-render.d.ts +3 -0
  128. package/dist/server/ssr-render.d.ts.map +1 -1
  129. package/dist/server/tree-builder.d.ts +12 -8
  130. package/dist/server/tree-builder.d.ts.map +1 -1
  131. package/dist/server/types.d.ts +1 -3
  132. package/dist/server/types.d.ts.map +1 -1
  133. package/dist/server/version-skew.d.ts +61 -0
  134. package/dist/server/version-skew.d.ts.map +1 -0
  135. package/dist/shims/navigation-client.d.ts +1 -1
  136. package/dist/shims/navigation-client.d.ts.map +1 -1
  137. package/dist/shims/navigation.d.ts +1 -1
  138. package/dist/shims/navigation.d.ts.map +1 -1
  139. package/dist/utils/state-machine.d.ts +80 -0
  140. package/dist/utils/state-machine.d.ts.map +1 -0
  141. package/package.json +12 -8
  142. package/src/client/browser-entry.ts +55 -13
  143. package/src/client/error-boundary.tsx +18 -1
  144. package/src/client/index.ts +9 -1
  145. package/src/client/link.tsx +9 -9
  146. package/src/client/navigation-context.ts +2 -2
  147. package/src/client/router.ts +102 -55
  148. package/src/client/rsc-fetch.ts +63 -2
  149. package/src/client/segment-cache.ts +1 -1
  150. package/src/client/stale-reload.ts +28 -0
  151. package/src/client/top-loader.tsx +2 -2
  152. package/src/client/use-params.ts +3 -3
  153. package/src/client/use-query-states.ts +1 -1
  154. package/src/codec.ts +21 -0
  155. package/src/cookies/define-cookie.ts +69 -18
  156. package/src/index.ts +255 -65
  157. package/src/params/define.ts +260 -0
  158. package/src/params/index.ts +28 -0
  159. package/src/plugins/adapter-build.ts +6 -0
  160. package/src/plugins/build-manifest.ts +11 -0
  161. package/src/plugins/client-chunks.ts +65 -0
  162. package/src/plugins/entries.ts +3 -6
  163. package/src/plugins/routing.ts +40 -14
  164. package/src/plugins/server-bundle.ts +32 -1
  165. package/src/plugins/shims.ts +1 -1
  166. package/src/plugins/static-build.ts +8 -4
  167. package/src/routing/codegen.ts +109 -88
  168. package/src/routing/scanner.ts +55 -6
  169. package/src/routing/status-file-lint.ts +2 -1
  170. package/src/routing/types.ts +7 -4
  171. package/src/rsc-runtime/rsc.ts +2 -0
  172. package/src/search-params/codecs.ts +1 -1
  173. package/src/search-params/define.ts +504 -0
  174. package/src/search-params/index.ts +12 -18
  175. package/src/search-params/registry.ts +1 -1
  176. package/src/search-params/wrappers.ts +85 -0
  177. package/src/server/access-gate.tsx +38 -8
  178. package/src/server/action-encryption.ts +144 -0
  179. package/src/server/action-handler.ts +16 -0
  180. package/src/server/als-registry.ts +4 -4
  181. package/src/server/build-manifest.ts +4 -4
  182. package/src/server/early-hints.ts +36 -15
  183. package/src/server/error-boundary-wrapper.ts +57 -14
  184. package/src/server/flight-injection-state.ts +152 -0
  185. package/src/server/form-data.ts +76 -0
  186. package/src/server/html-injectors.ts +42 -26
  187. package/src/server/index.ts +2 -4
  188. package/src/server/node-stream-transforms.ts +68 -41
  189. package/src/server/pipeline.ts +98 -26
  190. package/src/server/request-context.ts +49 -124
  191. package/src/server/route-element-builder.ts +102 -99
  192. package/src/server/route-matcher.ts +2 -2
  193. package/src/server/rsc-entry/error-renderer.ts +3 -2
  194. package/src/server/rsc-entry/index.ts +26 -11
  195. package/src/server/rsc-entry/rsc-payload.ts +2 -2
  196. package/src/server/rsc-entry/ssr-renderer.ts +4 -4
  197. package/src/server/slot-resolver.ts +204 -206
  198. package/src/server/ssr-entry.ts +3 -1
  199. package/src/server/ssr-render.ts +3 -0
  200. package/src/server/tree-builder.ts +84 -48
  201. package/src/server/types.ts +1 -3
  202. package/src/server/version-skew.ts +104 -0
  203. package/src/shims/navigation-client.ts +1 -1
  204. package/src/shims/navigation.ts +1 -1
  205. package/src/utils/state-machine.ts +111 -0
  206. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  207. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  208. package/dist/_chunks/request-context-BQUC8PHn.js.map +0 -1
  209. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  210. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  211. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  212. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  213. package/dist/client/error-boundary.js.map +0 -1
  214. package/dist/cookies/index.js.map +0 -1
  215. package/dist/plugins/dynamic-transform.d.ts +0 -72
  216. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  217. package/dist/search-params/analyze.d.ts +0 -54
  218. package/dist/search-params/analyze.d.ts.map +0 -1
  219. package/dist/search-params/builtin-codecs.d.ts +0 -105
  220. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  221. package/dist/search-params/create.d.ts +0 -106
  222. package/dist/search-params/create.d.ts.map +0 -1
  223. package/dist/search-params/index.js.map +0 -1
  224. package/dist/server/prerender.d.ts +0 -77
  225. package/dist/server/prerender.d.ts.map +0 -1
  226. package/src/plugins/dynamic-transform.ts +0 -161
  227. package/src/search-params/analyze.ts +0 -192
  228. package/src/search-params/builtin-codecs.ts +0 -228
  229. package/src/search-params/create.ts +0 -321
  230. package/src/server/prerender.ts +0 -139
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Shared state machine types and transitions for RSC Flight injection.
3
+ *
4
+ * Both html-injectors.ts (Web Streams) and node-stream-transforms.ts
5
+ * (Node.js streams) implement identical state transitions — only the
6
+ * I/O primitives differ. This module defines the discriminated union
7
+ * states and the transition map once.
8
+ *
9
+ * Valid state flow:
10
+ * init → streaming → body-level → flushing → done
11
+ * └──────────────→ flushing → done
12
+ * (any) → error
13
+ *
14
+ * Design doc: 02-rendering-pipeline.md
15
+ */
16
+
17
+ import type { TransitionMap } from '../utils/state-machine.js';
18
+
19
+ // ─── States ──────────────────────────────────────────────────────────────────
20
+
21
+ /** Waiting for first HTML chunk. Pull loop not started. */
22
+ export interface InitState {
23
+ phase: 'init';
24
+ }
25
+
26
+ /** HTML chunks flowing, pull loop running, suffix not yet stripped. */
27
+ export interface StreamingState {
28
+ phase: 'streaming';
29
+ }
30
+
31
+ /**
32
+ * Suffix (</body></html>) stripped. RSC scripts injected at body level.
33
+ * HTML may still be streaming (Suspense chunks after suffix).
34
+ */
35
+ export interface BodyLevelState {
36
+ phase: 'body-level';
37
+ }
38
+
39
+ /** HTML stream done (flush fired). Draining remaining RSC chunks. */
40
+ export interface FlushingState {
41
+ phase: 'flushing';
42
+ /** When true, suffix was found before flushing. */
43
+ hadSuffix: boolean;
44
+ }
45
+
46
+ /** All streams consumed. Terminal state. */
47
+ export interface DoneState {
48
+ phase: 'done';
49
+ hadSuffix: boolean;
50
+ }
51
+
52
+ /** Pull loop failed. Terminal state with captured error. */
53
+ export interface ErrorState {
54
+ phase: 'error';
55
+ error: unknown;
56
+ }
57
+
58
+ export type FlightInjectionState =
59
+ | InitState
60
+ | StreamingState
61
+ | BodyLevelState
62
+ | FlushingState
63
+ | DoneState
64
+ | ErrorState;
65
+
66
+ // ─── Events ──────────────────────────────────────────────────────────────────
67
+
68
+ /** First HTML chunk arrived — start the pull loop. */
69
+ export interface FirstChunkEvent {
70
+ type: 'FIRST_CHUNK';
71
+ }
72
+
73
+ /** The </body></html> suffix was found and stripped. */
74
+ export interface SuffixFoundEvent {
75
+ type: 'SUFFIX_FOUND';
76
+ }
77
+
78
+ /** HTML stream finished (flush/end). */
79
+ export interface HtmlDoneEvent {
80
+ type: 'HTML_DONE';
81
+ }
82
+
83
+ /** RSC pull loop completed (all chunks read or stream closed). */
84
+ export interface PullDoneEvent {
85
+ type: 'PULL_DONE';
86
+ }
87
+
88
+ /** RSC pull loop encountered an error. */
89
+ export interface PullErrorEvent {
90
+ type: 'PULL_ERROR';
91
+ error: unknown;
92
+ }
93
+
94
+ export type FlightInjectionEvent =
95
+ | FirstChunkEvent
96
+ | SuffixFoundEvent
97
+ | HtmlDoneEvent
98
+ | PullDoneEvent
99
+ | PullErrorEvent;
100
+
101
+ // ─── Transitions ─────────────────────────────────────────────────────────────
102
+
103
+ export const flightInjectionTransitions: TransitionMap<FlightInjectionState, FlightInjectionEvent> =
104
+ {
105
+ 'init': {
106
+ FIRST_CHUNK: (): StreamingState => ({ phase: 'streaming' }),
107
+ // Edge case: HTML stream ends immediately (empty body)
108
+ HTML_DONE: (): FlushingState => ({ phase: 'flushing', hadSuffix: false }),
109
+ },
110
+ 'streaming': {
111
+ SUFFIX_FOUND: (): BodyLevelState => ({ phase: 'body-level' }),
112
+ HTML_DONE: (): FlushingState => ({ phase: 'flushing', hadSuffix: false }),
113
+ PULL_DONE: (): StreamingState => ({ phase: 'streaming' }),
114
+ PULL_ERROR: (_s, e): ErrorState => ({ phase: 'error', error: e.error }),
115
+ },
116
+ 'body-level': {
117
+ HTML_DONE: (): FlushingState => ({ phase: 'flushing', hadSuffix: true }),
118
+ PULL_DONE: (): BodyLevelState => ({ phase: 'body-level' }),
119
+ PULL_ERROR: (_s, e): ErrorState => ({ phase: 'error', error: e.error }),
120
+ },
121
+ 'flushing': {
122
+ PULL_DONE: (s): DoneState => ({ phase: 'done', hadSuffix: s.hadSuffix }),
123
+ PULL_ERROR: (_s, e): ErrorState => ({ phase: 'error', error: e.error }),
124
+ // Suffix can be found during flushing if flush() processes remaining text
125
+ SUFFIX_FOUND: (_s): FlushingState => ({ phase: 'flushing', hadSuffix: true }),
126
+ },
127
+ };
128
+
129
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
130
+
131
+ /** Whether the machine is in a state where the suffix has been stripped. */
132
+ export function isSuffixStripped(state: FlightInjectionState): boolean {
133
+ switch (state.phase) {
134
+ case 'body-level':
135
+ return true;
136
+ case 'flushing':
137
+ case 'done':
138
+ return state.hadSuffix;
139
+ default:
140
+ return false;
141
+ }
142
+ }
143
+
144
+ /** Whether the HTML stream has finished (flush/end fired). */
145
+ export function isHtmlDone(state: FlightInjectionState): boolean {
146
+ return state.phase === 'flushing' || state.phase === 'done';
147
+ }
148
+
149
+ /** Whether the RSC pull loop has completed. */
150
+ export function isPullDone(state: FlightInjectionState): boolean {
151
+ return state.phase === 'done' || state.phase === 'error';
152
+ }
@@ -173,4 +173,80 @@ export const coerce = {
173
173
  return undefined;
174
174
  }
175
175
  },
176
+
177
+ /**
178
+ * Coerce a date string to a Date object.
179
+ * Handles `<input type="date">` (`"2024-01-15"`), `<input type="datetime-local">`
180
+ * (`"2024-01-15T10:30"`), and full ISO 8601 strings.
181
+ * - Valid date string → `Date`
182
+ * - `""` / `undefined` / `null` → `undefined`
183
+ * - Invalid date strings → `undefined` (schema validation will catch this)
184
+ * - Impossible dates that `new Date()` silently normalizes (e.g. Feb 31) → `undefined`
185
+ */
186
+ date(value: unknown): Date | undefined {
187
+ if (value === undefined || value === null || value === '') return undefined;
188
+ if (value instanceof Date) return value;
189
+ if (typeof value !== 'string') return undefined;
190
+ const date = new Date(value);
191
+ if (Number.isNaN(date.getTime())) return undefined;
192
+
193
+ // Overflow detection: extract Y/M/D from the input string and verify
194
+ // they match the parsed Date components. new Date('2024-02-31') silently
195
+ // normalizes to March 2nd — we reject such inputs.
196
+ const ymdMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})/);
197
+ if (ymdMatch) {
198
+ const inputYear = Number(ymdMatch[1]);
199
+ const inputMonth = Number(ymdMatch[2]);
200
+ const inputDay = Number(ymdMatch[3]);
201
+
202
+ // Use UTC methods for date-only and Z-suffixed strings to avoid
203
+ // timezone offset shifting the day. For datetime-local (no Z suffix),
204
+ // the Date constructor parses in local time, so use local methods.
205
+ const isUTC = value.length === 10 || value.endsWith('Z');
206
+ const parsedYear = isUTC ? date.getUTCFullYear() : date.getFullYear();
207
+ const parsedMonth = isUTC ? date.getUTCMonth() + 1 : date.getMonth() + 1;
208
+ const parsedDay = isUTC ? date.getUTCDate() : date.getDate();
209
+
210
+ if (inputYear !== parsedYear || inputMonth !== parsedMonth || inputDay !== parsedDay) {
211
+ return undefined;
212
+ }
213
+ }
214
+
215
+ return date;
216
+ },
217
+
218
+ /**
219
+ * Create a File coercion function with optional size and mime type validation.
220
+ * Returns the File if valid, `undefined` otherwise.
221
+ *
222
+ * ```ts
223
+ * // Basic — just checks it's a real File
224
+ * z.preprocess(coerce.file(), z.instanceof(File))
225
+ *
226
+ * // With constraints
227
+ * z.preprocess(
228
+ * coerce.file({ maxSize: 5 * 1024 * 1024, accept: ['image/png', 'image/jpeg'] }),
229
+ * z.instanceof(File)
230
+ * )
231
+ * ```
232
+ */
233
+ file(options?: { maxSize?: number; accept?: string[] }): (value: unknown) => File | undefined {
234
+ return (value: unknown): File | undefined => {
235
+ if (value === undefined || value === null || value === '') return undefined;
236
+ if (!(value instanceof File)) return undefined;
237
+
238
+ // Empty file input (no selection): browsers submit File with name="" and size=0
239
+ if (value.size === 0 && value.name === '') return undefined;
240
+
241
+ if (options?.maxSize !== undefined && value.size > options.maxSize) {
242
+ return undefined;
243
+ }
244
+
245
+ if (options?.accept !== undefined && !options.accept.includes(value.type)) {
246
+ return undefined;
247
+ }
248
+
249
+ return value;
250
+ };
251
+ },
176
252
  };
@@ -7,6 +7,16 @@
7
7
  * Design docs: 02-rendering-pipeline.md, 18-build-system.md §"Entry Files"
8
8
  */
9
9
 
10
+ import { createMachine } from '../utils/state-machine.js';
11
+ import {
12
+ flightInjectionTransitions,
13
+ isSuffixStripped,
14
+ isHtmlDone,
15
+ isPullDone,
16
+ type FlightInjectionState,
17
+ type FlightInjectionEvent,
18
+ } from './flight-injection-state.js';
19
+
10
20
  /**
11
21
  * Inject HTML content before a closing tag in the stream.
12
22
  *
@@ -179,6 +189,11 @@ export function createInlinedRscStream(
179
189
  * scanning or depth tracking needed — the suffix removal is the
180
190
  * structural guarantee.
181
191
  *
192
+ * State machine phases:
193
+ * init → streaming → body-level → flushing → done
194
+ * └──────────────→ flushing → done
195
+ * (any) → error
196
+ *
182
197
  * Inspired by Next.js createFlightDataInjectionTransformStream.
183
198
  */
184
199
  function createFlightInjectionTransform(
@@ -190,17 +205,13 @@ function createFlightInjectionTransform(
190
205
  const suffixBytes = encoder.encode(suffix);
191
206
 
192
207
  const rscReader = rscScriptStream.getReader();
208
+
209
+ const machine = createMachine<FlightInjectionState, FlightInjectionEvent>({
210
+ initial: { phase: 'init' },
211
+ transitions: flightInjectionTransitions,
212
+ });
213
+
193
214
  let pullPromise: Promise<void> | null = null;
194
- let donePulling = false;
195
- let pullError: unknown = null;
196
- // Once the suffix is stripped, all content is body-level and
197
- // scripts can safely be drained after any HTML chunk.
198
- let foundSuffix = false;
199
- // Set to true in flush() — once all HTML chunks have been emitted,
200
- // there's no need to yield between RSC reads. This eliminates
201
- // ~36 macrotask yields per request (18 chunks × 2 yields each)
202
- // that were the primary source of SSR overhead vs Next.js.
203
- let htmlStreamFinished = false;
204
215
 
205
216
  // RSC script chunks waiting to be injected at the body level.
206
217
  const pending: Uint8Array[] = [];
@@ -220,7 +231,7 @@ function createFlightInjectionTransform(
220
231
  for (;;) {
221
232
  const { done, value } = await rscReader.read();
222
233
  if (done) {
223
- donePulling = true;
234
+ machine.send({ type: 'PULL_DONE' });
224
235
  return;
225
236
  }
226
237
  pending.push(value);
@@ -228,13 +239,12 @@ function createFlightInjectionTransform(
228
239
  // through transform() first — but only while HTML is still
229
240
  // streaming. Once flush() fires (all HTML emitted), drain
230
241
  // remaining RSC chunks without yielding.
231
- if (!htmlStreamFinished) {
242
+ if (!isHtmlDone(machine.state)) {
232
243
  await new Promise<void>((r) => setImmediate(r));
233
244
  }
234
245
  }
235
246
  } catch (err) {
236
- pullError = err;
237
- donePulling = true;
247
+ machine.send({ type: 'PULL_ERROR', error: err });
238
248
  }
239
249
  }
240
250
 
@@ -243,10 +253,8 @@ function createFlightInjectionTransform(
243
253
  while (pending.length > 0) {
244
254
  controller.enqueue(pending.shift()!);
245
255
  }
246
- if (pullError) {
247
- const err = pullError;
248
- pullError = null;
249
- controller.error(err);
256
+ if (machine.state.phase === 'error') {
257
+ controller.error(machine.state.error);
250
258
  }
251
259
  }
252
260
 
@@ -257,11 +265,12 @@ function createFlightInjectionTransform(
257
265
  // and ensures the shell HTML is enqueued before any RSC
258
266
  // script tags. Without this, the pull loop starts eagerly
259
267
  // and may read RSC data before the browser has any HTML.
260
- if (!pullPromise) {
268
+ if (machine.state.phase === 'init') {
269
+ machine.send({ type: 'FIRST_CHUNK' });
261
270
  pullPromise = pullLoop();
262
271
  }
263
272
 
264
- if (foundSuffix) {
273
+ if (isSuffixStripped(machine.state)) {
265
274
  // Post-suffix: everything is body-level (Suspense chunks).
266
275
  // Emit HTML, then drain any buffered scripts.
267
276
  controller.enqueue(chunk);
@@ -273,7 +282,7 @@ function createFlightInjectionTransform(
273
282
  const text = decoder.decode(chunk, { stream: true });
274
283
  const idx = text.indexOf(suffix);
275
284
  if (idx !== -1) {
276
- foundSuffix = true;
285
+ machine.send({ type: 'SUFFIX_FOUND' });
277
286
  // Emit everything before the suffix (still inside <body>'s
278
287
  // child elements — don't inject scripts here).
279
288
  const before = text.slice(0, idx);
@@ -290,20 +299,27 @@ function createFlightInjectionTransform(
290
299
  }
291
300
  },
292
301
  flush(controller) {
293
- // All HTML chunks have been emitted. Signal the pull loop to
294
- // stop yielding between RSC reads — no more HTML to interleave.
295
- htmlStreamFinished = true;
302
+ // All HTML chunks have been emitted. Transition to flushing
303
+ // the pull loop will stop yielding between RSC reads since
304
+ // isHtmlDone() now returns true. This eliminates ~36 macrotask
305
+ // yields per request (18 chunks × 2 yields each) that were the
306
+ // primary source of SSR overhead vs Next.js.
307
+ machine.send({ type: 'HTML_DONE' });
296
308
 
297
309
  // Drain remaining RSC chunks at body level
298
310
  const finish = () => {
299
311
  drainPending(controller);
300
312
  // Re-emit the suffix at the very end so HTML is well-formed
301
- if (foundSuffix) {
313
+ if (machine.state.phase === 'done' && machine.state.hadSuffix) {
314
+ controller.enqueue(suffixBytes);
315
+ } else if (machine.state.phase === 'flushing' && machine.state.hadSuffix) {
316
+ // Pull was already done before flush — drainPending didn't
317
+ // transition, but we still need the suffix
302
318
  controller.enqueue(suffixBytes);
303
319
  }
304
320
  };
305
321
 
306
- if (donePulling) {
322
+ if (isPullDone(machine.state)) {
307
323
  finish();
308
324
  return;
309
325
  }
@@ -6,19 +6,17 @@ export type { MiddlewareContext } from './types';
6
6
  export type { RouteContext } from './types';
7
7
  export type { Metadata, MetadataRoute } from './types';
8
8
 
9
- // Request Context — ALS-backed headers(), cookies(), and searchParams()
9
+ // Request Context — ALS-backed headers(), cookies(), and rawSearchParams()
10
10
  // Design doc: design/04-authorization.md §"AccessContext does not include cookies or headers"
11
11
  // Design doc: design/23-search-params.md §"Server Integration"
12
12
  export {
13
13
  headers,
14
14
  cookies,
15
- searchParams,
16
- setParsedSearchParams,
15
+ rawSearchParams,
17
16
  runWithRequestContext,
18
17
  setMutableCookieContext,
19
18
  markResponseFlushed,
20
19
  getSetCookieHeaders,
21
- setCookieSecrets,
22
20
  } from './request-context';
23
21
  export type { ReadonlyHeaders, RequestCookies, CookieOptions } from './request-context';
24
22
 
@@ -20,6 +20,16 @@
20
20
  import { Transform } from 'node:stream';
21
21
  import { createGzip, constants } from 'node:zlib';
22
22
 
23
+ import { createMachine } from '../utils/state-machine.js';
24
+ import {
25
+ flightInjectionTransitions,
26
+ isSuffixStripped,
27
+ isHtmlDone,
28
+ isPullDone,
29
+ type FlightInjectionState,
30
+ type FlightInjectionEvent,
31
+ } from './flight-injection-state.js';
32
+
23
33
  // ─── Head Injection ──────────────────────────────────────────────────────────
24
34
 
25
35
  /**
@@ -124,29 +134,26 @@ export function createNodeFlightInjector(
124
134
  const rscReader = rscStream.getReader();
125
135
  const decoder = new TextDecoder('utf-8', { fatal: true });
126
136
 
127
- let donePulling = false;
128
- let pullError: unknown = null;
129
- let foundSuffix = false;
130
- let htmlStreamFinished = false;
131
- // Reference to the transform instance, set on first transform() call.
132
- // Used by pullLoop to push RSC chunks directly to the output without
133
- // waiting for HTML chunks to trigger drainPending.
134
- let transformRef: Transform | null = null;
137
+ const machine = createMachine<FlightInjectionState, FlightInjectionEvent>({
138
+ initial: { phase: 'init' },
139
+ transitions: flightInjectionTransitions,
140
+ });
135
141
 
136
142
  // pullLoop reads RSC chunks and pushes them directly to the transform
137
143
  // output as <script> tags. This ensures RSC data is delivered to the
138
144
  // browser as soon as it's available — not deferred until the next HTML
139
145
  // chunk. Critical for streaming: the shell RSC payload must arrive
140
146
  // with the shell HTML so hydration can start before Suspense resolves.
141
- async function pullLoop(): Promise<void> {
142
- // Yield once so the first transform() call can set transformRef and
143
- // emit the bootstrap signal before we start pushing data chunks.
147
+
148
+ async function pullLoop(stream: Transform): Promise<void> {
149
+ // Yield once so the first transform() call can emit the bootstrap
150
+ // signal before we start pushing data chunks.
144
151
  await new Promise<void>((r) => setImmediate(r));
145
152
  try {
146
153
  for (;;) {
147
154
  const { done, value } = await rscReader.read();
148
155
  if (done) {
149
- donePulling = true;
156
+ machine.send({ type: 'PULL_DONE' });
150
157
  return;
151
158
  }
152
159
  const decoded = decoder.decode(value, { stream: true });
@@ -154,34 +161,37 @@ export function createNodeFlightInjector(
154
161
  const scriptBuf = Buffer.from(`<script>self.__timber_f.push(${escaped})</script>`, 'utf-8');
155
162
  // Push directly to the transform output — don't wait for an
156
163
  // HTML chunk to trigger drainPending.
157
- if (transformRef) {
158
- transformRef.push(scriptBuf);
159
- }
160
- if (!htmlStreamFinished) {
164
+ stream.push(scriptBuf);
165
+ // Yield between reads so HTML chunks get a chance to flow
166
+ // through transform() first — but only while HTML is still
167
+ // streaming. Once flush() fires (all HTML emitted), drain
168
+ // remaining RSC chunks without yielding.
169
+ if (!isHtmlDone(machine.state)) {
161
170
  await new Promise<void>((r) => setImmediate(r));
162
171
  }
163
172
  }
164
173
  } catch (err) {
165
- pullError = err;
166
- donePulling = true;
174
+ machine.send({ type: 'PULL_ERROR', error: err });
167
175
  }
168
176
  }
169
177
 
170
- return new Transform({
178
+ // Bootstrap script to emit after the first HTML chunk (but before
179
+ // any RSC data chunks). Must come AFTER the doctype + <html> so
180
+ // browsers don't enter Quirks Mode.
181
+ const bootstrapBuf = Buffer.from(
182
+ `<script>(self.__timber_f=self.__timber_f||[]).push(${htmlEscapeJsonString(JSON.stringify([0]))})</script>`,
183
+ 'utf-8'
184
+ );
185
+
186
+ const transform = new Transform({
171
187
  transform(chunk: Buffer, _encoding, callback) {
172
- if (!transformRef) {
173
- transformRef = this;
174
- // Emit bootstrap signal with the first HTML chunk (the shell).
175
- // This must arrive before any data chunks.
176
- const bootstrap = `<script>(self.__timber_f=self.__timber_f||[]).push(${htmlEscapeJsonString(JSON.stringify([0]))})</script>`;
177
- this.push(Buffer.from(bootstrap, 'utf-8'));
178
- // Start the pull loop — it will push RSC data chunks directly
179
- // to the transform output as they arrive from the RSC stream.
180
- pullLoop();
188
+ const isFirst = machine.state.phase === 'init';
189
+ if (isFirst) {
190
+ machine.send({ type: 'FIRST_CHUNK' });
181
191
  }
182
192
 
183
- if (foundSuffix) {
184
- this.push(chunk);
193
+ if (isSuffixStripped(machine.state)) {
194
+ transform.push(chunk);
185
195
  callback();
186
196
  return;
187
197
  }
@@ -189,38 +199,53 @@ export function createNodeFlightInjector(
189
199
  const text = chunk.toString('utf-8');
190
200
  const idx = text.indexOf(suffix);
191
201
  if (idx !== -1) {
192
- foundSuffix = true;
202
+ machine.send({ type: 'SUFFIX_FOUND' });
193
203
  const before = text.slice(0, idx);
194
204
  const after = text.slice(idx + suffix.length);
195
- if (before) this.push(Buffer.from(before, 'utf-8'));
196
- if (after) this.push(Buffer.from(after, 'utf-8'));
205
+ if (before) transform.push(Buffer.from(before, 'utf-8'));
206
+ if (after) transform.push(Buffer.from(after, 'utf-8'));
197
207
  } else {
198
- this.push(chunk);
208
+ transform.push(chunk);
209
+ }
210
+
211
+ // Emit bootstrap AFTER the first HTML chunk so the doctype and
212
+ // <html> tag are the first bytes the browser sees. Then start
213
+ // the pull loop to stream RSC data chunks.
214
+ if (isFirst) {
215
+ transform.push(bootstrapBuf);
216
+ pullLoop(transform);
199
217
  }
200
218
  callback();
201
219
  },
202
220
  flush(callback) {
203
- htmlStreamFinished = true;
221
+ // All HTML chunks have been emitted. Transition to flushing —
222
+ // the pull loop will stop yielding between RSC reads since
223
+ // isHtmlDone() now returns true.
224
+ machine.send({ type: 'HTML_DONE' });
204
225
 
205
226
  const finish = () => {
206
- if (pullError) {
207
- this.destroy(pullError instanceof Error ? pullError : new Error(String(pullError)));
227
+ if (machine.state.phase === 'error') {
228
+ const err = machine.state.error;
229
+ transform.destroy(err instanceof Error ? err : new Error(String(err)));
208
230
  return;
209
231
  }
210
- if (foundSuffix) {
211
- this.push(suffixBuf);
232
+ const hadSuffix =
233
+ (machine.state.phase === 'done' && machine.state.hadSuffix) ||
234
+ (machine.state.phase === 'flushing' && machine.state.hadSuffix);
235
+ if (hadSuffix) {
236
+ transform.push(suffixBuf);
212
237
  }
213
238
  callback();
214
239
  };
215
240
 
216
- if (donePulling) {
241
+ if (isPullDone(machine.state)) {
217
242
  finish();
218
243
  return;
219
244
  }
220
245
  // Wait for the RSC stream to finish before closing.
221
246
  // pullLoop is already running and pushing directly.
222
247
  const waitForPull = () => {
223
- if (donePulling || pullError) {
248
+ if (isPullDone(machine.state)) {
224
249
  finish();
225
250
  } else {
226
251
  setImmediate(waitForPull);
@@ -229,6 +254,8 @@ export function createNodeFlightInjector(
229
254
  waitForPull();
230
255
  },
231
256
  });
257
+
258
+ return transform;
232
259
  }
233
260
 
234
261
  // ─── Error Handling ──────────────────────────────────────────────────────────