@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.
@@ -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 '../../types/production.d.ts' */
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 = !isWebWorker && Boolean(clientAssetMapFile);
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
- ? `JSON.parse(readFileSync(join(__dirname, ${JSON.stringify(clientAssetMapFile)}), 'utf-8'))`
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({ htmlTemplate, clientAssets = manifest.clientAssets }) {
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
+ }
@@ -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: (fn: Function, body: string) => Promise<string>;
181
- asyncContext: AsyncContext<{ origin?: string; platform?: unknown; context?: Context }>;
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';