@bleedingdev/modern-js-renderer-octane 3.9.3-ultramodern.7 → 3.9.3-ultramodern.9

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 (96) hide show
  1. package/dist/cjs/client.js +56 -6
  2. package/dist/cjs/entry-application.js +7 -2
  3. package/dist/cjs/entry-client.js +3 -0
  4. package/dist/cjs/entry-server.js +13 -5
  5. package/dist/cjs/federation.js +108 -6
  6. package/dist/cjs/i18n/I18nProvider.js +6 -14
  7. package/dist/cjs/i18n/LocalizedLink.js +24 -33
  8. package/dist/cjs/i18n/index.js +0 -3
  9. package/dist/cjs/i18n-external-snapshot.js +645 -0
  10. package/dist/cjs/index.js +4 -4
  11. package/dist/cjs/manifest.js +2 -2
  12. package/dist/cjs/plugin/compiler.js +2 -5
  13. package/dist/cjs/plugin/index.js +2 -2
  14. package/dist/cjs/plugin/profile.js +12 -10
  15. package/dist/cjs/router-client.js +1 -1
  16. package/dist/cjs/router-external-snapshot.js +263 -0
  17. package/dist/cjs/router-handler.js +11 -8
  18. package/dist/cjs/router-server.js +3 -11
  19. package/dist/cjs/router.js +1 -1
  20. package/dist/cjs/routes.js +11 -17
  21. package/dist/cjs/server.js +28 -7
  22. package/dist/cjs/worker-federation-context.js +63 -0
  23. package/dist/cjs/worker-federation-contexts.js +97 -0
  24. package/dist/cjs/worker-federation.js +97 -0
  25. package/dist/esm/client.mjs +56 -6
  26. package/dist/esm/entry-application.mjs +7 -2
  27. package/dist/esm/entry-client.mjs +3 -0
  28. package/dist/esm/entry-server.mjs +11 -6
  29. package/dist/esm/federation.mjs +110 -8
  30. package/dist/esm/i18n/I18nProvider.mjs +7 -15
  31. package/dist/esm/i18n/LocalizedLink.mjs +24 -30
  32. package/dist/esm/i18n/index.mjs +1 -1
  33. package/dist/esm/i18n-external-snapshot.mjs +576 -0
  34. package/dist/esm/index.mjs +4 -4
  35. package/dist/esm/manifest.mjs +2 -2
  36. package/dist/esm/router-client.mjs +1 -1
  37. package/dist/esm/router-external-snapshot.mjs +216 -0
  38. package/dist/esm/router-handler.mjs +11 -8
  39. package/dist/esm/router-server.mjs +0 -1
  40. package/dist/esm/router.mjs +2 -2
  41. package/dist/esm/routes.mjs +11 -17
  42. package/dist/esm/server.mjs +24 -3
  43. package/dist/esm/worker-federation-context.mjs +19 -0
  44. package/dist/esm/worker-federation-contexts.mjs +47 -0
  45. package/dist/esm/worker-federation.mjs +59 -0
  46. package/dist/esm-node/manifest.mjs +2 -2
  47. package/dist/esm-node/plugin/compiler.mjs +2 -5
  48. package/dist/esm-node/plugin/index.mjs +2 -2
  49. package/dist/esm-node/plugin/profile.mjs +12 -10
  50. package/dist/types/client.d.ts +5 -1
  51. package/dist/types/entry-client.d.ts +2 -0
  52. package/dist/types/entry-server.d.ts +4 -1
  53. package/dist/types/federation.d.ts +2 -0
  54. package/dist/types/i18n/LocalizedLink.d.ts +5 -32
  55. package/dist/types/i18n/context.d.ts +1 -1
  56. package/dist/types/i18n/index.d.ts +1 -1
  57. package/dist/types/i18n-external-snapshot.d.ts +43 -0
  58. package/dist/types/index.d.ts +4 -4
  59. package/dist/types/manifest.d.ts +2 -2
  60. package/dist/types/router-external-snapshot.d.ts +10 -0
  61. package/dist/types/router-server.d.ts +0 -2
  62. package/dist/types/server.d.ts +6 -0
  63. package/dist/types/worker-federation-context.d.ts +34 -0
  64. package/dist/types/worker-federation-contexts.d.ts +25 -0
  65. package/dist/types/worker-federation.d.ts +20 -0
  66. package/package.json +84 -22
  67. package/src/client.ts +99 -16
  68. package/src/entry-application.ts +7 -3
  69. package/src/entry-client.ts +10 -0
  70. package/src/entry-server.ts +20 -9
  71. package/src/federation.ts +159 -15
  72. package/src/i18n/I18nProvider.ts +11 -20
  73. package/src/i18n/LocalizedLink.ts +46 -72
  74. package/src/i18n/context.ts +1 -1
  75. package/src/i18n/index.ts +1 -5
  76. package/src/i18n-external-snapshot.ts +803 -0
  77. package/src/index.ts +4 -4
  78. package/src/manifest.ts +2 -2
  79. package/src/plugin/compiler-manifest.cjs +27 -1
  80. package/src/plugin/compiler.ts +6 -14
  81. package/src/plugin/index.ts +4 -2
  82. package/src/plugin/profile.ts +14 -10
  83. package/src/router-client.ts +1 -1
  84. package/src/router-external-snapshot.ts +306 -0
  85. package/src/router-handler.ts +11 -8
  86. package/src/router-server.ts +0 -2
  87. package/src/router.ts +2 -2
  88. package/src/routes.ts +28 -26
  89. package/src/server.ts +45 -5
  90. package/src/worker-federation-context.ts +51 -0
  91. package/src/worker-federation-contexts.ts +91 -0
  92. package/src/worker-federation.ts +120 -0
  93. package/dist/cjs/router-injection.js +0 -139
  94. package/dist/esm/router-injection.mjs +0 -101
  95. package/dist/types/router-injection.d.ts +0 -8
  96. package/src/router-injection.ts +0 -145
