@timber-js/app 0.2.0-alpha.5 → 0.2.0-alpha.51
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-gwlJkDuf.js → debug-ECi_61pb.js} +2 -2
- package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
- package/dist/_chunks/define-TK8C1M3x.js +279 -0
- package/dist/_chunks/define-TK8C1M3x.js.map +1 -0
- package/dist/_chunks/define-cookie-k9btcEfI.js +93 -0
- package/dist/_chunks/define-cookie-k9btcEfI.js.map +1 -0
- package/dist/_chunks/error-boundary-B9vT_YK_.js +211 -0
- package/dist/_chunks/error-boundary-B9vT_YK_.js.map +1 -0
- package/dist/_chunks/{format-DviM89f0.js → format-cX7wzEp2.js} +2 -2
- package/dist/_chunks/{format-DviM89f0.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-DIkVh_jG.js → request-context-0h-6Voad.js} +95 -69
- package/dist/_chunks/request-context-0h-6Voad.js.map +1 -0
- package/dist/_chunks/segment-context-DBn-nrMN.js +69 -0
- package/dist/_chunks/segment-context-DBn-nrMN.js.map +1 -0
- package/dist/_chunks/stale-reload-4L-_skC7.js +47 -0
- package/dist/_chunks/stale-reload-4L-_skC7.js.map +1 -0
- package/dist/_chunks/{tracing-Cwn7697K.js → tracing-JI4cYUdz.js} +17 -3
- package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-JI4cYUdz.js.map} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-wEXY2JQB.js} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-wEXY2JQB.js.map} +1 -1
- package/dist/_chunks/wrappers-C9XPg7-U.js +63 -0
- package/dist/_chunks/wrappers-C9XPg7-U.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 +90 -20
- 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 +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/index.d.ts +112 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +467 -246
- 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 +105 -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.map +1 -1
- package/dist/plugins/fonts.d.ts +7 -0
- package/dist/plugins/fonts.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 +159 -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 +4 -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/build-manifest.d.ts.map +1 -1
- package/dist/server/debug.d.ts +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 +66 -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 +51 -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 +1974 -1648
- 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 +113 -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/ssr-wrappers.d.ts +50 -0
- package/dist/server/ssr-wrappers.d.ts.map +1 -0
- 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 +40 -29
- package/src/cli.ts +0 -0
- package/src/client/browser-entry.ts +133 -93
- 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/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 +5 -7
- package/src/plugins/fonts.ts +93 -42
- 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 +518 -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 +7 -1
- 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 +16 -5
- package/src/server/compress.ts +25 -7
- package/src/server/debug.ts +1 -1
- 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 +113 -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 +261 -117
- package/src/server/index.ts +9 -4
- package/src/server/logger.ts +38 -35
- package/src/server/node-stream-transforms.ts +504 -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 +119 -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 +108 -43
- package/src/server/rsc-entry/rsc-payload.ts +52 -12
- package/src/server/rsc-entry/rsc-stream.ts +49 -12
- package/src/server/rsc-entry/ssr-renderer.ts +40 -13
- package/src/server/slot-resolver.ts +222 -217
- package/src/server/ssr-entry.ts +209 -30
- package/src/server/ssr-render.ts +289 -67
- package/src/server/ssr-wrappers.tsx +139 -0
- 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-gwlJkDuf.js.map +0 -1
- package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
- package/dist/_chunks/request-context-DIkVh_jG.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
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js native stream transforms for SSR HTML post-processing.
|
|
3
|
+
*
|
|
4
|
+
* These are Node.js Transform stream equivalents of the Web Stream
|
|
5
|
+
* transforms in html-injectors.ts. Used on Node.js/Bun where native
|
|
6
|
+
* streams (C++ backed) are faster than Web Streams (JS reimplementation).
|
|
7
|
+
*
|
|
8
|
+
* The transforms are pure string operations on HTML chunks — the same
|
|
9
|
+
* logic as the Web Stream versions, just wrapped in Node.js Transform
|
|
10
|
+
* instead of Web TransformStream.
|
|
11
|
+
*
|
|
12
|
+
* Architecture:
|
|
13
|
+
* renderToPipeableStream → pipe(errorHandler) → pipe(headInjector)
|
|
14
|
+
* → pipe(flightInjector) → Readable.toWeb() → Response
|
|
15
|
+
*
|
|
16
|
+
* All chunks stay in C++ Node.js stream buffers until the final
|
|
17
|
+
* Readable.toWeb() conversion for the Response body.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Transform } from 'node:stream';
|
|
21
|
+
import { createGzip, constants } from 'node:zlib';
|
|
22
|
+
|
|
23
|
+
// ─── Buffered Transform ──────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for the Node.js buffered transform.
|
|
27
|
+
*/
|
|
28
|
+
export interface NodeBufferedTransformOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Flush synchronously once the buffer reaches this many bytes.
|
|
31
|
+
* Prevents unbounded memory growth for very large Fizz flushes.
|
|
32
|
+
* Default: Infinity (no byte limit — flush only on tick boundary).
|
|
33
|
+
*/
|
|
34
|
+
readonly maxBufferByteLength?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Node.js Transform that buffers incoming chunks and coalesces them
|
|
39
|
+
* within a single event loop tick.
|
|
40
|
+
*
|
|
41
|
+
* Equivalent to createBufferedTransformStream() in html-injectors.ts.
|
|
42
|
+
* React Fizz may emit multiple micro-chunks within a single flush.
|
|
43
|
+
* Without buffering, downstream transforms (especially flight injection)
|
|
44
|
+
* could see chunk boundaries in the middle of HTML tags or attributes.
|
|
45
|
+
*
|
|
46
|
+
* This transform collects all chunks that arrive in the same tick and
|
|
47
|
+
* emits them as a single concatenated Buffer on the next `setImmediate`.
|
|
48
|
+
*
|
|
49
|
+
* **Not a polling loop.** Uses a single-shot `setImmediate` per flush
|
|
50
|
+
* cycle — no recursive scheduling, no busy-wait. See design/02 §"No Polling".
|
|
51
|
+
*
|
|
52
|
+
* Inspired by Next.js `createBufferedTransformStream`.
|
|
53
|
+
*/
|
|
54
|
+
export function createNodeBufferedTransform(options: NodeBufferedTransformOptions = {}): Transform {
|
|
55
|
+
const { maxBufferByteLength = Infinity } = options;
|
|
56
|
+
|
|
57
|
+
let bufferedChunks: Buffer[] = [];
|
|
58
|
+
let bufferByteLength = 0;
|
|
59
|
+
let pendingImmediate: ReturnType<typeof setImmediate> | null = null;
|
|
60
|
+
|
|
61
|
+
const transform = new Transform({
|
|
62
|
+
transform(chunk: Buffer, _encoding, callback) {
|
|
63
|
+
bufferedChunks.push(chunk);
|
|
64
|
+
bufferByteLength += chunk.byteLength;
|
|
65
|
+
|
|
66
|
+
if (bufferByteLength >= maxBufferByteLength) {
|
|
67
|
+
// Synchronous flush — buffer is too large to hold
|
|
68
|
+
flushBuffer();
|
|
69
|
+
} else if (!pendingImmediate) {
|
|
70
|
+
// Schedule a deferred flush for end of this tick.
|
|
71
|
+
// Single-shot setImmediate — NOT a recursive loop.
|
|
72
|
+
// See design/02 §"No Polling".
|
|
73
|
+
pendingImmediate = setImmediate(() => {
|
|
74
|
+
pendingImmediate = null;
|
|
75
|
+
flushBuffer();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
callback();
|
|
80
|
+
},
|
|
81
|
+
flush(callback) {
|
|
82
|
+
// Cancel any pending deferred flush and flush synchronously
|
|
83
|
+
if (pendingImmediate) {
|
|
84
|
+
clearImmediate(pendingImmediate);
|
|
85
|
+
pendingImmediate = null;
|
|
86
|
+
}
|
|
87
|
+
flushBuffer();
|
|
88
|
+
callback();
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function flushBuffer() {
|
|
93
|
+
if (bufferedChunks.length === 0) return;
|
|
94
|
+
|
|
95
|
+
const merged = Buffer.concat(bufferedChunks, bufferByteLength);
|
|
96
|
+
bufferedChunks = [];
|
|
97
|
+
bufferByteLength = 0;
|
|
98
|
+
transform.push(merged);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return transform;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Injection Transforms ────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
import { createMachine } from '../utils/state-machine.js';
|
|
107
|
+
import { flightChunkScript } from './flight-scripts.js';
|
|
108
|
+
import {
|
|
109
|
+
flightInjectionTransitions,
|
|
110
|
+
isHtmlDone,
|
|
111
|
+
isPullDone,
|
|
112
|
+
type FlightInjectionState,
|
|
113
|
+
type FlightInjectionEvent,
|
|
114
|
+
} from './flight-injection-state.js';
|
|
115
|
+
import { withTimeout, RenderTimeoutError } from './render-timeout.js';
|
|
116
|
+
import { logStreamingError } from './logger.js';
|
|
117
|
+
|
|
118
|
+
// ─── Move Suffix Transform ───────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
const SUFFIX = '</body></html>';
|
|
121
|
+
const SUFFIX_BUF = Buffer.from(SUFFIX, 'utf-8');
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Node.js Transform that moves `</body></html>` to the end of the stream.
|
|
125
|
+
*
|
|
126
|
+
* Equivalent to createMoveSuffixStream() in html-injectors.ts.
|
|
127
|
+
* Strips the suffix when first encountered and re-emits it in flush().
|
|
128
|
+
* If no suffix is found, it's appended anyway for well-formed HTML.
|
|
129
|
+
*/
|
|
130
|
+
export function createNodeMoveSuffixTransform(): Transform {
|
|
131
|
+
let foundSuffix = false;
|
|
132
|
+
|
|
133
|
+
return new Transform({
|
|
134
|
+
transform(chunk: Buffer, _encoding, callback) {
|
|
135
|
+
if (foundSuffix) {
|
|
136
|
+
this.push(chunk);
|
|
137
|
+
callback();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const text = chunk.toString('utf-8');
|
|
142
|
+
const idx = text.indexOf(SUFFIX);
|
|
143
|
+
if (idx === -1) {
|
|
144
|
+
this.push(chunk);
|
|
145
|
+
callback();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
foundSuffix = true;
|
|
150
|
+
|
|
151
|
+
// If the entire chunk is exactly the suffix, skip it
|
|
152
|
+
if (chunk.byteLength === SUFFIX_BUF.byteLength) {
|
|
153
|
+
callback();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Emit content before the suffix
|
|
158
|
+
const before = text.slice(0, idx);
|
|
159
|
+
const after = text.slice(idx + SUFFIX.length);
|
|
160
|
+
if (before) this.push(Buffer.from(before, 'utf-8'));
|
|
161
|
+
if (after) this.push(Buffer.from(after, 'utf-8'));
|
|
162
|
+
callback();
|
|
163
|
+
},
|
|
164
|
+
flush(callback) {
|
|
165
|
+
// Always emit the suffix at the very end
|
|
166
|
+
this.push(SUFFIX_BUF);
|
|
167
|
+
callback();
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── Head Injection ──────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Node.js Transform that injects HTML content before </head>.
|
|
176
|
+
*
|
|
177
|
+
* Equivalent to injectHead() in html-injectors.ts. Streams chunks
|
|
178
|
+
* through immediately, keeping only a small trailing buffer to handle
|
|
179
|
+
* </head> split across chunk boundaries.
|
|
180
|
+
*/
|
|
181
|
+
export function createNodeHeadInjector(headHtml: string): Transform {
|
|
182
|
+
if (!headHtml) {
|
|
183
|
+
return new Transform({
|
|
184
|
+
transform(chunk, _enc, cb) {
|
|
185
|
+
cb(null, chunk);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const target = '</head>';
|
|
191
|
+
const tailLen = target.length - 1;
|
|
192
|
+
let injected = false;
|
|
193
|
+
let tail = '';
|
|
194
|
+
|
|
195
|
+
return new Transform({
|
|
196
|
+
transform(chunk: Buffer, _encoding, callback) {
|
|
197
|
+
if (injected) {
|
|
198
|
+
callback(null, chunk);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const text = tail + chunk.toString('utf-8');
|
|
203
|
+
const tagIndex = text.indexOf(target);
|
|
204
|
+
|
|
205
|
+
if (tagIndex !== -1) {
|
|
206
|
+
const before = text.slice(0, tagIndex);
|
|
207
|
+
const after = text.slice(tagIndex);
|
|
208
|
+
this.push(Buffer.from(before + headHtml + after, 'utf-8'));
|
|
209
|
+
injected = true;
|
|
210
|
+
tail = '';
|
|
211
|
+
callback();
|
|
212
|
+
} else {
|
|
213
|
+
const safeEnd = Math.max(0, text.length - tailLen);
|
|
214
|
+
if (safeEnd > 0) {
|
|
215
|
+
this.push(Buffer.from(text.slice(0, safeEnd), 'utf-8'));
|
|
216
|
+
}
|
|
217
|
+
tail = text.slice(safeEnd);
|
|
218
|
+
callback();
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
flush(callback) {
|
|
222
|
+
if (!injected && tail) {
|
|
223
|
+
this.push(Buffer.from(tail, 'utf-8'));
|
|
224
|
+
}
|
|
225
|
+
callback();
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── RSC Flight Injection ────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Node.js Transform that merges RSC script tags into the HTML stream.
|
|
234
|
+
*
|
|
235
|
+
* Reads RSC chunks from the provided ReadableStream and injects them
|
|
236
|
+
* as `<script>` tags between HTML chunks. Scripts are buffered in
|
|
237
|
+
* pending[] and only drained from transform() (after a complete HTML
|
|
238
|
+
* chunk) or flush() — never pushed directly from the pull loop.
|
|
239
|
+
*
|
|
240
|
+
* Suffix stripping (</body></html>) is handled upstream by
|
|
241
|
+
* createNodeMoveSuffixTransform. This transform only interleaves
|
|
242
|
+
* RSC scripts at safe chunk boundaries.
|
|
243
|
+
*
|
|
244
|
+
* The RSC stream is a Web ReadableStream (from the tee'd RSC Flight
|
|
245
|
+
* stream). We read from it using the Web API — this is the one bridge
|
|
246
|
+
* point between Web Streams and Node.js streams in the pipeline.
|
|
247
|
+
*/
|
|
248
|
+
/**
|
|
249
|
+
* Options for the Node.js flight injector.
|
|
250
|
+
*/
|
|
251
|
+
export interface NodeFlightInjectorOptions {
|
|
252
|
+
/**
|
|
253
|
+
* Timeout in milliseconds for individual RSC stream reads.
|
|
254
|
+
* If a single `rscReader.read()` call does not resolve within
|
|
255
|
+
* this duration, the read is aborted and the stream errors with
|
|
256
|
+
* a RenderTimeoutError. Default: 30000 (30s).
|
|
257
|
+
*/
|
|
258
|
+
renderTimeoutMs?: number;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function createNodeFlightInjector(
|
|
262
|
+
rscStream: ReadableStream<Uint8Array> | undefined,
|
|
263
|
+
options?: NodeFlightInjectorOptions
|
|
264
|
+
): Transform {
|
|
265
|
+
if (!rscStream) {
|
|
266
|
+
return new Transform({
|
|
267
|
+
transform(chunk, _enc, cb) {
|
|
268
|
+
cb(null, chunk);
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const timeoutMs = options?.renderTimeoutMs ?? 30_000;
|
|
274
|
+
const rscReader = rscStream.getReader();
|
|
275
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
276
|
+
|
|
277
|
+
const machine = createMachine<FlightInjectionState, FlightInjectionEvent>({
|
|
278
|
+
initial: { phase: 'init' },
|
|
279
|
+
transitions: flightInjectionTransitions,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Stored promise from pullLoop — awaited in flush() via .then()
|
|
283
|
+
// instead of polling. See design/02 §"No Polling".
|
|
284
|
+
let pullPromise: Promise<void> | null = null;
|
|
285
|
+
|
|
286
|
+
// RSC script chunks waiting to be drained at a safe boundary.
|
|
287
|
+
// pullLoop buffers here; transform() and flush() drain.
|
|
288
|
+
// Scripts are NEVER pushed directly from pullLoop — they are only
|
|
289
|
+
// emitted from transform() (after a complete HTML chunk) or flush().
|
|
290
|
+
// This guarantees scripts never land mid-tag. See TIM-527/TIM-529.
|
|
291
|
+
const pending: Buffer[] = [];
|
|
292
|
+
|
|
293
|
+
// pullLoop reads RSC chunks and buffers them as <script> tags in
|
|
294
|
+
// pending[]. It does NOT push directly to the transform output —
|
|
295
|
+
// that would cause scripts to interleave at arbitrary byte
|
|
296
|
+
// boundaries within HTML chunks (TIM-527). Pending scripts are
|
|
297
|
+
// drained only from transform() or flush().
|
|
298
|
+
async function pullLoop(): Promise<void> {
|
|
299
|
+
// Yield once so the first transform() call can process the shell
|
|
300
|
+
// HTML chunk before we start reading RSC data.
|
|
301
|
+
await new Promise<void>((r) => setImmediate(r));
|
|
302
|
+
try {
|
|
303
|
+
for (;;) {
|
|
304
|
+
// Guard each RSC read with a timeout so a permanently hung
|
|
305
|
+
// RSC stream eventually aborts instead of blocking forever.
|
|
306
|
+
// See design/02-rendering-pipeline.md §"Streaming Constraints".
|
|
307
|
+
const readPromise = rscReader.read();
|
|
308
|
+
const { done, value } =
|
|
309
|
+
timeoutMs > 0
|
|
310
|
+
? await withTimeout(readPromise, timeoutMs, 'RSC stream read timed out')
|
|
311
|
+
: await readPromise;
|
|
312
|
+
if (done) {
|
|
313
|
+
machine.send({ type: 'PULL_DONE' });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const decoded = decoder.decode(value, { stream: true });
|
|
317
|
+
const scriptBuf = Buffer.from(flightChunkScript(decoded), 'utf-8');
|
|
318
|
+
// Buffer the script — drained by the next transform() or flush().
|
|
319
|
+
pending.push(scriptBuf);
|
|
320
|
+
// Yield between reads so HTML chunks get a chance to flow
|
|
321
|
+
// through transform() first — but only while HTML is still
|
|
322
|
+
// streaming. Once flush() fires, drain without yielding.
|
|
323
|
+
if (!isHtmlDone(machine.state)) {
|
|
324
|
+
await new Promise<void>((r) => setImmediate(r));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} catch (err) {
|
|
328
|
+
// On timeout, cancel the RSC reader to release resources.
|
|
329
|
+
if (err instanceof RenderTimeoutError) {
|
|
330
|
+
rscReader.cancel(err).catch(() => {});
|
|
331
|
+
}
|
|
332
|
+
machine.send({ type: 'PULL_ERROR', error: err });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Drain all buffered RSC script chunks to the transform output. */
|
|
337
|
+
function drainPending(): void {
|
|
338
|
+
while (pending.length > 0) {
|
|
339
|
+
transform.push(pending.shift()!);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// No bootstrap script here — the init script is in <head> via
|
|
344
|
+
// flightInitScript() (see flight-scripts.ts). This ensures __timber_f
|
|
345
|
+
// exists before any chunk scripts execute.
|
|
346
|
+
|
|
347
|
+
const transform = new Transform({
|
|
348
|
+
transform(chunk: Buffer, _encoding, callback) {
|
|
349
|
+
const isFirst = machine.state.phase === 'init';
|
|
350
|
+
if (isFirst) {
|
|
351
|
+
machine.send({ type: 'FIRST_CHUNK' });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Emit the HTML chunk, then drain any buffered RSC scripts.
|
|
355
|
+
// Scripts always come AFTER a complete HTML chunk — never mid-tag.
|
|
356
|
+
// The buffered transform upstream (TIM-528) ensures each chunk is
|
|
357
|
+
// a coherent HTML fragment. Suffix stripping is handled upstream
|
|
358
|
+
// by createNodeMoveSuffixTransform (TIM-530).
|
|
359
|
+
transform.push(chunk);
|
|
360
|
+
drainPending();
|
|
361
|
+
|
|
362
|
+
// Start the pull loop on the first HTML chunk.
|
|
363
|
+
if (isFirst) {
|
|
364
|
+
pullPromise = pullLoop();
|
|
365
|
+
}
|
|
366
|
+
callback();
|
|
367
|
+
},
|
|
368
|
+
flush(callback) {
|
|
369
|
+
// All HTML chunks have been emitted. Transition to flushing —
|
|
370
|
+
// the pull loop will stop yielding between RSC reads since
|
|
371
|
+
// isHtmlDone() now returns true.
|
|
372
|
+
machine.send({ type: 'HTML_DONE' });
|
|
373
|
+
|
|
374
|
+
const finish = () => {
|
|
375
|
+
// Drain any remaining buffered RSC scripts
|
|
376
|
+
drainPending();
|
|
377
|
+
if (machine.state.phase === 'error') {
|
|
378
|
+
const err = machine.state.error;
|
|
379
|
+
transform.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
callback();
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
if (isPullDone(machine.state)) {
|
|
386
|
+
finish();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// Wait for the RSC pull loop promise to resolve instead of
|
|
390
|
+
// polling with setImmediate. No CPU spin, no busy-poll —
|
|
391
|
+
// just a Promise chain. See design/02 §"No Polling".
|
|
392
|
+
if (!pullPromise) {
|
|
393
|
+
pullPromise = pullLoop();
|
|
394
|
+
}
|
|
395
|
+
pullPromise.then(finish, (err) => {
|
|
396
|
+
machine.send({ type: 'PULL_ERROR', error: err });
|
|
397
|
+
finish();
|
|
398
|
+
});
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return transform;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ─── Error Handling ──────────────────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
const NOINDEX_SCRIPT =
|
|
408
|
+
'<script>document.head.appendChild(Object.assign(document.createElement("meta"),{name:"robots",content:"noindex"}))</script>';
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Node.js Transform that catches post-shell streaming errors.
|
|
412
|
+
*
|
|
413
|
+
* Equivalent to wrapStreamWithErrorHandling() in ssr-render.ts.
|
|
414
|
+
* Catches errors from React's streaming phase (deny/throw inside Suspense
|
|
415
|
+
* after the shell has flushed) and closes the stream cleanly.
|
|
416
|
+
*/
|
|
417
|
+
export function createNodeErrorHandler(signal?: AbortSignal): Transform {
|
|
418
|
+
const transform = new Transform({
|
|
419
|
+
transform(chunk, _encoding, callback) {
|
|
420
|
+
callback(null, chunk);
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
transform.on('error', (error) => {
|
|
425
|
+
const isAbort =
|
|
426
|
+
(error instanceof DOMException && error.name === 'AbortError') ||
|
|
427
|
+
(error instanceof Error && error.name === 'AbortError') ||
|
|
428
|
+
signal?.aborted;
|
|
429
|
+
|
|
430
|
+
if (isAbort) {
|
|
431
|
+
transform.end();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
logStreamingError({ error });
|
|
436
|
+
transform.push(Buffer.from(NOINDEX_SCRIPT, 'utf-8'));
|
|
437
|
+
transform.end();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
return transform;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ─── Compression ─────────────────────────────────────────────────────────────
|
|
444
|
+
|
|
445
|
+
const COMPRESSIBLE_TYPES = new Set([
|
|
446
|
+
'text/html',
|
|
447
|
+
'text/css',
|
|
448
|
+
'text/plain',
|
|
449
|
+
'text/xml',
|
|
450
|
+
'text/javascript',
|
|
451
|
+
'text/x-component',
|
|
452
|
+
'application/json',
|
|
453
|
+
'application/javascript',
|
|
454
|
+
'application/xml',
|
|
455
|
+
'application/xhtml+xml',
|
|
456
|
+
'application/rss+xml',
|
|
457
|
+
'application/atom+xml',
|
|
458
|
+
'image/svg+xml',
|
|
459
|
+
]);
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Create a Node.js gzip Transform using native node:zlib.
|
|
463
|
+
*
|
|
464
|
+
* Uses `createGzip()` which is backed by C++ zlib — significantly faster
|
|
465
|
+
* than the Web Streams `CompressionStream` API (which is a JS wrapper
|
|
466
|
+
* around the same zlib but with per-chunk Promise overhead).
|
|
467
|
+
*
|
|
468
|
+
* Returns null if the response shouldn't be compressed (wrong content type,
|
|
469
|
+
* client doesn't accept gzip, already encoded, etc.).
|
|
470
|
+
*/
|
|
471
|
+
export function createNodeGzipCompressor(
|
|
472
|
+
requestHeaders: Headers,
|
|
473
|
+
responseHeaders: Headers
|
|
474
|
+
): Transform | null {
|
|
475
|
+
// Check Accept-Encoding
|
|
476
|
+
const acceptEncoding = requestHeaders.get('accept-encoding') || '';
|
|
477
|
+
if (!acceptEncoding.includes('gzip')) return null;
|
|
478
|
+
|
|
479
|
+
// Check content type is compressible
|
|
480
|
+
const contentType = responseHeaders.get('content-type') || '';
|
|
481
|
+
const mimeType = contentType.split(';')[0].trim().toLowerCase();
|
|
482
|
+
if (!COMPRESSIBLE_TYPES.has(mimeType)) return null;
|
|
483
|
+
|
|
484
|
+
// Don't double-compress
|
|
485
|
+
if (responseHeaders.has('content-encoding')) return null;
|
|
486
|
+
|
|
487
|
+
// Set response headers for gzip
|
|
488
|
+
responseHeaders.set('content-encoding', 'gzip');
|
|
489
|
+
responseHeaders.delete('content-length');
|
|
490
|
+
const existingVary = responseHeaders.get('vary');
|
|
491
|
+
if (existingVary) {
|
|
492
|
+
if (!existingVary.toLowerCase().includes('accept-encoding')) {
|
|
493
|
+
responseHeaders.set('vary', existingVary + ', Accept-Encoding');
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
responseHeaders.set('vary', 'Accept-Encoding');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Z_SYNC_FLUSH ensures each chunk is flushed to the output immediately.
|
|
500
|
+
// Without it, gzip buffers internally and the browser doesn't receive
|
|
501
|
+
// the HTML shell until the gzip stream closes — breaking streaming.
|
|
502
|
+
// ~2–5% size overhead vs Z_NO_FLUSH but preserves correct streaming.
|
|
503
|
+
return createGzip({ flush: constants.Z_SYNC_FLUSH });
|
|
504
|
+
}
|