@octanejs/app-core 0.0.50 → 0.0.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/package.json +4 -4
- package/src/codegen.js +195 -1
- package/src/html.js +1 -0
- package/src/index.js +7 -1
- package/src/resolve-config.js +21 -0
- package/src/server/html-stream.js +9 -1
- package/src/server/html-template.js +55 -0
- package/src/server/production.js +165 -27
- package/src/server/request-context.js +17 -5
- package/src/server/rpc.js +165 -29
- package/src/server/server-calls.js +132 -0
- package/src/server/server-entry.js +62 -5
- package/src/server/signal-owners.js +182 -0
- package/types/codegen.d.ts +21 -0
- package/types/html.d.ts +5 -0
- package/types/index.d.ts +54 -2
- package/types/production.d.ts +40 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { createContext, runMiddlewareChain } from './middleware.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Snapshot trusted middleware output only after this invocation was authorized.
|
|
6
|
+
* @param {import('@octanejs/app-core').Context} context
|
|
7
|
+
* @param {AbortSignal} [signal]
|
|
8
|
+
* @returns {import('octane/server').ServerCallContext}
|
|
9
|
+
*/
|
|
10
|
+
export function serverCallContext(context, signal = context.request.signal) {
|
|
11
|
+
return Object.freeze({
|
|
12
|
+
request: context.request,
|
|
13
|
+
signal,
|
|
14
|
+
viewer: context.viewer,
|
|
15
|
+
...(context.platform === undefined ? {} : { platform: context.platform }),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Each nested call gets a separate middleware context and async store. Reusing
|
|
21
|
+
* the parent's mutable `rpc` field would cross-authorize concurrent members.
|
|
22
|
+
* @param {import('@octanejs/app-core').Context} parent
|
|
23
|
+
* @param {Pick<import('@octanejs/app-core').RpcRequestOptions, 'asyncContext' | 'middlewares'> & { origin: string }} options
|
|
24
|
+
* @returns {import('octane/server').ServerCallHost}
|
|
25
|
+
*/
|
|
26
|
+
export function createServerCallHost(parent, options) {
|
|
27
|
+
return {
|
|
28
|
+
/**
|
|
29
|
+
* @template T
|
|
30
|
+
* @param {import('octane/server').ServerFunctionTarget} target
|
|
31
|
+
* @param {import('octane/server').ServerCallOptions} callOptions
|
|
32
|
+
* @param {(context: import('octane/server').ServerCallContext) => T} execute
|
|
33
|
+
* @returns {Promise<Awaited<T>>}
|
|
34
|
+
*/
|
|
35
|
+
async invoke(target, callOptions, execute) {
|
|
36
|
+
const signal =
|
|
37
|
+
callOptions.signal === undefined
|
|
38
|
+
? parent.request.signal
|
|
39
|
+
: AbortSignal.any([parent.request.signal, callOptions.signal]);
|
|
40
|
+
signal.throwIfAborted();
|
|
41
|
+
const context = createContext(parent.request, { ...parent.params }, parent.platform);
|
|
42
|
+
context.state = new Map(parent.state);
|
|
43
|
+
context.viewer = parent.viewer;
|
|
44
|
+
context.rpc = Object.freeze({ ...target });
|
|
45
|
+
const store = {
|
|
46
|
+
signalRequestContext:
|
|
47
|
+
options.asyncContext.getStore()?.signalRequestContext ??
|
|
48
|
+
options.asyncContext.getStore()?.context ??
|
|
49
|
+
parent,
|
|
50
|
+
origin: options.origin,
|
|
51
|
+
context,
|
|
52
|
+
platform: parent.platform,
|
|
53
|
+
serverCallHost: createServerCallHost(context, options),
|
|
54
|
+
};
|
|
55
|
+
return await options.asyncContext.run(store, async () => {
|
|
56
|
+
let invoked = false;
|
|
57
|
+
/** @type {{ value: Awaited<T> } | undefined} */
|
|
58
|
+
let result;
|
|
59
|
+
const response = await runMiddlewareChain(
|
|
60
|
+
context,
|
|
61
|
+
options.middlewares ?? [],
|
|
62
|
+
[],
|
|
63
|
+
async () => {
|
|
64
|
+
signal.throwIfAborted();
|
|
65
|
+
invoked = true;
|
|
66
|
+
result = { value: await execute(serverCallContext(context, signal)) };
|
|
67
|
+
return new Response(null, { status: 204 });
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
if (!invoked || !response.ok || result === undefined) {
|
|
71
|
+
throw Object.assign(new Error('Server function call was rejected by middleware'), {
|
|
72
|
+
code: 'OCTANE_SERVER_CALL_REJECTED',
|
|
73
|
+
status: response.ok ? 403 : response.status,
|
|
74
|
+
invoked,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const value = result.value;
|
|
78
|
+
// Async generators execute on next(), not when their function returns.
|
|
79
|
+
// Keep those later pulls in this invocation's authorized request store.
|
|
80
|
+
if (value !== null && typeof value === 'object' && Symbol.asyncIterator in value) {
|
|
81
|
+
const iterable = /** @type {AsyncIterable<unknown>} */ (value);
|
|
82
|
+
return /** @type {typeof value} */ ({
|
|
83
|
+
[Symbol.asyncIterator]() {
|
|
84
|
+
const iterator = Promise.resolve(
|
|
85
|
+
options.asyncContext.run(store, () => iterable[Symbol.asyncIterator]()),
|
|
86
|
+
);
|
|
87
|
+
let closed = false;
|
|
88
|
+
const close = () => {
|
|
89
|
+
if (closed) return Promise.resolve({ done: true, value: undefined });
|
|
90
|
+
closed = true;
|
|
91
|
+
signal.removeEventListener('abort', abort);
|
|
92
|
+
return iterator.then((producer) =>
|
|
93
|
+
options.asyncContext.run(
|
|
94
|
+
store,
|
|
95
|
+
() => producer.return?.() ?? { done: true, value: undefined },
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
const abort = () => {
|
|
100
|
+
void close().catch(() => {});
|
|
101
|
+
};
|
|
102
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
103
|
+
return {
|
|
104
|
+
async next() {
|
|
105
|
+
try {
|
|
106
|
+
signal.throwIfAborted();
|
|
107
|
+
if (closed) return { done: true, value: undefined };
|
|
108
|
+
const producer = await iterator;
|
|
109
|
+
if (closed) return { done: true, value: undefined };
|
|
110
|
+
const result = await options.asyncContext.run(store, () => producer.next());
|
|
111
|
+
signal.throwIfAborted();
|
|
112
|
+
if (closed) return { done: true, value: undefined };
|
|
113
|
+
if (result.done) {
|
|
114
|
+
closed = true;
|
|
115
|
+
signal.removeEventListener('abort', abort);
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
void close().catch(() => {});
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
return: close,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
/** @import { Route, RootBoundaryOptions } from '@octanejs/app-core' */
|
|
32
|
-
/** @import { ClientAssetEntry } from '
|
|
32
|
+
/** @import { ClientAssetEntry } from '@octanejs/app-core/production' */
|
|
33
33
|
|
|
34
34
|
import { get_route_entry_export_name, get_route_entry_path } from '../routes.js';
|
|
35
35
|
|
|
@@ -41,6 +41,10 @@ import { get_route_entry_export_name, get_route_entry_path } from '../routes.js'
|
|
|
41
41
|
* @property {string[]} [rpcModulePaths] - Project-root module IDs containing `module server`
|
|
42
42
|
* @property {Record<string, ClientAssetEntry>} [clientAssetMap] - Route entry path → built client asset paths
|
|
43
43
|
* @property {string} [clientAssetMapFile] - JSON asset map, resolved beside the built server entry at runtime
|
|
44
|
+
* @property {import('@octanejs/app-core/production').ClientBuildManifest} [clientBuild] - Completed client compilation identity
|
|
45
|
+
* @property {string} [clientBuildFile] - Required completed client metadata, resolved beside the server entry
|
|
46
|
+
* @property {Record<string, unknown>} [independentHydrationManifest] - Completed client-build independent Hydrate manifest
|
|
47
|
+
* @property {string} [independentHydrationManifestFile] - Optional JSON manifest resolved beside the built server entry
|
|
44
48
|
* @property {Record<string, string>} [moduleImports] - Stable module ID → bundler import specifier
|
|
45
49
|
* @property {((id: string) => string)} [resolveImport] - Fallback module-specifier mapper
|
|
46
50
|
* @property {string} [configImportPath] - Bundler import specifier for octane.config.ts
|
|
@@ -65,6 +69,10 @@ export function generateServerEntry(options) {
|
|
|
65
69
|
rpcModulePaths = [],
|
|
66
70
|
clientAssetMap = {},
|
|
67
71
|
clientAssetMapFile,
|
|
72
|
+
clientBuild,
|
|
73
|
+
clientBuildFile,
|
|
74
|
+
independentHydrationManifest,
|
|
75
|
+
independentHydrationManifestFile,
|
|
68
76
|
moduleImports = {},
|
|
69
77
|
resolveImport,
|
|
70
78
|
configImportPath,
|
|
@@ -168,10 +176,29 @@ export function generateServerEntry(options) {
|
|
|
168
176
|
return `\t${JSON.stringify(modulePath)}: ${varName}._$_server_$_,`;
|
|
169
177
|
})
|
|
170
178
|
.join('\n');
|
|
179
|
+
const independentHydrationSource = independentHydrationManifestFile
|
|
180
|
+
? `(() => {
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(readFileSync(join(__dirname, ${JSON.stringify(independentHydrationManifestFile)}), 'utf-8'));
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error && typeof error === 'object' && error.code === 'ENOENT') return null;
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
})()`
|
|
188
|
+
: JSON.stringify(independentHydrationManifest ?? null, null, '\t');
|
|
189
|
+
const clientBuildSource = clientBuildFile
|
|
190
|
+
? `(() => {
|
|
191
|
+
const value = JSON.parse(readFileSync(join(__dirname, ${JSON.stringify(clientBuildFile)}), 'utf-8'));
|
|
192
|
+
if (value == null) throw new Error('[octane] Completed client build metadata is required.');
|
|
193
|
+
return value;
|
|
194
|
+
})()`
|
|
195
|
+
: JSON.stringify(clientBuild ?? null, null, '\t');
|
|
171
196
|
|
|
172
197
|
if (mode === 'manifest' || mode === 'webworker') {
|
|
173
198
|
const isWebWorker = mode === 'webworker';
|
|
174
|
-
const readsAssetFile =
|
|
199
|
+
const readsAssetFile =
|
|
200
|
+
!isWebWorker &&
|
|
201
|
+
Boolean(clientAssetMapFile || independentHydrationManifestFile || clientBuildFile);
|
|
175
202
|
const assetFileImports = readsAssetFile
|
|
176
203
|
? `import { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n`
|
|
177
204
|
: '';
|
|
@@ -185,7 +212,9 @@ export function generateServerEntry(options) {
|
|
|
185
212
|
? `const __dirname = dirname(fileURLToPath(import.meta.url));\n`
|
|
186
213
|
: '';
|
|
187
214
|
const clientAssets = readsAssetFile
|
|
188
|
-
?
|
|
215
|
+
? clientAssetMapFile
|
|
216
|
+
? `JSON.parse(readFileSync(join(__dirname, ${JSON.stringify(clientAssetMapFile)}), 'utf-8'))`
|
|
217
|
+
: JSON.stringify(clientAssetMap, null, '\t')
|
|
189
218
|
: JSON.stringify(clientAssetMap, null, '\t');
|
|
190
219
|
const runtime = isWebWorker
|
|
191
220
|
? `const runtime = octaneConfig.adapter?.runtime;
|
|
@@ -203,9 +232,15 @@ if (!runtime) {
|
|
|
203
232
|
};`;
|
|
204
233
|
const workerFactory = isWebWorker
|
|
205
234
|
? `
|
|
206
|
-
export function createWebWorkerHandler({
|
|
235
|
+
export function createWebWorkerHandler({
|
|
236
|
+
htmlTemplate,
|
|
237
|
+
clientAssets = manifest.clientAssets,
|
|
238
|
+
clientBuild = manifest.clientBuild,
|
|
239
|
+
independentHydration = manifest.independentHydration,
|
|
240
|
+
}) {
|
|
241
|
+
if (!clientBuild) throw new Error('[octane] Completed client build metadata is required.');
|
|
207
242
|
return createHandler(
|
|
208
|
-
{ ...manifest, clientAssets },
|
|
243
|
+
{ ...manifest, clientAssets, clientBuild, independentHydration },
|
|
209
244
|
{ ...rendererDeps, htmlTemplate },
|
|
210
245
|
);
|
|
211
246
|
}
|
|
@@ -222,6 +257,10 @@ export function createWebWorkerHandler({ htmlTemplate, clientAssets = manifest.c
|
|
|
222
257
|
${assetFileImports}${platformImports}import {
|
|
223
258
|
renderToReadableStream,
|
|
224
259
|
executeServerFunction,
|
|
260
|
+
executeServerFunctionStream,
|
|
261
|
+
executeServerFunctionBatch,
|
|
262
|
+
installSignalOwnerEnvironment,
|
|
263
|
+
retireSignalOwnerIdentity,
|
|
225
264
|
Suspense,
|
|
226
265
|
ErrorBoundary,
|
|
227
266
|
createElement,
|
|
@@ -270,6 +309,8 @@ ${rpc_entries}
|
|
|
270
309
|
};
|
|
271
310
|
|
|
272
311
|
${assetDirectory}const clientAssets = ${clientAssets};
|
|
312
|
+
const clientBuild = ${isWebWorker && clientBuildFile ? 'null' : clientBuildSource};
|
|
313
|
+
const independentHydration = ${isWebWorker && independentHydrationManifestFile ? 'null' : independentHydrationSource};
|
|
273
314
|
|
|
274
315
|
export const manifest = {
|
|
275
316
|
routes: octaneConfig.router.routes,
|
|
@@ -285,12 +326,17 @@ export const manifest = {
|
|
|
285
326
|
rpcModules,
|
|
286
327
|
runtime,
|
|
287
328
|
clientAssets,
|
|
329
|
+
clientBuild,
|
|
330
|
+
independentHydration,
|
|
288
331
|
};
|
|
289
332
|
|
|
290
333
|
export const rendererDeps = {
|
|
291
334
|
renderToReadableStream,
|
|
292
335
|
prerender,
|
|
293
336
|
executeServerFunction,
|
|
337
|
+
streamServerFunction: executeServerFunctionStream,
|
|
338
|
+
batchServerFunctions: executeServerFunctionBatch,
|
|
339
|
+
signalOwners: { install: installSignalOwnerEnvironment, retire: retireSignalOwnerIdentity },
|
|
294
340
|
Suspense,
|
|
295
341
|
ErrorBoundary,
|
|
296
342
|
createElement,
|
|
@@ -312,6 +358,10 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
312
358
|
import {
|
|
313
359
|
renderToReadableStream,
|
|
314
360
|
executeServerFunction,
|
|
361
|
+
executeServerFunctionStream,
|
|
362
|
+
executeServerFunctionBatch,
|
|
363
|
+
installSignalOwnerEnvironment,
|
|
364
|
+
retireSignalOwnerIdentity,
|
|
315
365
|
Suspense,
|
|
316
366
|
ErrorBoundary,
|
|
317
367
|
createElement,
|
|
@@ -380,6 +430,8 @@ const clientAssets = ${
|
|
|
380
430
|
? `JSON.parse(readFileSync(join(__dirname, ${JSON.stringify(clientAssetMapFile)}), 'utf-8'))`
|
|
381
431
|
: JSON.stringify(clientAssetMap, null, '\t')
|
|
382
432
|
};
|
|
433
|
+
const independentHydration = ${independentHydrationSource};
|
|
434
|
+
const clientBuild = ${clientBuildSource};
|
|
383
435
|
|
|
384
436
|
export const handler = createHandler(
|
|
385
437
|
{
|
|
@@ -396,12 +448,17 @@ export const handler = createHandler(
|
|
|
396
448
|
rpcModules,
|
|
397
449
|
runtime,
|
|
398
450
|
clientAssets,
|
|
451
|
+
clientBuild,
|
|
452
|
+
independentHydration,
|
|
399
453
|
},
|
|
400
454
|
{
|
|
401
455
|
renderToReadableStream,
|
|
402
456
|
prerender,
|
|
403
457
|
htmlTemplate,
|
|
404
458
|
executeServerFunction,
|
|
459
|
+
streamServerFunction: executeServerFunctionStream,
|
|
460
|
+
batchServerFunctions: executeServerFunctionBatch,
|
|
461
|
+
signalOwners: { install: installSignalOwnerEnvironment, retire: retireSignalOwnerIdentity },
|
|
405
462
|
Suspense,
|
|
406
463
|
ErrorBoundary,
|
|
407
464
|
createElement,
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @typedef {import('@octanejs/app-core').RpcRequestOptions['asyncContext']} RequestStorage */
|
|
3
|
+
/** @typedef {NonNullable<ReturnType<RequestStorage['getStore']>>} RequestStore */
|
|
4
|
+
/** @typedef {import('@octanejs/app-core').SignalRequestHooks} SignalRequestHooks */
|
|
5
|
+
|
|
6
|
+
/** @type {WeakMap<object, WeakSet<SignalRequestHooks['install']>>} */
|
|
7
|
+
const installed = new WeakMap();
|
|
8
|
+
/** @type {WeakMap<object, import('octane/server').SignalOwner>} */
|
|
9
|
+
const owners = new WeakMap();
|
|
10
|
+
const retired = new WeakSet();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Signal reads are synchronous even though the adapter's broad run() type also
|
|
14
|
+
* permits an async wrapper. Capture the callback's actual result, and fail
|
|
15
|
+
* clearly (without invoking delayed work) if the provider defers entry itself.
|
|
16
|
+
* @template T
|
|
17
|
+
* @param {RequestStorage} storage
|
|
18
|
+
* @param {RequestStore} store
|
|
19
|
+
* @param {() => T} callback
|
|
20
|
+
* @returns {T}
|
|
21
|
+
*/
|
|
22
|
+
function runSynchronous(storage, store, callback) {
|
|
23
|
+
/** @type {{ value: T } | { error: unknown } | undefined} */
|
|
24
|
+
let outcome;
|
|
25
|
+
let entering = true;
|
|
26
|
+
try {
|
|
27
|
+
const pending = storage.run(store, () => {
|
|
28
|
+
if (!entering) return;
|
|
29
|
+
try {
|
|
30
|
+
outcome = { value: callback() };
|
|
31
|
+
} catch (error) {
|
|
32
|
+
outcome = { error };
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
// Callback exceptions are preserved in outcome; an async context wrapper
|
|
36
|
+
// must not leave an unobserved promise when synchronous entry is rejected.
|
|
37
|
+
if (pending !== undefined) void Promise.resolve(pending).catch(() => {});
|
|
38
|
+
} finally {
|
|
39
|
+
entering = false;
|
|
40
|
+
}
|
|
41
|
+
if (outcome === undefined)
|
|
42
|
+
throw new Error('Signal owners require synchronous request-context entry');
|
|
43
|
+
if ('error' in outcome) throw outcome.error;
|
|
44
|
+
return outcome.value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {RequestStorage} storage
|
|
49
|
+
* @param {SignalRequestHooks} hooks
|
|
50
|
+
*/
|
|
51
|
+
function install(storage, hooks) {
|
|
52
|
+
let environments = installed.get(storage);
|
|
53
|
+
if (environments?.has(hooks.install)) return;
|
|
54
|
+
if (!environments) {
|
|
55
|
+
environments = new WeakSet();
|
|
56
|
+
installed.set(storage, environments);
|
|
57
|
+
}
|
|
58
|
+
/** @param {RequestStore | undefined} store */
|
|
59
|
+
const rootContext = (store) => store?.signalRequestContext ?? store?.context;
|
|
60
|
+
hooks.install({
|
|
61
|
+
current() {
|
|
62
|
+
const store = storage.getStore();
|
|
63
|
+
const root = rootContext(store);
|
|
64
|
+
if (root && retired.has(root)) throw new Error('The signal request owner has retired');
|
|
65
|
+
if (store?.signalOwner !== undefined) return store.signalOwner;
|
|
66
|
+
if (!root) return null;
|
|
67
|
+
let owner = owners.get(root);
|
|
68
|
+
if (!owner) {
|
|
69
|
+
owner = Object.freeze({ scopeKey: 'octane:document' });
|
|
70
|
+
owners.set(root, owner);
|
|
71
|
+
}
|
|
72
|
+
return owner;
|
|
73
|
+
},
|
|
74
|
+
run(owner, callback) {
|
|
75
|
+
const store = storage.getStore();
|
|
76
|
+
return runSynchronous(
|
|
77
|
+
storage,
|
|
78
|
+
{ ...store, signalOwner: owner, signalRequestContext: rootContext(store) },
|
|
79
|
+
callback,
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
capture(owner) {
|
|
83
|
+
// Capture the request now, not whichever request later invokes this callback.
|
|
84
|
+
const store = storage.getStore();
|
|
85
|
+
return (callback) =>
|
|
86
|
+
runSynchronous(
|
|
87
|
+
storage,
|
|
88
|
+
{ ...store, signalOwner: owner, signalRequestContext: rootContext(store) },
|
|
89
|
+
callback,
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
environments.add(hooks.install);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Keep one lazy signal identity through middleware, sibling renders, and later
|
|
98
|
+
* response pulls. Completion retires the subscription owner, never a separately
|
|
99
|
+
* accepted durable server operation.
|
|
100
|
+
* @param {RequestStorage} storage
|
|
101
|
+
* @param {RequestStore} store
|
|
102
|
+
* @param {SignalRequestHooks | undefined} hooks
|
|
103
|
+
* @param {() => Promise<Response>} callback
|
|
104
|
+
* @returns {Promise<Response>}
|
|
105
|
+
*/
|
|
106
|
+
export async function runServerRequest(storage, store, hooks, callback) {
|
|
107
|
+
if (hooks === undefined) return storage.run(store, callback);
|
|
108
|
+
install(storage, hooks);
|
|
109
|
+
const root = store.signalRequestContext ?? store.context;
|
|
110
|
+
let finished = false;
|
|
111
|
+
const finish = () => {
|
|
112
|
+
if (finished) return;
|
|
113
|
+
finished = true;
|
|
114
|
+
store.context?.request.signal.removeEventListener('abort', abort);
|
|
115
|
+
if (root) {
|
|
116
|
+
retired.add(root);
|
|
117
|
+
const owner = owners.get(root);
|
|
118
|
+
if (owner) hooks.retire(owner);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/** @type {ReadableStreamDefaultReader<Uint8Array> | undefined} */
|
|
122
|
+
let reader;
|
|
123
|
+
const abort = () => {
|
|
124
|
+
// Cancel before retirement so iterator finally blocks still have their owner.
|
|
125
|
+
const pending = storage.run(store, () => reader?.cancel());
|
|
126
|
+
finish();
|
|
127
|
+
void Promise.resolve(pending).catch(() => {});
|
|
128
|
+
};
|
|
129
|
+
try {
|
|
130
|
+
const response = await storage.run(store, callback);
|
|
131
|
+
if (response.body === null) {
|
|
132
|
+
finish();
|
|
133
|
+
return response;
|
|
134
|
+
}
|
|
135
|
+
reader = response.body.getReader();
|
|
136
|
+
const bodyReader = reader;
|
|
137
|
+
store.context?.request.signal.addEventListener('abort', abort, { once: true });
|
|
138
|
+
if (store.context?.request.signal.aborted) abort();
|
|
139
|
+
return new Response(
|
|
140
|
+
new ReadableStream(
|
|
141
|
+
{
|
|
142
|
+
pull(controller) {
|
|
143
|
+
return storage.run(store, async () => {
|
|
144
|
+
try {
|
|
145
|
+
const item = await bodyReader.read();
|
|
146
|
+
if (item.done) {
|
|
147
|
+
bodyReader.releaseLock();
|
|
148
|
+
finish();
|
|
149
|
+
controller.close();
|
|
150
|
+
} else controller.enqueue(item.value);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
finish();
|
|
153
|
+
controller.error(error);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
cancel(reason) {
|
|
158
|
+
return storage.run(store, async () => {
|
|
159
|
+
try {
|
|
160
|
+
const pending = bodyReader.cancel(reason);
|
|
161
|
+
finish();
|
|
162
|
+
await pending;
|
|
163
|
+
} finally {
|
|
164
|
+
bodyReader.releaseLock();
|
|
165
|
+
finish();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{ highWaterMark: 0 },
|
|
171
|
+
),
|
|
172
|
+
{
|
|
173
|
+
status: response.status,
|
|
174
|
+
statusText: response.statusText,
|
|
175
|
+
headers: response.headers,
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
finish();
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
package/types/codegen.d.ts
CHANGED
|
@@ -25,9 +25,23 @@ export interface StaticClientEntry {
|
|
|
25
25
|
specifier: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export interface IndependentClientEntry {
|
|
29
|
+
/** Opaque module ID written into the independent hydration build manifest. */
|
|
30
|
+
id: string;
|
|
31
|
+
/** Bundler-resolvable compiler query whose default export is the activator. */
|
|
32
|
+
specifier: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
export interface ClientEntryOptions {
|
|
29
36
|
configPath?: string;
|
|
30
37
|
staticEntries?: Array<string | StaticClientEntry>;
|
|
38
|
+
independentEntries?: IndependentClientEntry[];
|
|
39
|
+
/** Immutable token embedded before the client bundle is hashed. */
|
|
40
|
+
clientBuildId?: string;
|
|
41
|
+
/** Trusted bundler expression (for example Rspack's __webpack_hash__). */
|
|
42
|
+
clientBuildIdExpression?: string;
|
|
43
|
+
/** Subscribe to Vite's current-build capability discovery in development. */
|
|
44
|
+
devClientBuild?: boolean;
|
|
31
45
|
resolveImport?: (id: string) => string;
|
|
32
46
|
runtimeModuleId?: string;
|
|
33
47
|
generatedBy?: string;
|
|
@@ -43,6 +57,13 @@ export interface ServerEntryOptions {
|
|
|
43
57
|
clientAssetMap?: Record<string, ClientAssetEntry>;
|
|
44
58
|
/** JSON file resolved beside the built server entry at module evaluation. */
|
|
45
59
|
clientAssetMapFile?: string;
|
|
60
|
+
clientBuild?: import('@octanejs/app-core/production').ClientBuildManifest;
|
|
61
|
+
/** Required completed-client metadata; a missing file is an error. */
|
|
62
|
+
clientBuildFile?: string;
|
|
63
|
+
/** Completed client-build metadata for strict independent Hydrate boundaries. */
|
|
64
|
+
independentHydrationManifest?: import('@octanejs/app-core/production').IndependentHydrationBuildManifest;
|
|
65
|
+
/** Optional JSON manifest resolved beside the built server entry. */
|
|
66
|
+
independentHydrationManifestFile?: string;
|
|
46
67
|
/** Stable application module ID to emitted bundler import specifier. */
|
|
47
68
|
moduleImports?: Record<string, string>;
|
|
48
69
|
resolveImport?: (id: string) => string;
|
package/types/html.d.ts
CHANGED
|
@@ -5,10 +5,15 @@ export function composeHtmlStream(
|
|
|
5
5
|
prefix: string,
|
|
6
6
|
renderStream: ReadableStream<Uint8Array>,
|
|
7
7
|
suffix: string,
|
|
8
|
+
afterShell?: string,
|
|
8
9
|
): ReadableStream<Uint8Array>;
|
|
9
10
|
export function validateSsrTemplate(html: string): void;
|
|
10
11
|
export function injectHydrationEntry(html: string, source: string, nonce?: string | null): string;
|
|
11
12
|
export function splitSsrTemplate(html: string): [prefix: string, suffix: string];
|
|
13
|
+
export function prepareStreamingHydrationTemplate(html: string): {
|
|
14
|
+
html: string;
|
|
15
|
+
afterShell: string;
|
|
16
|
+
};
|
|
12
17
|
export function applyHydrationNonce(html: string, nonce?: string | null): string;
|
|
13
18
|
export function nonceAttribute(nonce?: string | null): string;
|
|
14
19
|
export function getContextNonce(context: Context): string | null;
|
package/types/index.d.ts
CHANGED
|
@@ -97,6 +97,10 @@ export interface ServerRouteOptions {
|
|
|
97
97
|
// ============================================================================
|
|
98
98
|
|
|
99
99
|
export interface Context {
|
|
100
|
+
/** Trusted completed client generation. Browser routing data is not authorization. */
|
|
101
|
+
readonly clientBuild?: import('./production.js').ClientBuildManifest;
|
|
102
|
+
/** Production asset identities; development renders retain inline scoped CSS. */
|
|
103
|
+
readonly clientAssets?: Readonly<Record<string, import('./production.js').ClientAssetEntry>>;
|
|
100
104
|
/** The incoming Request object */
|
|
101
105
|
request: Request;
|
|
102
106
|
/** URL parameters extracted from the route pattern */
|
|
@@ -109,6 +113,8 @@ export interface Context {
|
|
|
109
113
|
* renderer inline scripts, hydration data, and the hydrate module script.
|
|
110
114
|
*/
|
|
111
115
|
state: Map<string, unknown>;
|
|
116
|
+
/** Identity established by authorization middleware, never by browser RPC arguments. */
|
|
117
|
+
viewer?: unknown;
|
|
112
118
|
/** Request-scoped bindings supplied by the active platform integration. */
|
|
113
119
|
platform?: unknown;
|
|
114
120
|
/**
|
|
@@ -168,6 +174,11 @@ export function handleServerRoute(
|
|
|
168
174
|
export function is_rpc_request(pathname: string): boolean;
|
|
169
175
|
|
|
170
176
|
/** Security policy and execution dependencies for a server-function request. */
|
|
177
|
+
export interface SignalRequestHooks {
|
|
178
|
+
install: typeof import('octane/server').installSignalOwnerEnvironment;
|
|
179
|
+
retire: typeof import('octane/server').retireSignalOwnerIdentity;
|
|
180
|
+
}
|
|
181
|
+
|
|
171
182
|
export interface RpcRequestOptions {
|
|
172
183
|
resolveFunction: (hash: string) => Function | null | Promise<Function | null>;
|
|
173
184
|
/**
|
|
@@ -177,8 +188,30 @@ export interface RpcRequestOptions {
|
|
|
177
188
|
* mapping, and the middleware chain must not wait on it.
|
|
178
189
|
*/
|
|
179
190
|
describeFunction?: (hash: string) => { module: string; export: string } | null;
|
|
180
|
-
executeServerFunction: (
|
|
181
|
-
|
|
191
|
+
executeServerFunction: (
|
|
192
|
+
fn: Function,
|
|
193
|
+
body: string,
|
|
194
|
+
context?: import('octane/server').ServerCallContext,
|
|
195
|
+
) => Promise<string>;
|
|
196
|
+
streamServerFunction?: (
|
|
197
|
+
fn: Function,
|
|
198
|
+
body: string,
|
|
199
|
+
context: import('octane/server').ServerCallContext,
|
|
200
|
+
limits?: import('octane/server').ServerResultLimits,
|
|
201
|
+
) => ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>;
|
|
202
|
+
resultLimits?: import('octane/server').ServerResultLimits;
|
|
203
|
+
batchServerFunctions?: (
|
|
204
|
+
...args: Parameters<typeof import('octane/server').executeServerFunctionBatch>
|
|
205
|
+
) => ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>;
|
|
206
|
+
asyncContext: AsyncContext<{
|
|
207
|
+
origin?: string;
|
|
208
|
+
platform?: unknown;
|
|
209
|
+
context?: Context;
|
|
210
|
+
serverCallHost?: import('octane/server').ServerCallHost;
|
|
211
|
+
signalOwner?: import('octane/server').SignalOwner;
|
|
212
|
+
signalRequestContext?: Context;
|
|
213
|
+
}>;
|
|
214
|
+
signalOwners?: SignalRequestHooks;
|
|
182
215
|
trustProxy?: boolean;
|
|
183
216
|
middlewares?: Middleware[];
|
|
184
217
|
allowedOrigins?: readonly string[];
|
|
@@ -189,6 +222,21 @@ export interface RpcRequestOptions {
|
|
|
189
222
|
/** Apply Octane's security policy and global middleware to a server function. */
|
|
190
223
|
export function handleRpcRequest(request: Request, options: RpcRequestOptions): Promise<Response>;
|
|
191
224
|
|
|
225
|
+
/** Shared request boundary for dev, production, and host-managed render routes. */
|
|
226
|
+
export function runServerRequest(
|
|
227
|
+
storage: RpcRequestOptions['asyncContext'],
|
|
228
|
+
store: NonNullable<ReturnType<RpcRequestOptions['asyncContext']['getStore']>>,
|
|
229
|
+
hooks: SignalRequestHooks | undefined,
|
|
230
|
+
callback: () => Promise<Response>,
|
|
231
|
+
): Promise<Response>;
|
|
232
|
+
|
|
233
|
+
export function createServerCallHost(
|
|
234
|
+
parent: Context,
|
|
235
|
+
options: Pick<RpcRequestOptions, 'asyncContext' | 'middlewares'> & { origin: string },
|
|
236
|
+
): import('octane/server').ServerCallHost;
|
|
237
|
+
|
|
238
|
+
export function setRequestContextSource(storage: RpcRequestOptions['asyncContext']): void;
|
|
239
|
+
|
|
192
240
|
/**
|
|
193
241
|
* The `Context` for the in-flight request.
|
|
194
242
|
*
|
|
@@ -430,6 +478,8 @@ export interface OctaneConfigOptions {
|
|
|
430
478
|
allowedOrigins?: string[];
|
|
431
479
|
/** Maximum encoded request size in bytes. @default 1048576 */
|
|
432
480
|
maxBodyBytes?: number;
|
|
481
|
+
/** Budgets for the opt-in streamed response, separate from request size. */
|
|
482
|
+
resultLimits?: import('octane/server').ServerResultLimits;
|
|
433
483
|
};
|
|
434
484
|
/**
|
|
435
485
|
* Production SSR mode: 'streaming' (default) flushes the shell at
|
|
@@ -477,6 +527,8 @@ export interface ResolvedOctaneConfig {
|
|
|
477
527
|
allowedOrigins: string[];
|
|
478
528
|
/** Maximum encoded request size in bytes. @default 1048576 */
|
|
479
529
|
maxBodyBytes: number;
|
|
530
|
+
/** Explicit response-frame, total-byte, and invocation-deadline budgets. */
|
|
531
|
+
resultLimits?: import('octane/server').ServerResultLimits;
|
|
480
532
|
};
|
|
481
533
|
/** @default 'streaming' */
|
|
482
534
|
render: 'streaming' | 'buffered';
|