package/src/index.ts CHANGED
@@ -3,8 +3,8 @@ export type { OctaneDocumentBootstrap } from './bootstrap';
3
3
 
4
4
  export const OCTANE_RUNTIME_PROFILE = Object.freeze({
5
5
  renderer: 'octane',
6
- runtime: '0.7.1+ultramodern.1331985ea3b0',
7
- router: '0.1.60+ultramodern.0b9f76ee3003',
8
- routerCore: '1.171.15',
9
- history: '1.162.0',
6
+ runtime: '0.13.8',
7
+ router: '0.2.10',
8
+ routerCore: '1.171.34',
9
+ history: '1.162.4',
10
10
  });
package/src/manifest.ts CHANGED
@@ -9,8 +9,8 @@ import {
9
9
  assertOctaneIdentity,
10
10
  } from './bootstrap';
11
11
 
12
- export const OCTANE_RUNTIME_VERSION = '0.7.1+ultramodern.1331985ea3b0';
13
- export const OCTANE_COMPILER_VERSION = '0.1.55';
12
+ export const OCTANE_RUNTIME_VERSION = '0.13.8';
13
+ export const OCTANE_COMPILER_VERSION = '0.2.4';
14
14
 
15
15
  export interface OctaneCompiledSource {
16
16
  readonly resource: string;
@@ -107,6 +107,29 @@ class OctaneCompilerManifestPlugin {
107
107
  sources.sort((left, right) =>
108
108
  left.resource.localeCompare(right.resource),
109
109
  );
110
+ // Module Federation publishes exposed native modules through a
111
+ // separate container entry in this same compilation. Its browser
112
+ // closure belongs to this application's publication, even when the
113
+ // document itself never imports an exposed component.
114
+ const containerChunks = new Set();
115
+ for (const [entryName, entry] of compilation.entries) {
116
+ if (
117
+ !entry.dependencies.some(
118
+ dependency => dependency.type === 'container entry',
119
+ )
120
+ )
121
+ continue;
122
+ const container = compilation.entrypoints.get(entryName);
123
+ if (!container)
124
+ throw new Error(
125
+ `Octane compiler has no federation container entry for ${entryName}.`,
126
+ );
127
+ for (const chunk of container.chunks) {
128
+ containerChunks.add(chunk);
129
+ for (const referenced of chunk.getAllReferencedChunks())
130
+ containerChunks.add(referenced);
131
+ }
132
+ }
110
133
  const identities = this.options.rendererIdentities();
111
134
  if (!identities || !Object.keys(identities).length) {
112
135
  throw new Error(
@@ -129,7 +152,10 @@ class OctaneCompilerManifestPlugin {
129
152
  throw new Error(
130
153
  `Octane compiler has no client entry for ${entryName}.`,
131
154
  );
132
- const entryChunks = new Set(entrypoint.chunks);
155
+ const entryChunks = new Set([
156
+ ...entrypoint.chunks,
157
+ ...containerChunks,
158
+ ]);
133
159
  for (const chunk of entrypoint.chunks) {
134
160
  for (const referenced of chunk.getAllReferencedChunks())
135
161
  entryChunks.add(referenced);
@@ -41,10 +41,15 @@ export function createOctaneCompilerPlugin(
41
41
  } = require('./compiler-manifest.cjs');
42
42
  // The application installs the Octane compiler; resolve it lazily so
43
43
  // descriptor readers never load it.
44
+ const appRequire = createRequire(
45
+ path.join(api.context.rootPath, 'package.json'),
46
+ );
44
47
  const {
45
48
  inferRspackEnvironment,
46
49
  OctaneRspackPlugin,
47
- }: typeof import('@octanejs/rspack-plugin') = require('@octanejs/rspack-plugin');
50
+ }: typeof import('@octanejs/rspack-plugin') = appRequire(
51
+ '@octanejs/rspack-plugin',
52
+ );
48
53
  api.modifyRsbuildConfig((config, { mergeRsbuildConfig }) =>
49
54
  mergeRsbuildConfig(config, {
50
55
  source: {
@@ -60,19 +65,6 @@ export function createOctaneCompilerPlugin(
60
65
  rsbuildEnvironment.name === options.workerEnvironmentName
61
66
  ? 'server'
62
67
  : inferRspackEnvironment(config.target);
63
- // Octane prepends '.tsrx' only when it is absent. Keep TypeScript
64
- // modules first so an extensionless './link' cannot select a
65
- // case-colliding Link.tsrx on case-insensitive file systems.
66
- config.resolve ??= {};
67
- const extensions = (config.resolve.extensions ?? []).filter(
68
- extension => extension !== '.tsrx',
69
- );
70
- extensions.splice(
71
- Math.max(extensions.indexOf('.ts'), extensions.indexOf('.tsx')) + 1,
72
- 0,
73
- '.tsrx',
74
- );
75
- config.resolve.extensions = extensions;
76
68
  config.plugins ??= [];
77
69
  config.plugins.push(
78
70
  new OctaneRspackPlugin({
@@ -81,7 +81,7 @@ export const rendererAdapter: NativeRendererAdapter<'octane'> =
81
81
  artifacts: compilerArtifacts,
82
82
  assertSupportedSource: assertOctaneEntrySource,
83
83
  create: {
84
- templates: { federation: true, workers: false },
84
+ templates: { federation: true, workers: true },
85
85
  dependencies: (profile, { typescriptVersion }) => ({
86
86
  frameworkDependencies: [
87
87
  '@modern-js/renderer-core',
@@ -89,7 +89,9 @@ export const rendererAdapter: NativeRendererAdapter<'octane'> =
89
89
  ],
90
90
  dependencies: nativeRendererDependencies(profile),
91
91
  devDependencies: {
92
- [profile.compiler.name]: profile.compiler.version,
92
+ [profile.compiler.name]:
93
+ profile.dependencies[profile.compiler.name] ??
94
+ profile.compiler.version,
93
95
  typescript: typescriptVersion,
94
96
  },
95
97
  typecheckCommand: 'octane-tsc --noEmit --project tsconfig.json',
@@ -4,7 +4,7 @@ export const octaneProfile: RendererBuildProfile<'octane'> = {
4
4
  renderer: 'octane',
5
5
  status: 'preview',
6
6
  protocolVersion: 1,
7
- minimumNode: '26.10.0',
7
+ minimumNode: '26.12.0',
8
8
  hmr: {
9
9
  editedBoundary: 'may-reset',
10
10
  unaffectedComponents: 'preserved',
@@ -12,28 +12,32 @@ export const octaneProfile: RendererBuildProfile<'octane'> = {
12
12
  roots: 'single',
13
13
  cleanup: 'exactly-once',
14
14
  },
15
- compiler: { name: '@octanejs/rspack-plugin', version: '0.1.55' },
15
+ compiler: { name: '@octanejs/rspack-plugin', version: '0.2.4' },
16
16
  hydration: {
17
17
  name: 'octane',
18
- version: '0.7.1+ultramodern.1331985ea3b0',
18
+ version: '0.13.8',
19
19
  },
20
20
  router: {
21
21
  name: '@octanejs/tanstack-router',
22
- version: '0.1.60+ultramodern.0b9f76ee3003',
22
+ version: '0.2.10',
23
23
  coreName: '@tanstack/router-core',
24
- coreVersion: '1.171.15',
24
+ coreVersion: '1.171.38',
25
25
  },
26
26
  sourceExtensions: ['.tsx', '.tsrx', '.ts', '.js'],
27
27
  jsxImportSource: 'octane',
28
28
  dependencies: {
29
+ '@tsrx/oxc': '0.20.0',
29
30
  octane:
30
- 'https://github.com/bleedingdev/octane/releases/download/octane%400.7.1%2Bultramodern.1331985ea3b0/octane-0.7.1%2Bultramodern.1331985ea3b0.tgz',
31
+ 'https://github.com/BleedingDev/octane/releases/download/ultramodern-octane-0bf2313bbdfb/octane-0.13.8.tgz',
31
32
  '@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',
33
+ 'https://github.com/BleedingDev/octane/releases/download/ultramodern-octane-0bf2313bbdfb/octanejs-tanstack-router-0.2.10.tgz',
34
+ '@octanejs/rspack-plugin':
35
+ 'https://github.com/BleedingDev/octane/releases/download/ultramodern-octane-0bf2313bbdfb/octanejs-rspack-plugin-0.2.4.tgz',
36
+ '@tanstack/router-core':
37
+ 'https://github.com/BleedingDev/router/releases/download/ultramodern-router-core-585d3233f7f5/tanstack-router-core-1.171.38.tgz',
34
38
  '@modern-js/renderer-octane': '3.8.3',
35
- seroval: '1.6.8',
36
- 'seroval-plugins': '1.6.8',
39
+ seroval: '1.6.9',
40
+ 'seroval-plugins': '1.6.9',
37
41
  },
38
42
  capabilities: {
39
43
  worker: true,
@@ -64,7 +64,7 @@ function waitForCommittedMatches(
64
64
  signal?: AbortSignal,
65
65
  ): Promise<void> {
66
66
  signal?.throwIfAborted();
67
- const matchesId = router.stores.matchesId;
67
+ const matchesId = router.stores.ids;
68
68
  if (matchesId.get().length > 0) return Promise.resolve();
69
69
  if (!isReactiveMatchIds(matchesId)) {
70
70
  throw new Error(
@@ -0,0 +1,306 @@
1
+ import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions/paths';
2
+ import {
3
+ type AnyRouter,
4
+ assertDefaultRouterRenderHost,
5
+ associateRouterRenderProfile,
6
+ captureRouterRenderSnapshot,
7
+ createRouterRenderRouteTree,
8
+ defaultParseSearch,
9
+ defaultStringifySearch,
10
+ disposeRouterRenderSnapshot,
11
+ getRouterRenderProfile,
12
+ matchContext,
13
+ type RouterRenderJsonRecord,
14
+ type RouterRenderSnapshot,
15
+ registerRouterRenderProfile,
16
+ restoreRouterRenderSnapshot,
17
+ routerContext,
18
+ } from '@octanejs/tanstack-router';
19
+ import {
20
+ getOctaneWorkerRouterRewrite,
21
+ OCTANE_WORKER_CONTEXT_KEYS,
22
+ registerOctaneWorkerContext,
23
+ } from './worker-federation-contexts';
24
+
25
+ export const OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE = Object.freeze({
26
+ key: 'ultramodern.file-routes',
27
+ version: '1',
28
+ });
29
+
30
+ interface GeneratedRouterOrigin {
31
+ readonly publicContext: RouterRenderJsonRecord;
32
+ readonly rewrite: AnyRouter['options']['rewrite'];
33
+ }
34
+ const origins = new WeakMap<AnyRouter, GeneratedRouterOrigin>();
35
+ let profileRegistered = false;
36
+
37
+ function plainRecord(value: unknown): value is Record<string, unknown> {
38
+ return (
39
+ value !== null &&
40
+ typeof value === 'object' &&
41
+ [Object.prototype, null].includes(Object.getPrototypeOf(value))
42
+ );
43
+ }
44
+
45
+ function record(
46
+ value: unknown,
47
+ fields?: readonly string[],
48
+ ): Record<string, unknown> {
49
+ if (!plainRecord(value))
50
+ throw new TypeError('Worker router context requires a plain record');
51
+ for (const key of Reflect.ownKeys(value)) {
52
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
53
+ if (
54
+ typeof key !== 'string' ||
55
+ (fields && !fields.includes(key)) ||
56
+ !descriptor ||
57
+ !descriptor.enumerable ||
58
+ !('value' in descriptor)
59
+ )
60
+ throw new TypeError('Unknown Worker router context field or accessor');
61
+ }
62
+ return value;
63
+ }
64
+
65
+ function nonempty(value: unknown): string {
66
+ if (typeof value !== 'string' || !value.trim() || value.length > 1024)
67
+ throw new TypeError('Invalid Worker router identity');
68
+ return value;
69
+ }
70
+
71
+ /** Only the host entry's public logical identity crosses the Worker boundary. */
72
+ function publicContext(value: unknown): RouterRenderJsonRecord {
73
+ const context = record(value, ['ultramodern']);
74
+ const metadata = record(context.ultramodern, ['rendererIdentity']);
75
+ const identity = record(metadata.rendererIdentity, [
76
+ 'renderer',
77
+ 'appId',
78
+ 'entryName',
79
+ 'protocolVersion',
80
+ 'buildId',
81
+ ]);
82
+ if (identity.renderer !== 'octane' || identity.protocolVersion !== 1)
83
+ throw new TypeError('Unsupported Worker router identity');
84
+ return Object.freeze({
85
+ ultramodern: Object.freeze({
86
+ rendererIdentity: Object.freeze({
87
+ renderer: 'octane',
88
+ appId: nonempty(identity.appId),
89
+ entryName: nonempty(identity.entryName),
90
+ protocolVersion: 1,
91
+ buildId: nonempty(identity.buildId),
92
+ }),
93
+ }),
94
+ });
95
+ }
96
+
97
+ function localizedUrls(value: unknown): LocalisedUrlsOption | undefined {
98
+ if (value === undefined || typeof value === 'boolean') return value;
99
+ const input = record(value);
100
+ const result: Exclude<LocalisedUrlsOption, boolean> = {};
101
+ for (const [path, translations] of Object.entries(input)) {
102
+ const names: Record<string, string> = {};
103
+ for (const [language, target] of Object.entries(record(translations)))
104
+ names[nonempty(language)] = nonempty(target);
105
+ result[path] = names;
106
+ }
107
+ return result;
108
+ }
109
+
110
+ function rewriteDescriptor(value: unknown):
111
+ | {
112
+ readonly language: string;
113
+ readonly languages: readonly string[];
114
+ readonly localisedUrls?: LocalisedUrlsOption;
115
+ }
116
+ | undefined {
117
+ if (value === undefined) return undefined;
118
+ const input = record(value, ['language', 'languages', 'localisedUrls']);
119
+ if (
120
+ !Array.isArray(input.languages) ||
121
+ !input.languages.length ||
122
+ input.languages.length > 64
123
+ )
124
+ throw new TypeError('Invalid Worker router i18n languages');
125
+ const languages = input.languages.map(nonempty);
126
+ const language = nonempty(input.language);
127
+ if (
128
+ new Set(languages).size !== languages.length ||
129
+ !languages.includes(language)
130
+ )
131
+ throw new TypeError('Invalid Worker router i18n language');
132
+ const urls = localizedUrls(input.localisedUrls);
133
+ return {
134
+ language,
135
+ languages,
136
+ ...(urls === undefined ? {} : { localisedUrls: urls }),
137
+ };
138
+ }
139
+
140
+ function nativeRewriteDescriptor(
141
+ router: AnyRouter,
142
+ ): RouterRenderJsonRecord | undefined {
143
+ if (!router.options.rewrite) return undefined;
144
+ const known = getOctaneWorkerRouterRewrite()?.capture(router.options.rewrite);
145
+ if (!known)
146
+ throw new TypeError(
147
+ 'Custom router rewrite requires an exact Worker profile',
148
+ );
149
+ const descriptor = rewriteDescriptor(known);
150
+ if (!descriptor)
151
+ throw new TypeError('Missing native router rewrite provenance');
152
+ return {
153
+ language: descriptor.language,
154
+ languages: [...descriptor.languages],
155
+ ...(descriptor.localisedUrls === undefined
156
+ ? {}
157
+ : { localisedUrls: descriptor.localisedUrls }),
158
+ };
159
+ }
160
+
161
+ function validateDefaultSnapshot(snapshot: RouterRenderSnapshot): void {
162
+ publicContext(snapshot.publicContext);
163
+ rewriteDescriptor(snapshot.i18n);
164
+ }
165
+
166
+ function ensureDefaultProfile(): void {
167
+ if (profileRegistered) return;
168
+ registerRouterRenderProfile({
169
+ id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
170
+ assertHost: (router, snapshot) => {
171
+ const origin = origins.get(router);
172
+ if (
173
+ !origin ||
174
+ origin.rewrite !== router.options.rewrite ||
175
+ JSON.stringify(publicContext(router.options.context)) !==
176
+ JSON.stringify(origin.publicContext)
177
+ )
178
+ throw new TypeError('Generated Worker router origin changed');
179
+ assertDefaultRouterRenderHost(router, snapshot.i18n !== undefined);
180
+ validateDefaultSnapshot(snapshot);
181
+ if (
182
+ JSON.stringify(nativeRewriteDescriptor(router)) !==
183
+ JSON.stringify(snapshot.i18n)
184
+ )
185
+ throw new TypeError(
186
+ 'Worker router rewrite differs from its native provenance',
187
+ );
188
+ },
189
+ createRoutingTree: snapshot => {
190
+ validateDefaultSnapshot(snapshot);
191
+ return createRouterRenderRouteTree(snapshot.routes);
192
+ },
193
+ routingOptions: snapshot => {
194
+ const defaults = {
195
+ parseSearch: defaultParseSearch,
196
+ stringifySearch: defaultStringifySearch,
197
+ };
198
+ const i18n = rewriteDescriptor(snapshot.i18n);
199
+ if (!i18n) return defaults;
200
+ const codec = getOctaneWorkerRouterRewrite();
201
+ if (!codec)
202
+ throw new TypeError('Missing owning Worker router i18n codec');
203
+ return { ...defaults, rewrite: codec.restore(i18n) };
204
+ },
205
+ });
206
+ profileRegistered = true;
207
+ }
208
+
209
+ /** Associate the genuine router produced by the generated file-route factory. */
210
+ export function associateOctaneFileRouteSnapshot(router: AnyRouter): void {
211
+ ensureDefaultProfile();
212
+ const context = publicContext(router.options.context);
213
+ origins.set(router, {
214
+ publicContext: context,
215
+ rewrite: router.options.rewrite,
216
+ });
217
+ associateRouterRenderProfile(router, {
218
+ id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
219
+ publicContext: context,
220
+ });
221
+ }
222
+
223
+ export function captureOctaneRouterSnapshot(
224
+ router: AnyRouter,
225
+ ): RouterRenderSnapshot {
226
+ const origin = origins.get(router);
227
+ const declared = getRouterRenderProfile(router);
228
+ if (
229
+ origin &&
230
+ declared?.key === OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE.key &&
231
+ declared.version === OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE.version
232
+ ) {
233
+ const i18n = nativeRewriteDescriptor(router);
234
+ associateRouterRenderProfile(router, {
235
+ id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
236
+ publicContext: origin.publicContext,
237
+ ...(i18n === undefined ? {} : { i18n }),
238
+ });
239
+ }
240
+ return captureRouterRenderSnapshot(router);
241
+ }
242
+
243
+ let registrations = 0;
244
+ let releaseContexts: (() => void) | undefined;
245
+
246
+ /** Startup registration uses the actual physical native router/match Contexts. */
247
+ export function registerOctaneRouterWorkerContexts(): () => void {
248
+ ensureDefaultProfile();
249
+ if (!releaseContexts) {
250
+ const releaseRouter = registerOctaneWorkerContext(routerContext, {
251
+ key: OCTANE_WORKER_CONTEXT_KEYS.router,
252
+ encode: value => {
253
+ if (!value) throw new TypeError('Missing native router provider');
254
+ return captureOctaneRouterSnapshot(value);
255
+ },
256
+ decode: (value, signal) => restoreRouterRenderSnapshot(value, { signal }),
257
+ dispose: value => {
258
+ if (value) disposeRouterRenderSnapshot(value);
259
+ },
260
+ isProvided: value => value !== undefined,
261
+ });
262
+ try {
263
+ const releaseMatch = registerOctaneWorkerContext(matchContext, {
264
+ key: OCTANE_WORKER_CONTEXT_KEYS.match,
265
+ encode: nonempty,
266
+ decode: nonempty,
267
+ validate: (value, readContext) => {
268
+ const selected = readContext(routerContext);
269
+ const router = selected.present ? selected.value : undefined;
270
+ // Native Match and useMatch share the active route store key, not
271
+ // the location-specific match identity (which includes parameters).
272
+ const current = router?.state.matches.find(
273
+ match => match.routeId === value,
274
+ );
275
+ if (
276
+ !router ||
277
+ !current ||
278
+ typeof value !== 'string' ||
279
+ router.stores.getMatchStore(value).get() !== current
280
+ )
281
+ throw new TypeError(
282
+ 'Worker match context requires its selected native router and a current match',
283
+ );
284
+ },
285
+ isProvided: value => value !== undefined,
286
+ });
287
+ releaseContexts = () => {
288
+ releaseMatch();
289
+ releaseRouter();
290
+ };
291
+ } catch (error) {
292
+ releaseRouter();
293
+ throw error;
294
+ }
295
+ }
296
+ registrations++;
297
+ let released = false;
298
+ return () => {
299
+ if (released) return;
300
+ released = true;
301
+ if (--registrations === 0) {
302
+ releaseContexts?.();
303
+ releaseContexts = undefined;
304
+ }
305
+ };
306
+ }
@@ -91,8 +91,9 @@ function requestHeaders(router: AnyRouter): Headers {
91
91
  );
92
92
  appendHeaders(headers, field?.value);
93
93
  }
94
- const redirect = router.stores.redirect.get();
95
- if (redirect) appendHeaders(headers, redirect.headers);
94
+ const result = router._serverResult;
95
+ if (result?.type === 'redirect')
96
+ appendHeaders(headers, result.redirect.headers);
96
97
  return headers;
97
98
  }
98
99
 
@@ -117,7 +118,6 @@ export function createOctaneRequestHandler<TRouter extends AnyRouter>({
117
118
  ...serialization,
118
119
  forbiddenValues: [request, ...(serialization?.forbiddenValues ?? [])],
119
120
  });
120
- session.registerCleanup(() => router.cancelMatches());
121
121
 
122
122
  const { url } = getNormalizedURL(request.url, 'http://localhost');
123
123
  const href = url.href.replace(url.origin, '');
@@ -126,14 +126,14 @@ export function createOctaneRequestHandler<TRouter extends AnyRouter>({
126
126
  history: createMemoryHistory({ initialEntries: [href] }),
127
127
  origin: router.options.origin ?? getOrigin(request),
128
128
  });
129
- await withAbort(router.load(), session.signal);
129
+ await withAbort(router.load({ _signal: session.signal }), session.signal);
130
130
  guard.assertActive();
131
131
 
132
132
  // A native redirect is a terminal HTTP Response. It must never enter
133
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);
134
+ const loadResult = router._serverResult;
135
+ if (loadResult?.type === 'redirect') {
136
+ const resolved = loadResult.redirect;
137
137
  const headers = requestHeaders(router);
138
138
  return new Response(null, {
139
139
  status: resolved.status,
@@ -146,7 +146,10 @@ export function createOctaneRequestHandler<TRouter extends AnyRouter>({
146
146
  const serverSsr = router.serverSsr;
147
147
  if (!serverSsr)
148
148
  throw new Error('Native Octane router SSR state is required');
149
- await withAbort(serverSsr.dehydrate(), session.signal);
149
+ await withAbort(
150
+ serverSsr.dehydrate({ signal: session.signal }),
151
+ session.signal,
152
+ );
150
153
  guard.assertActive();
151
154
  const responseHeaders = requestHeaders(router);
152
155
  // A callback that ignores cancellation must not hold the router SSR
@@ -5,8 +5,6 @@ import { createElement, type ServerRenderNode } from 'octane/server';
5
5
  export * from '@octanejs/tanstack-router/ssr/server';
6
6
  export type { OctaneRequestHandlerOptions } from './router-handler';
7
7
  export { createOctaneRequestHandler } from './router-handler';
8
- export type { OctaneRouterInjectionOptions } from './router-injection';
9
- export { createOctaneRouterInjection } from './router-injection';
10
8
  export type {
11
9
  OctaneRouterSerializationGuard,
12
10
  OctaneRouterSerializationOptions,
package/src/router.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  useMatch,
10
10
  useRouter,
11
11
  } from '@octanejs/tanstack-router';
12
- import { createSubSlot, useCallback, useEffect, useRef } from 'octane';
12
+ import { createSubSlot, useCallback, useLayoutEffect, useRef } from 'octane';
13
13
  import { assertOctaneIdentity } from './bootstrap';
14
14
  import { resolveRouteData } from './routes';
15
15
 
@@ -161,7 +161,7 @@ export function useOctaneRouteAction(...args: unknown[]) {
161
161
  const identity = useApplicationIdentity();
162
162
  const routeId = useApplicationRouteId();
163
163
  const owner = useRef<AbortController | null>(null, actionSlot(slot, 'owner'));
164
- useEffect(
164
+ useLayoutEffect(
165
165
  () => {
166
166
  const controller = new AbortController();
167
167
  owner.current = controller;
package/src/routes.ts CHANGED
@@ -59,7 +59,7 @@ export function resolveRouteData(
59
59
  }
60
60
 
61
61
  interface ReactiveNativeMatch {
62
- get(): AnyRouteMatch;
62
+ get(): AnyRouteMatch | undefined;
63
63
  subscribe(callback: () => void): { unsubscribe(): void };
64
64
  }
65
65
 
@@ -75,24 +75,18 @@ function isReactiveNativeMatch(value: unknown): value is ReactiveNativeMatch {
75
75
  /** A transport failure belongs to the native generation that fetched it. */
76
76
  function observeRouteCompletion(
77
77
  router: AnyRouter,
78
+ routeId: string,
78
79
  controller: AbortController,
79
80
  signal: AbortSignal,
80
81
  completion: Promise<void>,
81
82
  ) {
82
83
  void completion.catch(error => {
83
84
  if (signal.aborted) return;
84
- const match = [
85
- ...router.stores.pendingMatches.get(),
86
- ...router.stores.matches.get(),
87
- ...router.stores.cachedMatches.get(),
88
- ].find(candidate => candidate.abortController === controller);
89
- if (!match) return;
90
- const store = [
91
- router.stores.pendingMatchStores.get(match.id),
92
- router.stores.matchStores.get(match.id),
93
- router.stores.cachedMatchStores.get(match.id),
94
- ].find(candidate => candidate?.get().abortController === controller);
95
- if (!store) return;
85
+ const route = Object.values(router.routesById).find(
86
+ candidate => candidate.options.staticData?.ultramodernRouteId === routeId,
87
+ );
88
+ if (!route) return;
89
+ const store = router.stores.getMatchStore(route.id);
96
90
  let subscription: { unsubscribe(): void } | undefined;
97
91
  let settled = false;
98
92
  const cleanup = () => {
@@ -103,9 +97,10 @@ function observeRouteCompletion(
103
97
  settled = true;
104
98
  cleanup();
105
99
  };
106
- const publish = () => {
100
+ const publish = async () => {
107
101
  if (settled) return;
108
102
  const current = store.get();
103
+ if (!current) return;
109
104
  if (
110
105
  current.abortController !== controller ||
111
106
  signal.aborted ||
@@ -121,23 +116,29 @@ function observeRouteCompletion(
121
116
  const route = router.routesById[current.routeId];
122
117
  let failure = error;
123
118
  try {
119
+ await route?.options.errorComponent?.preload?.();
120
+ if (signal.aborted || store.get()?.abortController !== controller)
121
+ return;
124
122
  route?.options.onError?.(failure);
125
123
  } catch (onErrorFailure) {
126
124
  failure = onErrorFailure;
127
125
  }
128
- // Native successful loads have already preloaded their route components.
126
+ // The error component is ready before publishing a late transport failure.
129
127
  if (signal.aborted) return;
130
- router.updateMatch(match.id, previous =>
131
- previous.abortController === controller &&
132
- previous.status === 'success' &&
133
- !previous.isFetching
134
- ? {
135
- ...previous,
136
- error: failure,
137
- status: 'error',
138
- isFetching: false,
139
- }
140
- : previous,
128
+ router.stores.setMatches(
129
+ router.stores.matches.get().map(previous =>
130
+ previous.routeId === route.id &&
131
+ previous.abortController === controller &&
132
+ previous.status === 'success' &&
133
+ !previous.isFetching
134
+ ? {
135
+ ...previous,
136
+ error: failure,
137
+ status: 'error',
138
+ isFetching: false,
139
+ }
140
+ : previous,
141
+ ),
141
142
  );
142
143
  };
143
144
  signal.addEventListener('abort', onAbort, { once: true });
@@ -203,6 +204,7 @@ export function createFileSystemRouteTree<Context = unknown>(
203
204
  if (failed) throw failure;
204
205
  observeRouteCompletion(
205
206
  router,
207
+ route.id,
206
208
  match.abortController,
207
209
  signal,
208
210
  outcome.completion,