@bleedingdev/modern-js-renderer-octane 0.0.0-bootstrap → 3.9.3-ultramodern.1

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 (137) hide show
  1. package/LICENSE +21 -0
  2. package/bin/octane-tsc.mjs +4 -0
  3. package/dist/cjs/bootstrap.js +68 -0
  4. package/dist/cjs/client.js +371 -0
  5. package/dist/cjs/entry-application.js +83 -0
  6. package/dist/cjs/entry-client.js +92 -0
  7. package/dist/cjs/entry-server.js +127 -0
  8. package/dist/cjs/federation-context.js +72 -0
  9. package/dist/cjs/federation.js +103 -0
  10. package/dist/cjs/i18n/I18nProvider.js +67 -0
  11. package/dist/cjs/i18n/LocalizedLink.js +89 -0
  12. package/dist/cjs/i18n/context.js +41 -0
  13. package/dist/cjs/i18n/index.js +86 -0
  14. package/dist/cjs/i18n/languageSync.js +99 -0
  15. package/dist/cjs/i18n/types.js +18 -0
  16. package/dist/cjs/i18n/useI18n.js +110 -0
  17. package/dist/cjs/index.js +46 -0
  18. package/dist/cjs/manifest.js +167 -0
  19. package/dist/cjs/plugin/compiler.js +125 -0
  20. package/dist/cjs/plugin/create.js +265 -0
  21. package/dist/cjs/plugin/index.js +151 -0
  22. package/dist/cjs/plugin/profile.js +89 -0
  23. package/dist/cjs/plugin/source.js +56 -0
  24. package/dist/cjs/router-client.js +139 -0
  25. package/dist/cjs/router-handler.js +168 -0
  26. package/dist/cjs/router-injection.js +139 -0
  27. package/dist/cjs/router-server-cleanup.js +46 -0
  28. package/dist/cjs/router-server-snapshot.js +405 -0
  29. package/dist/cjs/router-server.js +108 -0
  30. package/dist/cjs/router-snapshot.js +270 -0
  31. package/dist/cjs/router.js +218 -0
  32. package/dist/cjs/routes.js +187 -0
  33. package/dist/cjs/server.js +224 -0
  34. package/dist/cjs/typecheck.js +615 -0
  35. package/dist/esm/bootstrap.mjs +24 -0
  36. package/dist/esm/client.mjs +327 -0
  37. package/dist/esm/entry-application.mjs +39 -0
  38. package/dist/esm/entry-client.mjs +54 -0
  39. package/dist/esm/entry-server.mjs +86 -0
  40. package/dist/esm/federation-context.mjs +25 -0
  41. package/dist/esm/federation.mjs +65 -0
  42. package/dist/esm/i18n/I18nProvider.mjs +29 -0
  43. package/dist/esm/i18n/LocalizedLink.mjs +48 -0
  44. package/dist/esm/i18n/context.mjs +3 -0
  45. package/dist/esm/i18n/index.mjs +8 -0
  46. package/dist/esm/i18n/languageSync.mjs +61 -0
  47. package/dist/esm/i18n/types.mjs +0 -0
  48. package/dist/esm/i18n/useI18n.mjs +72 -0
  49. package/dist/esm/index.mjs +8 -0
  50. package/dist/esm/manifest.mjs +117 -0
  51. package/dist/esm/router-client.mjs +98 -0
  52. package/dist/esm/router-handler.mjs +130 -0
  53. package/dist/esm/router-injection.mjs +101 -0
  54. package/dist/esm/router-server-cleanup.mjs +8 -0
  55. package/dist/esm/router-server-snapshot.mjs +367 -0
  56. package/dist/esm/router-server.mjs +12 -0
  57. package/dist/esm/router-snapshot.mjs +199 -0
  58. package/dist/esm/router.mjs +94 -0
  59. package/dist/esm/routes.mjs +134 -0
  60. package/dist/esm/server.mjs +183 -0
  61. package/dist/esm-node/bootstrap.mjs +24 -0
  62. package/dist/esm-node/manifest.mjs +117 -0
  63. package/dist/esm-node/plugin/compiler.mjs +70 -0
  64. package/dist/esm-node/plugin/create.mjs +227 -0
  65. package/dist/esm-node/plugin/index.mjs +103 -0
  66. package/dist/esm-node/plugin/profile.mjs +51 -0
  67. package/dist/esm-node/plugin/source.mjs +7 -0
  68. package/dist/esm-node/typecheck.mjs +560 -0
  69. package/dist/types/bootstrap.d.ts +10 -0
  70. package/dist/types/client.d.ts +36 -0
  71. package/dist/types/entry-application.d.ts +19 -0
  72. package/dist/types/entry-client.d.ts +8 -0
  73. package/dist/types/entry-server.d.ts +11 -0
  74. package/dist/types/federation-context.d.ts +28 -0
  75. package/dist/types/federation.d.ts +18 -0
  76. package/dist/types/i18n/I18nProvider.d.ts +8 -0
  77. package/dist/types/i18n/LocalizedLink.d.ts +41 -0
  78. package/dist/types/i18n/context.d.ts +9 -0
  79. package/dist/types/i18n/index.d.ts +9 -0
  80. package/dist/types/i18n/languageSync.d.ts +26 -0
  81. package/dist/types/i18n/types.d.ts +34 -0
  82. package/dist/types/i18n/useI18n.d.ts +21 -0
  83. package/dist/types/index.d.ts +9 -0
  84. package/dist/types/manifest.d.ts +34 -0
  85. package/dist/types/plugin/compiler-manifest.d.cts +10 -0
  86. package/dist/types/plugin/compiler.d.ts +5 -0
  87. package/dist/types/plugin/create.d.ts +23 -0
  88. package/dist/types/plugin/index.d.ts +3 -0
  89. package/dist/types/plugin/profile.d.ts +2 -0
  90. package/dist/types/plugin/source.d.ts +1 -0
  91. package/dist/types/plugin/svg-component-template.d.cts +6 -0
  92. package/dist/types/router-client.d.ts +12 -0
  93. package/dist/types/router-handler.d.ts +13 -0
  94. package/dist/types/router-injection.d.ts +8 -0
  95. package/dist/types/router-server-cleanup.d.ts +3 -0
  96. package/dist/types/router-server-snapshot.d.ts +15 -0
  97. package/dist/types/router-server.d.ts +13 -0
  98. package/dist/types/router-snapshot.d.ts +14 -0
  99. package/dist/types/router.d.ts +30 -0
  100. package/dist/types/routes.d.ts +18 -0
  101. package/dist/types/server.d.ts +41 -0
  102. package/dist/types/typecheck.d.ts +31 -0
  103. package/package.json +211 -4
  104. package/src/bootstrap.ts +60 -0
  105. package/src/client.ts +485 -0
  106. package/src/entry-application.ts +72 -0
  107. package/src/entry-client.ts +80 -0
  108. package/src/entry-server.ts +142 -0
  109. package/src/federation-context.ts +53 -0
  110. package/src/federation.ts +158 -0
  111. package/src/i18n/I18nProvider.ts +56 -0
  112. package/src/i18n/LocalizedLink.ts +112 -0
  113. package/src/i18n/context.ts +12 -0
  114. package/src/i18n/index.ts +35 -0
  115. package/src/i18n/languageSync.ts +132 -0
  116. package/src/i18n/types.ts +36 -0
  117. package/src/i18n/useI18n.ts +119 -0
  118. package/src/index.ts +10 -0
  119. package/src/manifest.ts +253 -0
  120. package/src/plugin/compiler-manifest.cjs +178 -0
  121. package/src/plugin/compiler.ts +112 -0
  122. package/src/plugin/create.ts +272 -0
  123. package/src/plugin/index.ts +105 -0
  124. package/src/plugin/profile.ts +46 -0
  125. package/src/plugin/source.ts +16 -0
  126. package/src/plugin/svg-component-template.cjs +17 -0
  127. package/src/router-client.ts +144 -0
  128. package/src/router-handler.ts +182 -0
  129. package/src/router-injection.ts +145 -0
  130. package/src/router-server-cleanup.ts +11 -0
  131. package/src/router-server-snapshot.ts +564 -0
  132. package/src/router-server.ts +23 -0
  133. package/src/router-snapshot.ts +251 -0
  134. package/src/router.ts +192 -0
  135. package/src/routes.ts +263 -0
  136. package/src/server.ts +300 -0
  137. package/src/typecheck.ts +723 -0
