@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
package/src/server/production.js
CHANGED
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
import { createRouter } from './router.js';
|
|
26
26
|
import { createContext, runMiddlewareChain } from './middleware.js';
|
|
27
27
|
import { handleRpcRequest } from './rpc.js';
|
|
28
|
+
import { setRequestContextSource } from './request-context.js';
|
|
29
|
+
import { createServerCallHost } from './server-calls.js';
|
|
30
|
+
import { runServerRequest } from './signal-owners.js';
|
|
28
31
|
import { rpcIdCollision } from './rpc-registry.js';
|
|
29
32
|
import { handleServerRoute } from './server-route.js';
|
|
30
33
|
import { composeHtmlStream } from './html-stream.js';
|
|
@@ -32,6 +35,7 @@ import {
|
|
|
32
35
|
applyHydrationNonce,
|
|
33
36
|
getContextNonce,
|
|
34
37
|
nonceAttribute,
|
|
38
|
+
prepareStreamingHydrationTemplate,
|
|
35
39
|
splitSsrTemplate,
|
|
36
40
|
validateSsrTemplate,
|
|
37
41
|
} from './html-template.js';
|
|
@@ -69,7 +73,7 @@ const FETCH_COORDINATOR_KEY = Symbol.for('octane.app-core.fetch-coordinator');
|
|
|
69
73
|
|
|
70
74
|
/**
|
|
71
75
|
* @typedef {Object} FetchCoordinator
|
|
72
|
-
* @property {import('@
|
|
76
|
+
* @property {import('@octanejs/app-core').RpcRequestOptions['asyncContext']} asyncContext
|
|
73
77
|
* @property {((request: Request, platform?: unknown) => Promise<Response>) | null} handler
|
|
74
78
|
*/
|
|
75
79
|
|
|
@@ -138,9 +142,16 @@ function buildRpcDescriptors(rpcModules, hashFn) {
|
|
|
138
142
|
* the body-marker contract.
|
|
139
143
|
*
|
|
140
144
|
* @param {string} html
|
|
145
|
+
* @param {boolean} [earlyHydration]
|
|
141
146
|
* @returns {(headContent: string) => string[]}
|
|
142
147
|
*/
|
|
143
|
-
function prepareSsrTemplate(html) {
|
|
148
|
+
function prepareSsrTemplate(html, earlyHydration = false) {
|
|
149
|
+
let afterShell = '';
|
|
150
|
+
if (earlyHydration) {
|
|
151
|
+
const prepared = prepareStreamingHydrationTemplate(html);
|
|
152
|
+
html = prepared.html;
|
|
153
|
+
afterShell = prepared.afterShell;
|
|
154
|
+
}
|
|
144
155
|
const [prefix, suffix] = splitSsrTemplate(html);
|
|
145
156
|
const prefixHeadAt = prefix.indexOf(HEAD_MARKER);
|
|
146
157
|
const headInPrefix = prefixHeadAt !== -1;
|
|
@@ -153,9 +164,9 @@ function prepareSsrTemplate(html) {
|
|
|
153
164
|
const nextPrefix = headInPrefix ? beforeHead + headContent + afterHead : prefix;
|
|
154
165
|
const nextSuffix = headInPrefix ? suffix : beforeHead + headContent + afterHead;
|
|
155
166
|
if (headContent.includes(BODY_MARKER) || BODY_CLOSE_TAG.test(headContent)) {
|
|
156
|
-
return splitSsrTemplate(nextPrefix + BODY_MARKER + nextSuffix);
|
|
167
|
+
return [...splitSsrTemplate(nextPrefix + BODY_MARKER + nextSuffix), afterShell];
|
|
157
168
|
}
|
|
158
|
-
return [nextPrefix, nextSuffix];
|
|
169
|
+
return [nextPrefix, nextSuffix, afterShell];
|
|
159
170
|
};
|
|
160
171
|
}
|
|
161
172
|
|
|
@@ -197,6 +208,83 @@ function prepareRenderRoutes(routes) {
|
|
|
197
208
|
return prepared;
|
|
198
209
|
}
|
|
199
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Freeze the client-build lookup behind the renderer's narrow per-boundary
|
|
213
|
+
* resolver. Request renders can then join immutable asset identity with their
|
|
214
|
+
* own encoded captures without consulting global mutable state.
|
|
215
|
+
*
|
|
216
|
+
* @param {ServerManifest['independentHydration']} manifest
|
|
217
|
+
* @returns {import('octane/server').RenderOptions['independentHydration'] | undefined}
|
|
218
|
+
*/
|
|
219
|
+
function prepareIndependentHydration(manifest) {
|
|
220
|
+
if (manifest == null) return undefined;
|
|
221
|
+
if (
|
|
222
|
+
manifest.version !== 1 ||
|
|
223
|
+
typeof manifest.buildId !== 'string' ||
|
|
224
|
+
manifest.buildId.length === 0 ||
|
|
225
|
+
manifest.widgets === null ||
|
|
226
|
+
typeof manifest.widgets !== 'object' ||
|
|
227
|
+
Array.isArray(manifest.widgets)
|
|
228
|
+
) {
|
|
229
|
+
throw new TypeError('Invalid independent Hydrate build manifest.');
|
|
230
|
+
}
|
|
231
|
+
return Object.freeze({
|
|
232
|
+
buildId: manifest.buildId,
|
|
233
|
+
resolve(boundaryId) {
|
|
234
|
+
const entry = manifest.widgets[boundaryId];
|
|
235
|
+
if (
|
|
236
|
+
entry === null ||
|
|
237
|
+
typeof entry !== 'object' ||
|
|
238
|
+
entry.version !== 1 ||
|
|
239
|
+
entry.boundaryId !== boundaryId ||
|
|
240
|
+
typeof entry.moduleId !== 'string' ||
|
|
241
|
+
!Array.isArray(entry.styles) ||
|
|
242
|
+
!entry.styles.every((style) => typeof style === 'string') ||
|
|
243
|
+
entry.parentDependencies !== false
|
|
244
|
+
) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
return { moduleId: entry.moduleId, styles: entry.styles };
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Snapshot completed build authority once per handler, never from request data.
|
|
254
|
+
* Legacy custom handlers without a client build retain their existing path.
|
|
255
|
+
* @param {ServerManifest} manifest
|
|
256
|
+
* @returns {ServerManifest['clientBuild'] | undefined}
|
|
257
|
+
*/
|
|
258
|
+
function prepareClientBuild(manifest) {
|
|
259
|
+
const build = manifest.clientBuild;
|
|
260
|
+
if (build == null) return undefined;
|
|
261
|
+
if (
|
|
262
|
+
build.version !== 1 ||
|
|
263
|
+
typeof build.buildId !== 'string' ||
|
|
264
|
+
build.buildId.length === 0 ||
|
|
265
|
+
(build.mode !== 'production' && build.mode !== 'development') ||
|
|
266
|
+
typeof build.capabilities?.independentHydration !== 'boolean'
|
|
267
|
+
) {
|
|
268
|
+
throw new TypeError('Invalid completed client build metadata.');
|
|
269
|
+
}
|
|
270
|
+
if (
|
|
271
|
+
(manifest.independentHydration != null &&
|
|
272
|
+
manifest.independentHydration.buildId !== build.buildId) ||
|
|
273
|
+
(build.capabilities.independentHydration && manifest.independentHydration == null) ||
|
|
274
|
+
(!build.capabilities.independentHydration &&
|
|
275
|
+
manifest.independentHydration != null &&
|
|
276
|
+
Object.keys(manifest.independentHydration.widgets).length !== 0)
|
|
277
|
+
) {
|
|
278
|
+
throw new Error('Independent Hydrate metadata does not match the completed client build.');
|
|
279
|
+
}
|
|
280
|
+
return Object.freeze({
|
|
281
|
+
version: 1,
|
|
282
|
+
buildId: build.buildId,
|
|
283
|
+
mode: build.mode,
|
|
284
|
+
capabilities: Object.freeze({ independentHydration: build.capabilities.independentHydration }),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
200
288
|
/**
|
|
201
289
|
* @param {ServerManifest} manifest
|
|
202
290
|
* @param {RenderRoute} route
|
|
@@ -255,12 +343,17 @@ export function createHandler(manifest, deps) {
|
|
|
255
343
|
const trustProxy = manifest.trustProxy ?? false;
|
|
256
344
|
const rpcPolicy = manifest.rpc;
|
|
257
345
|
const runtime = manifest.runtime;
|
|
346
|
+
const independentHydration = prepareIndependentHydration(manifest.independentHydration);
|
|
347
|
+
const clientBuild = prepareClientBuild(manifest);
|
|
258
348
|
validateSsrTemplate(htmlTemplate);
|
|
259
349
|
// Also pin the built-template contract up front. The marker is emitted by
|
|
260
350
|
// the integration's HTML transform and survives source hashing. Prepare the
|
|
261
351
|
// normalized no-nonce template once: this is the common request path, and its
|
|
262
352
|
// static fragments are identical for every request handled by this manifest.
|
|
263
|
-
const
|
|
353
|
+
const normalizedTemplate = applyHydrationNonce(htmlTemplate, null);
|
|
354
|
+
const splitHydrationTemplate = prepareSsrTemplate(normalizedTemplate);
|
|
355
|
+
/** @type {ReturnType<typeof prepareSsrTemplate> | undefined} */
|
|
356
|
+
let splitEarlyHydrationTemplate;
|
|
264
357
|
|
|
265
358
|
// RPC lookup for statically imported `module server` functions
|
|
266
359
|
// (compiler hash → server function).
|
|
@@ -300,6 +393,10 @@ export function createHandler(manifest, deps) {
|
|
|
300
393
|
return rpcDescriptors?.get(hash) ?? null;
|
|
301
394
|
},
|
|
302
395
|
executeServerFunction,
|
|
396
|
+
streamServerFunction: deps.streamServerFunction,
|
|
397
|
+
batchServerFunctions: deps.batchServerFunctions,
|
|
398
|
+
signalOwners: deps.signalOwners,
|
|
399
|
+
resultLimits: rpcPolicy?.resultLimits,
|
|
303
400
|
asyncContext,
|
|
304
401
|
trustProxy,
|
|
305
402
|
middlewares: globalMiddlewares,
|
|
@@ -319,22 +416,44 @@ export function createHandler(manifest, deps) {
|
|
|
319
416
|
}
|
|
320
417
|
|
|
321
418
|
const context = createContext(request, match.params, platform);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
419
|
+
if (clientBuild != null) Object.defineProperty(context, 'clientBuild', { value: clientBuild });
|
|
420
|
+
if (manifest.clientAssets != null)
|
|
421
|
+
Object.defineProperty(context, 'clientAssets', { value: manifest.clientAssets });
|
|
422
|
+
const run = async () => {
|
|
423
|
+
try {
|
|
424
|
+
if (match.route.type === 'render') {
|
|
425
|
+
return await runMiddlewareChain(
|
|
426
|
+
context,
|
|
427
|
+
globalMiddlewares,
|
|
428
|
+
match.route.before || [],
|
|
429
|
+
async () => renderRoute(/** @type {RenderRoute} */ (match.route), context),
|
|
430
|
+
[],
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
return await handleServerRoute(match.route, context, globalMiddlewares);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
console.error('[octane] Request error:', error);
|
|
436
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
332
437
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
438
|
+
};
|
|
439
|
+
if (!asyncContext) return run();
|
|
440
|
+
setRequestContextSource(asyncContext);
|
|
441
|
+
const origin = url.origin;
|
|
442
|
+
return runServerRequest(
|
|
443
|
+
asyncContext,
|
|
444
|
+
{
|
|
445
|
+
origin,
|
|
446
|
+
platform,
|
|
447
|
+
context,
|
|
448
|
+
serverCallHost: createServerCallHost(context, {
|
|
449
|
+
asyncContext,
|
|
450
|
+
middlewares: globalMiddlewares,
|
|
451
|
+
origin,
|
|
452
|
+
}),
|
|
453
|
+
},
|
|
454
|
+
deps.signalOwners,
|
|
455
|
+
run,
|
|
456
|
+
);
|
|
338
457
|
};
|
|
339
458
|
|
|
340
459
|
if (fetchCoordinator) fetchCoordinator.handler = handler;
|
|
@@ -388,6 +507,13 @@ export function createHandler(manifest, deps) {
|
|
|
388
507
|
|
|
389
508
|
// The hydration payload — SAME keys, SAME order as dev render-route.js, so
|
|
390
509
|
// the data script is byte-identical between dev and production.
|
|
510
|
+
const streamedSignals =
|
|
511
|
+
clientBuild == null
|
|
512
|
+
? undefined
|
|
513
|
+
: {
|
|
514
|
+
buildId: clientBuild.buildId,
|
|
515
|
+
documentId: crypto.randomUUID(),
|
|
516
|
+
};
|
|
391
517
|
const routeData = JSON.stringify({
|
|
392
518
|
entry: entryPath,
|
|
393
519
|
exportName: exportName ?? null,
|
|
@@ -397,6 +523,8 @@ export function createHandler(manifest, deps) {
|
|
|
397
523
|
url: requestUrl,
|
|
398
524
|
preHydrate: manifest.preHydrate ?? null,
|
|
399
525
|
rootBoundary: manifest.rootBoundaryEntries ?? { pending: null, catch: null },
|
|
526
|
+
clientBuild,
|
|
527
|
+
streamedSignals,
|
|
400
528
|
});
|
|
401
529
|
const dataScript = `<script id="__octane_data" type="application/json"${nonceAttribute(nonce)}>${escapeScript(routeData)}</script>`;
|
|
402
530
|
|
|
@@ -427,12 +555,14 @@ export function createHandler(manifest, deps) {
|
|
|
427
555
|
// `$&`, `` $` ``, `$'` and `$1` in the inserted text expand against the
|
|
428
556
|
// match, and this text now carries author-controlled metadata as well as
|
|
429
557
|
// the serialized route data.
|
|
430
|
-
/** @param {string} hoistedHead */
|
|
431
|
-
const splitAroundBody = (hoistedHead) => {
|
|
558
|
+
/** @param {string} hoistedHead @param {boolean} [earlyHydration] */
|
|
559
|
+
const splitAroundBody = (hoistedHead, earlyHydration = false) => {
|
|
432
560
|
const completeHead = headContent + hoistedHead;
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
561
|
+
if (noncedTemplate !== null)
|
|
562
|
+
return prepareSsrTemplate(noncedTemplate, earlyHydration)(completeHead);
|
|
563
|
+
if (!earlyHydration) return splitHydrationTemplate(completeHead);
|
|
564
|
+
splitEarlyHydrationTemplate ??= prepareSsrTemplate(normalizedTemplate, true);
|
|
565
|
+
return splitEarlyHydrationTemplate(completeHead);
|
|
436
566
|
};
|
|
437
567
|
|
|
438
568
|
if (manifest.render === 'buffered') {
|
|
@@ -447,6 +577,8 @@ export function createHandler(manifest, deps) {
|
|
|
447
577
|
} = await prerender(RootComponent, undefined, {
|
|
448
578
|
nonce: nonce ?? undefined,
|
|
449
579
|
headChannel: 'separate',
|
|
580
|
+
...(streamedSignals === undefined ? {} : { streamedSignals }),
|
|
581
|
+
...(independentHydration === undefined ? {} : { independentHydration }),
|
|
450
582
|
signal: context.request.signal,
|
|
451
583
|
onError(/** @type {unknown} */ error) {
|
|
452
584
|
console.error('[octane] SSR render error:', error);
|
|
@@ -459,10 +591,16 @@ export function createHandler(manifest, deps) {
|
|
|
459
591
|
// Streaming (default): shell flushes at first await, suspense segments
|
|
460
592
|
// stream out-of-order behind it — identical to dev.
|
|
461
593
|
let hoistedHead = '';
|
|
594
|
+
let earlyHydration = clientBuild?.capabilities.independentHydration === true;
|
|
462
595
|
/** @type {ReadableStream<Uint8Array>} */
|
|
463
596
|
const renderStream = await renderToReadableStream(RootComponent, undefined, {
|
|
597
|
+
onEarlyHydrationReady() {
|
|
598
|
+
earlyHydration = true;
|
|
599
|
+
},
|
|
464
600
|
nonce: nonce ?? undefined,
|
|
465
601
|
headChannel: 'separate',
|
|
602
|
+
...(streamedSignals === undefined ? {} : { streamedSignals }),
|
|
603
|
+
...(independentHydration === undefined ? {} : { independentHydration }),
|
|
466
604
|
// Fires before the shell is written, so the metadata is in hand before
|
|
467
605
|
// the template prefix (which carries `<head>`) is composed below.
|
|
468
606
|
onHeadReady(/** @type {string} */ head) {
|
|
@@ -474,8 +612,8 @@ export function createHandler(manifest, deps) {
|
|
|
474
612
|
},
|
|
475
613
|
});
|
|
476
614
|
|
|
477
|
-
const [prefix, suffix] = splitAroundBody(hoistedHead);
|
|
478
|
-
const body = composeHtmlStream(prefix, renderStream, suffix);
|
|
615
|
+
const [prefix, suffix, afterShell] = splitAroundBody(hoistedHead, earlyHydration);
|
|
616
|
+
const body = composeHtmlStream(prefix, renderStream, suffix, afterShell);
|
|
479
617
|
|
|
480
618
|
return new Response(body, { status, headers });
|
|
481
619
|
}
|
|
@@ -18,14 +18,17 @@
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* @typedef {import('@octanejs/app-core').Context} Context
|
|
21
|
-
* @typedef {import('@
|
|
22
|
-
* @typedef {{ asyncContext: RequestAsyncContext }} RequestContextHolder
|
|
21
|
+
* @typedef {import('@octanejs/app-core').RpcRequestOptions['asyncContext']} RequestAsyncContext
|
|
22
|
+
* @typedef {{ asyncContext: RequestAsyncContext, serverCallSource: import('octane/server').ServerCallContextSource }} RequestContextHolder
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
const REQUEST_CONTEXT_KEY = Symbol.for('octane.app-core.request-context');
|
|
26
|
+
const SERVER_CALL_CONTEXT_KEY = Symbol.for('octane.server.call-context');
|
|
26
27
|
|
|
27
28
|
const globals =
|
|
28
|
-
/** @type {typeof globalThis & { [REQUEST_CONTEXT_KEY]?: RequestContextHolder }} */ (
|
|
29
|
+
/** @type {typeof globalThis & { [REQUEST_CONTEXT_KEY]?: RequestContextHolder, [SERVER_CALL_CONTEXT_KEY]?: import('octane/server').ServerCallContextSource }} */ (
|
|
30
|
+
globalThis
|
|
31
|
+
);
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
34
|
* Publish the async context a request boundary runs its handler inside. Called
|
|
@@ -37,12 +40,21 @@ const globals =
|
|
|
37
40
|
* @returns {void}
|
|
38
41
|
*/
|
|
39
42
|
export function setRequestContextSource(asyncContext) {
|
|
40
|
-
|
|
43
|
+
let current = globals[REQUEST_CONTEXT_KEY];
|
|
41
44
|
if (current === undefined) {
|
|
42
|
-
|
|
45
|
+
current = {
|
|
46
|
+
asyncContext,
|
|
47
|
+
serverCallSource: {
|
|
48
|
+
getStore: () => globals[REQUEST_CONTEXT_KEY]?.asyncContext.getStore()?.serverCallHost,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
globals[REQUEST_CONTEXT_KEY] = current;
|
|
43
52
|
} else if (current.asyncContext !== asyncContext) {
|
|
44
53
|
current.asyncContext = asyncContext;
|
|
45
54
|
}
|
|
55
|
+
if (globals[SERVER_CALL_CONTEXT_KEY] !== current.serverCallSource) {
|
|
56
|
+
globals[SERVER_CALL_CONTEXT_KEY] = current.serverCallSource;
|
|
57
|
+
}
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
/**
|
package/src/server/rpc.js
CHANGED
|
@@ -13,6 +13,8 @@ import { derive_origin } from '@ripple-ts/adapter/rpc';
|
|
|
13
13
|
import { DEFAULT_RPC_MAX_BODY_BYTES } from '../constants.js';
|
|
14
14
|
import { createContext, runMiddlewareChain } from './middleware.js';
|
|
15
15
|
import { setRequestContextSource } from './request-context.js';
|
|
16
|
+
import { createServerCallHost, serverCallContext } from './server-calls.js';
|
|
17
|
+
import { runServerRequest } from './signal-owners.js';
|
|
16
18
|
|
|
17
19
|
const RPC_PATH_PREFIX = '/_$_ripple_rpc_$_/';
|
|
18
20
|
|
|
@@ -25,7 +27,11 @@ const RPC_PATH_PREFIX = '/_$_ripple_rpc_$_/';
|
|
|
25
27
|
function rpcError(status, message, headers) {
|
|
26
28
|
return new Response(JSON.stringify({ error: message }), {
|
|
27
29
|
status,
|
|
28
|
-
headers: {
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
32
|
+
'Octane-RPC-Outcome': 'rejected',
|
|
33
|
+
...headers,
|
|
34
|
+
},
|
|
29
35
|
});
|
|
30
36
|
}
|
|
31
37
|
|
|
@@ -74,6 +80,7 @@ function withRpcCors(response, origin) {
|
|
|
74
80
|
|
|
75
81
|
const headers = new Headers(response.headers);
|
|
76
82
|
headers.set('Access-Control-Allow-Origin', origin);
|
|
83
|
+
headers.set('Access-Control-Expose-Headers', 'Octane-RPC-Outcome');
|
|
77
84
|
headers.append('Vary', 'Origin');
|
|
78
85
|
return new Response(response.body, {
|
|
79
86
|
status: response.status,
|
|
@@ -212,6 +219,53 @@ export async function handleRpcRequest(request, options) {
|
|
|
212
219
|
}
|
|
213
220
|
const browserOrigin = request.headers.get('origin');
|
|
214
221
|
const corsOrigin = browserOrigin === origin ? null : browserOrigin;
|
|
222
|
+
if (request.headers.get('accept') === 'application/x-octane-rpc-batch+ndjson') {
|
|
223
|
+
if (options.batchServerFunctions === undefined || options.streamServerFunction === undefined) {
|
|
224
|
+
return withRpcCors(rpcError(406, 'Server-call batching is not configured'), corsOrigin);
|
|
225
|
+
}
|
|
226
|
+
const payload = await readBoundedRpcBody(
|
|
227
|
+
request,
|
|
228
|
+
options.maxBodyBytes ?? DEFAULT_RPC_MAX_BODY_BYTES,
|
|
229
|
+
);
|
|
230
|
+
if ('error' in payload) return withRpcCors(payload.error, corsOrigin);
|
|
231
|
+
try {
|
|
232
|
+
const stream = await options.batchServerFunctions(
|
|
233
|
+
payload.body,
|
|
234
|
+
(member, signal) => {
|
|
235
|
+
const url = new URL(request.url);
|
|
236
|
+
url.pathname = RPC_PATH_PREFIX + member.hash;
|
|
237
|
+
const headers = new Headers(request.headers);
|
|
238
|
+
headers.set('accept', 'application/x-octane-rpc+ndjson');
|
|
239
|
+
headers.delete('content-length');
|
|
240
|
+
// Re-enter the complete boundary for every member, with isolated
|
|
241
|
+
// authorization state. The batch envelope grants no credentials.
|
|
242
|
+
return handleRpcRequest(
|
|
243
|
+
new Request(url, {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers,
|
|
246
|
+
body: member.body,
|
|
247
|
+
signal,
|
|
248
|
+
}),
|
|
249
|
+
options,
|
|
250
|
+
);
|
|
251
|
+
},
|
|
252
|
+
{ ...options.resultLimits, signal: request.signal },
|
|
253
|
+
);
|
|
254
|
+
return withRpcCors(
|
|
255
|
+
new Response(stream, {
|
|
256
|
+
headers: {
|
|
257
|
+
'Content-Type': 'application/x-octane-rpc-batch+ndjson',
|
|
258
|
+
'Cache-Control': 'no-store',
|
|
259
|
+
},
|
|
260
|
+
}),
|
|
261
|
+
corsOrigin,
|
|
262
|
+
);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (isInvalidRpcPayload(error))
|
|
265
|
+
return withRpcCors(rpcError(400, 'Invalid server batch arguments'), corsOrigin);
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
215
269
|
|
|
216
270
|
const context = createContext(request, {}, options.platform);
|
|
217
271
|
// Name the target before middleware so a policy can authorize per function.
|
|
@@ -230,41 +284,123 @@ export async function handleRpcRequest(request, options) {
|
|
|
230
284
|
options.platform === undefined
|
|
231
285
|
? { origin, context }
|
|
232
286
|
: { origin, platform: options.platform, context };
|
|
287
|
+
Object.assign(store, {
|
|
288
|
+
serverCallHost: createServerCallHost(context, {
|
|
289
|
+
asyncContext: options.asyncContext,
|
|
290
|
+
middlewares: options.middlewares,
|
|
291
|
+
origin,
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
233
294
|
setRequestContextSource(options.asyncContext);
|
|
295
|
+
let invoked = false;
|
|
234
296
|
|
|
235
297
|
try {
|
|
236
|
-
const response = await
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
options.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
298
|
+
const response = await runServerRequest(
|
|
299
|
+
options.asyncContext,
|
|
300
|
+
store,
|
|
301
|
+
options.signalOwners,
|
|
302
|
+
async () =>
|
|
303
|
+
runMiddlewareChain(
|
|
304
|
+
context,
|
|
305
|
+
options.middlewares ?? [],
|
|
306
|
+
[],
|
|
307
|
+
async () => {
|
|
308
|
+
const fn = await options.resolveFunction(hash);
|
|
309
|
+
if (fn === null) return rpcError(404, 'RPC function not found');
|
|
310
|
+
const payload = await readBoundedRpcBody(
|
|
311
|
+
request,
|
|
312
|
+
options.maxBodyBytes ?? DEFAULT_RPC_MAX_BODY_BYTES,
|
|
313
|
+
);
|
|
314
|
+
if ('error' in payload) return payload.error;
|
|
315
|
+
try {
|
|
316
|
+
if (request.headers.get('accept') === 'application/x-octane-rpc+ndjson') {
|
|
317
|
+
if (options.streamServerFunction === undefined) {
|
|
318
|
+
return rpcError(406, 'Streamed RPC results are not configured');
|
|
319
|
+
}
|
|
320
|
+
invoked = true;
|
|
321
|
+
const stream = await options.streamServerFunction(
|
|
322
|
+
fn,
|
|
323
|
+
payload.body,
|
|
324
|
+
serverCallContext(context),
|
|
325
|
+
options.resultLimits,
|
|
326
|
+
);
|
|
327
|
+
const reader = stream.getReader();
|
|
328
|
+
return new Response(
|
|
329
|
+
new ReadableStream(
|
|
330
|
+
{
|
|
331
|
+
pull(controller) {
|
|
332
|
+
return options.asyncContext.run(store, async () => {
|
|
333
|
+
try {
|
|
334
|
+
const item = await reader.read();
|
|
335
|
+
if (item.done) {
|
|
336
|
+
reader.releaseLock();
|
|
337
|
+
controller.close();
|
|
338
|
+
} else controller.enqueue(item.value);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
controller.error(error);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
},
|
|
344
|
+
cancel(reason) {
|
|
345
|
+
return options.asyncContext.run(store, async () => {
|
|
346
|
+
try {
|
|
347
|
+
await reader.cancel(reason);
|
|
348
|
+
} finally {
|
|
349
|
+
reader.releaseLock();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
{ highWaterMark: 0 },
|
|
355
|
+
),
|
|
356
|
+
{
|
|
357
|
+
headers: {
|
|
358
|
+
'Content-Type': 'application/x-octane-rpc+ndjson',
|
|
359
|
+
'Cache-Control': 'no-store',
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
invoked = true;
|
|
365
|
+
const result = await options.executeServerFunction(
|
|
366
|
+
fn,
|
|
367
|
+
payload.body,
|
|
368
|
+
serverCallContext(context),
|
|
369
|
+
);
|
|
370
|
+
return new Response(result, {
|
|
371
|
+
status: 200,
|
|
372
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
373
|
+
});
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (isInvalidRpcPayload(error)) {
|
|
376
|
+
return rpcError(400, 'Invalid server function arguments');
|
|
377
|
+
}
|
|
378
|
+
throw error;
|
|
258
379
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
[],
|
|
263
|
-
),
|
|
380
|
+
},
|
|
381
|
+
[],
|
|
382
|
+
),
|
|
264
383
|
);
|
|
384
|
+
if (!response.ok && !invoked) {
|
|
385
|
+
const headers = new Headers(response.headers);
|
|
386
|
+
headers.set('Octane-RPC-Outcome', 'rejected');
|
|
387
|
+
return withRpcCors(
|
|
388
|
+
new Response(response.body, {
|
|
389
|
+
status: response.status,
|
|
390
|
+
statusText: response.statusText,
|
|
391
|
+
headers,
|
|
392
|
+
}),
|
|
393
|
+
corsOrigin,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
265
396
|
return withRpcCors(response, corsOrigin);
|
|
266
397
|
} catch (error) {
|
|
267
398
|
console.error('[octane] RPC request error:', error);
|
|
268
|
-
return withRpcCors(
|
|
399
|
+
return withRpcCors(
|
|
400
|
+
rpcError(500, 'Internal Server Error', {
|
|
401
|
+
'Octane-RPC-Outcome': invoked ? 'uncertain' : 'rejected',
|
|
402
|
+
}),
|
|
403
|
+
corsOrigin,
|
|
404
|
+
);
|
|
269
405
|
}
|
|
270
406
|
}
|