@@ -0,0 +1,46 @@
1
+ import type { RendererBuildProfile } from '@modern-js/renderer-core/adapter';
2
+
3
+ export const octaneProfile: RendererBuildProfile<'octane'> = {
4
+ renderer: 'octane',
5
+ status: 'preview',
6
+ protocolVersion: 1,
7
+ minimumNode: '26.10.0',
8
+ hmr: {
9
+ editedBoundary: 'may-reset',
10
+ unaffectedComponents: 'preserved',
11
+ document: 'preserved',
12
+ roots: 'single',
13
+ cleanup: 'exactly-once',
14
+ },
15
+ compiler: { name: '@octanejs/rspack-plugin', version: '0.1.55' },
16
+ hydration: {
17
+ name: 'octane',
18
+ version: '0.7.1+ultramodern.1331985ea3b0',
19
+ },
20
+ router: {
21
+ name: '@octanejs/tanstack-router',
22
+ version: '0.1.60+ultramodern.0b9f76ee3003',
23
+ coreName: '@tanstack/router-core',
24
+ coreVersion: '1.171.15',
25
+ },
26
+ sourceExtensions: ['.tsx', '.tsrx', '.ts', '.js'],
27
+ jsxImportSource: 'octane',
28
+ dependencies: {
29
+ octane:
30
+ 'https://github.com/bleedingdev/octane/releases/download/octane%400.7.1%2Bultramodern.1331985ea3b0/octane-0.7.1%2Bultramodern.1331985ea3b0.tgz',
31
+ '@octanejs/tanstack-router':
32
+ 'https://github.com/bleedingdev/octane/releases/download/%40octanejs%2Ftanstack-router%400.1.60%2Bultramodern.0b9f76ee3003/octanejs-tanstack-router-0.1.60%2Bultramodern.0b9f76ee3003.tgz',
33
+ '@octanejs/rspack-plugin': '0.1.55',
34
+ '@modern-js/renderer-octane': '3.8.3',
35
+ seroval: '1.6.8',
36
+ 'seroval-plugins': '1.6.8',
37
+ },
38
+ capabilities: {
39
+ worker: true,
40
+ moduleFederation: true,
41
+ rsc: false,
42
+ ssg: true,
43
+ i18n: true,
44
+ svgComponent: true,
45
+ },
46
+ };
@@ -0,0 +1,16 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { octaneProfile } from './profile';
4
+
5
+ export async function assertOctaneEntrySource(
6
+ source: string | false | undefined,
7
+ ): Promise<void> {
8
+ if (
9
+ source &&
10
+ path.extname(source).toLowerCase() === '.jsx' &&
11
+ (await fs.stat(source)).isFile()
12
+ )
13
+ throw new Error(
14
+ `unsupported-renderer-capability: Octane does not support .jsx source: ${source}. Supported source extensions: ${octaneProfile.sourceExtensions.join(', ')}.`,
15
+ );
16
+ }
@@ -0,0 +1,17 @@
1
+ // Octane follows React host semantics: refs are props and inner markup is
2
+ // trusted HTML. Native attribute names and `class` pass through unchanged.
3
+ module.exports = ({
4
+ attributes,
5
+ markup,
6
+ origin,
7
+ }) => `/** @jsxImportSource octane */
8
+ // Generated by UltraModern from ${origin}. Do not edit.
9
+ import { trustHTML } from 'octane';
10
+
11
+ const attributes: { readonly [name: string]: string } = ${attributes};
12
+ const markup = trustHTML(${markup});
13
+
14
+ export default function SvgComponent(props: { readonly [name: string]: unknown }) {
15
+ return <svg {...attributes} {...props} dangerouslySetInnerHTML={markup} />;
16
+ }
17
+ `;
@@ -0,0 +1,144 @@
1
+ import { type AnyRouter, RouterProvider } from '@octanejs/tanstack-router';
2
+ import { hydrate } from '@octanejs/tanstack-router/ssr/client';
3
+ import {
4
+ createElement,
5
+ hookSlots,
6
+ initializeHydrationEventCapture,
7
+ useEffect,
8
+ } from 'octane';
9
+ import { octaneRouterSnapshotAdapter } from './router-snapshot';
10
+
11
+ export interface OctaneRouterHydrationOptions {
12
+ signal?: AbortSignal;
13
+ }
14
+
15
+ const hydration = new WeakMap<AnyRouter, Promise<void>>();
16
+
17
+ function abortReason(signal: AbortSignal): unknown {
18
+ return signal.reason === undefined
19
+ ? new DOMException('Octane router hydration aborted', 'AbortError')
20
+ : signal.reason;
21
+ }
22
+
23
+ function awaitHydration(
24
+ promise: Promise<void>,
25
+ signal?: AbortSignal,
26
+ ): Promise<void> {
27
+ if (!signal) return promise;
28
+ signal.throwIfAborted();
29
+ return new Promise((resolve, reject) => {
30
+ let settled = false;
31
+ const finish = (success: boolean, error?: unknown) => {
32
+ if (settled) return;
33
+ settled = true;
34
+ signal.removeEventListener('abort', onAbort);
35
+ if (success) resolve();
36
+ else reject(error);
37
+ };
38
+ const onAbort = () => finish(false, abortReason(signal));
39
+ signal.addEventListener('abort', onAbort, { once: true });
40
+ promise.then(
41
+ () => finish(true),
42
+ error => finish(false, error),
43
+ );
44
+ if (signal.aborted) onAbort();
45
+ });
46
+ }
47
+
48
+ interface ReactiveMatchIds {
49
+ get(): readonly string[];
50
+ subscribe(callback: () => void): { unsubscribe(): void };
51
+ }
52
+
53
+ function isReactiveMatchIds(value: unknown): value is ReactiveMatchIds {
54
+ return (
55
+ typeof value === 'object' &&
56
+ value !== null &&
57
+ typeof Reflect.get(value, 'get') === 'function' &&
58
+ typeof Reflect.get(value, 'subscribe') === 'function'
59
+ );
60
+ }
61
+
62
+ function waitForCommittedMatches(
63
+ router: AnyRouter,
64
+ signal?: AbortSignal,
65
+ ): Promise<void> {
66
+ signal?.throwIfAborted();
67
+ const matchesId = router.stores.matchesId;
68
+ if (matchesId.get().length > 0) return Promise.resolve();
69
+ if (!isReactiveMatchIds(matchesId)) {
70
+ throw new Error(
71
+ 'Octane router hydration requires its native reactive match store',
72
+ );
73
+ }
74
+ return new Promise((resolve, reject) => {
75
+ let subscription: ReturnType<typeof matchesId.subscribe> | undefined;
76
+ let settled = false;
77
+ const cleanup = () => {
78
+ subscription?.unsubscribe();
79
+ signal?.removeEventListener('abort', onAbort);
80
+ };
81
+ const finish = () => {
82
+ if (settled || matchesId.get().length === 0) return;
83
+ settled = true;
84
+ cleanup();
85
+ resolve();
86
+ };
87
+ const onAbort = () => {
88
+ if (settled) return;
89
+ settled = true;
90
+ cleanup();
91
+ if (signal) reject(abortReason(signal));
92
+ };
93
+ signal?.addEventListener('abort', onAbort, { once: true });
94
+ if (signal?.aborted) {
95
+ onAbort();
96
+ return;
97
+ }
98
+ subscription = matchesId.subscribe(finish);
99
+ // Native stores can notify synchronously while a subscription is installed.
100
+ if (settled) subscription.unsubscribe();
101
+ else finish();
102
+ });
103
+ }
104
+
105
+ /** Restore native SSR matches before Octane adopts the server's route ranges. */
106
+ export async function prepareOctaneRouterHydration(
107
+ router: AnyRouter,
108
+ options: OctaneRouterHydrationOptions = {},
109
+ ): Promise<void> {
110
+ initializeHydrationEventCapture();
111
+ options.signal?.throwIfAborted();
112
+ const adapters = router.options.serializationAdapters ?? [];
113
+ if (!adapters.includes(octaneRouterSnapshotAdapter)) {
114
+ const nativeOptions = {
115
+ ...router.options,
116
+ serializationAdapters: [octaneRouterSnapshotAdapter, ...adapters],
117
+ };
118
+ router.update(nativeOptions);
119
+ }
120
+ let pending = hydration.get(router);
121
+ if (!pending) {
122
+ pending = Promise.resolve(hydrate(router)).then(() => {});
123
+ // Native hydration can finish after this startup owner has been cancelled.
124
+ void pending.catch(() => {});
125
+ hydration.set(router, pending);
126
+ }
127
+ await awaitHydration(pending, options.signal);
128
+ await waitForCommittedMatches(router, options.signal);
129
+ options.signal?.throwIfAborted();
130
+ }
131
+
132
+ const routerHydrationEffectSlot = Symbol(hookSlots(1));
133
+
134
+ /** Adopt the native provider's ranges inside the admitted fragment host. */
135
+ export function OctaneRouterRoot({ router }: { router: AnyRouter }) {
136
+ useEffect(
137
+ () => {
138
+ window.$_TSR?.h();
139
+ },
140
+ [],
141
+ routerHydrationEffectSlot,
142
+ );
143
+ return createElement(RouterProvider, { router });
144
+ }
@@ -0,0 +1,182 @@
1
+ import type { RequestSession } from '@modern-js/renderer-core/session';
2
+ import { type AnyRouter, createMemoryHistory } from '@octanejs/tanstack-router';
3
+ import {
4
+ attachRouterServerSsrUtils,
5
+ getNormalizedURL,
6
+ getOrigin,
7
+ normalizeSsrResponse,
8
+ type RequestHandler,
9
+ } from '@octanejs/tanstack-router/ssr/server';
10
+ import { cleanupOctaneRouterSSR } from './router-server-cleanup';
11
+ import {
12
+ type OctaneRouterSerializationOptions,
13
+ prepareOctaneRouterSerialization,
14
+ } from './router-server-snapshot';
15
+
16
+ export interface OctaneRequestHandlerOptions<TRouter extends AnyRouter> {
17
+ createRouter(): TRouter;
18
+ request: Request;
19
+ session: RequestSession;
20
+ getRouterManifest?: () =>
21
+ | Parameters<typeof attachRouterServerSsrUtils>[0]['manifest']
22
+ | Promise<Parameters<typeof attachRouterServerSsrUtils>[0]['manifest']>;
23
+ serialization?: OctaneRouterSerializationOptions;
24
+ }
25
+
26
+ function withAbort<T>(pending: Promise<T>, signal: AbortSignal): Promise<T> {
27
+ signal.throwIfAborted();
28
+ return new Promise((resolve, reject) => {
29
+ let settled = false;
30
+ const finish = (success: boolean, value: unknown) => {
31
+ if (settled) return;
32
+ settled = true;
33
+ signal.removeEventListener('abort', onAbort);
34
+ if (success) resolve(value as T);
35
+ else reject(value);
36
+ };
37
+ const onAbort = () => finish(false, signal.reason);
38
+ signal.addEventListener('abort', onAbort, { once: true });
39
+ pending.then(
40
+ value => finish(true, value),
41
+ error => finish(false, error),
42
+ );
43
+ if (signal.aborted) onAbort();
44
+ });
45
+ }
46
+
47
+ /** List fields that nested matches add to: CSP policies, links and timing. */
48
+ const CSP_HEADERS = new Set([
49
+ 'content-security-policy',
50
+ 'content-security-policy-report-only',
51
+ 'link',
52
+ 'server-timing',
53
+ ]);
54
+
55
+ /**
56
+ * Match headers are server HTTP policy, never part of the public router
57
+ * snapshot. Nested matches union `Vary` and append CSP fields; other
58
+ * singleton fields stay last-writer-wins.
59
+ */
60
+ function appendHeaders(destination: Headers, value: unknown): void {
61
+ if (value == null) return;
62
+ const source =
63
+ value instanceof Headers ? value : new Headers(value as HeadersInit);
64
+ for (const [key, item] of Headers.prototype.entries.call(source)) {
65
+ if (key === 'set-cookie') continue;
66
+ if (CSP_HEADERS.has(key)) destination.append(key, item);
67
+ else if (key === 'vary') {
68
+ const fields = new Map<string, string>();
69
+ for (const field of `${destination.get('vary') ?? ''},${item}`.split(
70
+ ',',
71
+ )) {
72
+ const name = field.trim();
73
+ if (name && !fields.has(name.toLowerCase()))
74
+ fields.set(name.toLowerCase(), name);
75
+ }
76
+ destination.set('vary', [...fields.values()].join(', '));
77
+ } else destination.set(key, item);
78
+ }
79
+ for (const cookie of Headers.prototype.getSetCookie.call(source)) {
80
+ destination.append('set-cookie', cookie);
81
+ }
82
+ }
83
+
84
+ function requestHeaders(router: AnyRouter): Headers {
85
+ const headers = new Headers({ 'content-type': 'text/html; charset=utf-8' });
86
+ for (const match of router.stores.matches.get()) {
87
+ const field = Object.getOwnPropertyDescriptor(match, 'headers');
88
+ if (field?.get || field?.set)
89
+ throw new TypeError(
90
+ 'Native Octane response headers must be owned values',
91
+ );
92
+ appendHeaders(headers, field?.value);
93
+ }
94
+ const redirect = router.stores.redirect.get();
95
+ if (redirect) appendHeaders(headers, redirect.headers);
96
+ return headers;
97
+ }
98
+
99
+ /** Resolve native routes and HTTP disposition before the native document serializer. */
100
+ export function createOctaneRequestHandler<TRouter extends AnyRouter>({
101
+ createRouter,
102
+ request,
103
+ session,
104
+ getRouterManifest,
105
+ serialization,
106
+ }: OctaneRequestHandlerOptions<TRouter>): RequestHandler<TRouter> {
107
+ return async callback => {
108
+ session.signal.throwIfAborted();
109
+ const router = createRouter();
110
+ let responseOwnsCleanup = false;
111
+ try {
112
+ const manifest = getRouterManifest
113
+ ? await withAbort(Promise.resolve(getRouterManifest()), session.signal)
114
+ : undefined;
115
+ attachRouterServerSsrUtils({ router, manifest });
116
+ const guard = prepareOctaneRouterSerialization(router, session, {
117
+ ...serialization,
118
+ forbiddenValues: [request, ...(serialization?.forbiddenValues ?? [])],
119
+ });
120
+ session.registerCleanup(() => router.cancelMatches());
121
+
122
+ const { url } = getNormalizedURL(request.url, 'http://localhost');
123
+ const href = url.href.replace(url.origin, '');
124
+ router.update({
125
+ ...router.options,
126
+ history: createMemoryHistory({ initialEntries: [href] }),
127
+ origin: router.options.origin ?? getOrigin(request),
128
+ });
129
+ await withAbort(router.load(), session.signal);
130
+ guard.assertActive();
131
+
132
+ // A native redirect is a terminal HTTP Response. It must never enter
133
+ // the public serializer as a match error or document payload.
134
+ const redirect = router.stores.redirect.get();
135
+ if (redirect) {
136
+ const resolved = router.resolveRedirect(redirect);
137
+ const headers = requestHeaders(router);
138
+ return new Response(null, {
139
+ status: resolved.status,
140
+ statusText: resolved.statusText,
141
+ headers,
142
+ });
143
+ }
144
+
145
+ guard.assertMatches();
146
+ const serverSsr = router.serverSsr;
147
+ if (!serverSsr)
148
+ throw new Error('Native Octane router SSR state is required');
149
+ await withAbort(serverSsr.dehydrate(), session.signal);
150
+ guard.assertActive();
151
+ const responseHeaders = requestHeaders(router);
152
+ // A callback that ignores cancellation must not hold the router SSR
153
+ // state; a result arriving after abort only has its body released.
154
+ const rendered = Promise.resolve(
155
+ callback({ request, router, responseHeaders }),
156
+ );
157
+ const result = normalizeSsrResponse(
158
+ await withAbort(rendered, session.signal).catch(error => {
159
+ if (session.signal.aborted)
160
+ void rendered.then(
161
+ late => {
162
+ try {
163
+ void normalizeSsrResponse(late)
164
+ .response.body?.cancel()
165
+ .catch(() => {});
166
+ } catch {}
167
+ },
168
+ () => {},
169
+ );
170
+ throw error;
171
+ }),
172
+ );
173
+ responseOwnsCleanup = result.serverSsrCleanup === 'stream';
174
+ return result.response;
175
+ } catch (error) {
176
+ session.fail(error);
177
+ throw error;
178
+ } finally {
179
+ if (!responseOwnsCleanup) cleanupOctaneRouterSSR(router);
180
+ }
181
+ };
182
+ }
@@ -0,0 +1,145 @@
1
+ import type { RequestSession } from '@modern-js/renderer-core/session';
2
+ import type { AnyRouter } from '@octanejs/tanstack-router';
3
+ import type { StreamInjectionSource } from 'octane/server';
4
+ import { cleanupOctaneRouterSSR } from './router-server-cleanup';
5
+
6
+ const SERIALIZATION_TIMEOUT_MS = 60_000;
7
+
8
+ export interface OctaneRouterInjectionOptions {
9
+ serializationTimeoutMs?: number;
10
+ }
11
+
12
+ /** Couple native router serialization to the response's single stream owner. */
13
+ export function createOctaneRouterInjection(
14
+ router: AnyRouter,
15
+ session: RequestSession,
16
+ options: OctaneRouterInjectionOptions = {},
17
+ ): StreamInjectionSource {
18
+ const serverSsr = router.serverSsr;
19
+ if (!serverSsr) {
20
+ throw new Error('Native Octane router.serverSsr is required for streaming');
21
+ }
22
+ const timeoutMs = options.serializationTimeoutMs ?? SERIALIZATION_TIMEOUT_MS;
23
+ if (
24
+ !Number.isInteger(timeoutMs) ||
25
+ timeoutMs <= 0 ||
26
+ timeoutMs > SERIALIZATION_TIMEOUT_MS
27
+ ) {
28
+ throw new TypeError(
29
+ 'Octane router serialization timeout must be 1 to 60000 ms',
30
+ );
31
+ }
32
+ session.signal.throwIfAborted();
33
+ let settled = false;
34
+ let released = false;
35
+ let renderFinished = false;
36
+ let timer: ReturnType<typeof setTimeout> | undefined;
37
+ let stopSerialization: (() => void) | undefined;
38
+ const subscriptions = new Set<() => void>();
39
+ let resolveDone!: () => void;
40
+ let rejectDone!: (error: unknown) => void;
41
+ const done = new Promise<void>((resolve, reject) => {
42
+ resolveDone = resolve;
43
+ rejectDone = reject;
44
+ });
45
+ // The caller may fail before attaching the injection to its native renderer.
46
+ void done.catch(() => {});
47
+
48
+ const settle = (error?: unknown) => {
49
+ if (settled) return;
50
+ settled = true;
51
+ if (timer !== undefined) clearTimeout(timer);
52
+ timer = undefined;
53
+ stopSerialization?.();
54
+ stopSerialization = undefined;
55
+ if (error === undefined) resolveDone();
56
+ else rejectDone(error);
57
+ };
58
+ const release = () => {
59
+ if (released) return;
60
+ released = true;
61
+ if (timer !== undefined) clearTimeout(timer);
62
+ timer = undefined;
63
+ stopSerialization?.();
64
+ stopSerialization = undefined;
65
+ for (const unsubscribe of subscriptions) unsubscribe();
66
+ subscriptions.clear();
67
+ session.signal.removeEventListener('abort', onAbort);
68
+ if (!settled) {
69
+ settle(
70
+ session.signal.reason ??
71
+ new Error('Octane router serialization was interrupted'),
72
+ );
73
+ }
74
+ };
75
+ function onAbort() {
76
+ settle(
77
+ session.signal.reason ??
78
+ new DOMException('Octane request aborted', 'AbortError'),
79
+ );
80
+ release();
81
+ }
82
+
83
+ if (serverSsr.isSerializationFinished()) settle();
84
+ else stopSerialization = serverSsr.onSerializationFinished(() => settle());
85
+ serverSsr.onCleanup(release);
86
+ session.signal.addEventListener('abort', onAbort, { once: true });
87
+ try {
88
+ session.registerCleanup(() => {
89
+ release();
90
+ cleanupOctaneRouterSSR(router);
91
+ });
92
+ } catch (error) {
93
+ release();
94
+ cleanupOctaneRouterSSR(router);
95
+ throw error;
96
+ }
97
+
98
+ return {
99
+ take() {
100
+ if (released) return '';
101
+ // Octane takes injected HTML only once the shell is rendered, so the
102
+ // router's Scripts has already moved the $_TSR bootstrap into its
103
+ // barrier script. Lift the barrier here, not on subscribe: streamed
104
+ // signals subscribe before rendering. Deferred $_TSR chunks then stream
105
+ // as they settle instead of waiting for setRenderFinished.
106
+ serverSsr.liftScriptBarrier();
107
+ return serverSsr.takeBufferedHtml() ?? '';
108
+ },
109
+ subscribe(notify) {
110
+ if (released) return () => {};
111
+ const stop = serverSsr.onInjectedHtml(notify);
112
+ let subscribed = true;
113
+ const unsubscribe = () => {
114
+ if (!subscribed) return;
115
+ subscribed = false;
116
+ subscriptions.delete(unsubscribe);
117
+ stop();
118
+ };
119
+ subscriptions.add(unsubscribe);
120
+ return unsubscribe;
121
+ },
122
+ done,
123
+ cancel(reason) {
124
+ settle(
125
+ reason ?? new DOMException('Octane rendering cancelled', 'AbortError'),
126
+ );
127
+ release();
128
+ void session.abort(reason);
129
+ },
130
+ renderComplete() {
131
+ if (released || renderFinished) return;
132
+ renderFinished = true;
133
+ serverSsr.setRenderFinished();
134
+ if (!serverSsr.isSerializationFinished() && !settled) {
135
+ timer = setTimeout(() => {
136
+ const error = new Error(
137
+ 'Octane router serialization timed out after app rendering',
138
+ );
139
+ settle(error);
140
+ void session.fail(error);
141
+ }, timeoutMs);
142
+ }
143
+ },
144
+ };
145
+ }
@@ -0,0 +1,11 @@
1
+ import type { AnyRouter } from '@octanejs/tanstack-router';
2
+
3
+ const released = new WeakSet<NonNullable<AnyRouter['serverSsr']>>();
4
+
5
+ /** The request handler, serializer and renderer share one native SSR owner. */
6
+ export function cleanupOctaneRouterSSR(router: AnyRouter): void {
7
+ const serverSsr = router.serverSsr;
8
+ if (!serverSsr || released.has(serverSsr)) return;
9
+ released.add(serverSsr);
10
+ serverSsr.cleanup();
11
+ }