@reckona/mreact-server 0.0.179 → 0.0.181
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/dist/flight.d.ts.map +1 -1
- package/dist/flight.js +106 -71
- package/dist/flight.js.map +1 -1
- package/package.json +3 -3
- package/src/flight.ts +198 -138
package/src/flight.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { createCacheScope, runWithCacheScope } from "@reckona/mreact-compat/internal";
|
|
1
2
|
import { getNativeFlight } from "./native-flight.js";
|
|
2
3
|
|
|
3
4
|
/** Symbol tag used to identify client references in serialized Flight values. */
|
|
4
5
|
export const CLIENT_REFERENCE_TYPE = Symbol.for("modular.react.client_reference");
|
|
5
6
|
/** Symbol tag used to identify server references in serialized Flight values. */
|
|
6
7
|
export const SERVER_REFERENCE_TYPE = Symbol.for("modular.react.server_reference");
|
|
7
|
-
const CACHE_SCOPE_SYMBOL = Symbol.for("modular.react.cache_scope");
|
|
8
8
|
|
|
9
9
|
const REACT_COMPAT_ELEMENT_TYPE = Symbol.for("react.transitional.element");
|
|
10
10
|
const REACT_COMPAT_FRAGMENT_TYPE = Symbol.for("react.fragment");
|
|
@@ -88,6 +88,10 @@ export interface ServerActionHandlerOptions {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
91
|
+
const MAX_SERVER_ACTION_ARGUMENT_DEPTH = 64;
|
|
92
|
+
const MAX_SERVER_ACTION_ARGUMENT_ARRAY_LENGTH = 2_000;
|
|
93
|
+
const MAX_SERVER_ACTION_ARGUMENT_OBJECT_KEYS = 200;
|
|
94
|
+
const SERVER_ACTION_FORBIDDEN_JSON_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
91
95
|
|
|
92
96
|
/** Options for embedding a serialized Flight response in a script tag. */
|
|
93
97
|
export interface FlightScriptOptions {
|
|
@@ -274,8 +278,38 @@ export type FlightTypedArrayName =
|
|
|
274
278
|
| "BigInt64Array"
|
|
275
279
|
| "BigUint64Array";
|
|
276
280
|
|
|
277
|
-
const reactFlightBinaryRowTags = [
|
|
278
|
-
|
|
281
|
+
const reactFlightBinaryRowTags = [
|
|
282
|
+
"A",
|
|
283
|
+
"O",
|
|
284
|
+
"o",
|
|
285
|
+
"U",
|
|
286
|
+
"S",
|
|
287
|
+
"s",
|
|
288
|
+
"L",
|
|
289
|
+
"l",
|
|
290
|
+
"G",
|
|
291
|
+
"g",
|
|
292
|
+
"M",
|
|
293
|
+
"m",
|
|
294
|
+
"V",
|
|
295
|
+
] as const;
|
|
296
|
+
const reactFlightRowTags = [
|
|
297
|
+
"C",
|
|
298
|
+
"D",
|
|
299
|
+
"E",
|
|
300
|
+
"F",
|
|
301
|
+
"H",
|
|
302
|
+
"I",
|
|
303
|
+
"J",
|
|
304
|
+
"N",
|
|
305
|
+
"P",
|
|
306
|
+
"R",
|
|
307
|
+
"T",
|
|
308
|
+
"W",
|
|
309
|
+
"X",
|
|
310
|
+
"x",
|
|
311
|
+
"r",
|
|
312
|
+
] as const;
|
|
279
313
|
const reactFlightModelTokens = [
|
|
280
314
|
"$",
|
|
281
315
|
"$$",
|
|
@@ -328,12 +362,6 @@ interface FlightSerializationState {
|
|
|
328
362
|
serverReferenceIndexes: Map<string, number>;
|
|
329
363
|
}
|
|
330
364
|
|
|
331
|
-
interface ServerCacheScope {
|
|
332
|
-
functionCaches: WeakMap<(...args: never[]) => unknown, unknown>;
|
|
333
|
-
controller: AbortController;
|
|
334
|
-
ownerStack: string[];
|
|
335
|
-
}
|
|
336
|
-
|
|
337
365
|
/** Creates a runtime client reference marker for a module export. */
|
|
338
366
|
export function createClientReference(
|
|
339
367
|
moduleId: string,
|
|
@@ -431,8 +459,7 @@ export function createServerActionHandler(
|
|
|
431
459
|
const allowedActionKeys = options.allowedActions?.map((reference) =>
|
|
432
460
|
serverActionKey(reference.moduleId, reference.exportName),
|
|
433
461
|
);
|
|
434
|
-
const allowedActionSet =
|
|
435
|
-
allowedActionKeys === undefined ? undefined : new Set(allowedActionKeys);
|
|
462
|
+
const allowedActionSet = allowedActionKeys === undefined ? undefined : new Set(allowedActionKeys);
|
|
436
463
|
|
|
437
464
|
return async (request: Request): Promise<Response> => {
|
|
438
465
|
if (request.method !== "POST") {
|
|
@@ -491,7 +518,20 @@ export function createServerActionHandler(
|
|
|
491
518
|
const action = getServerAction(actionEntry);
|
|
492
519
|
const validateArgs = getServerActionArgsValidator(actionEntry);
|
|
493
520
|
const boundArgs = Array.isArray(payload.bound) ? payload.bound : [];
|
|
494
|
-
const
|
|
521
|
+
const extraArgs = Array.isArray(payload.args) ? payload.args : [];
|
|
522
|
+
const argsStructure = validateServerActionJsonArgumentStructure(boundArgs, extraArgs);
|
|
523
|
+
|
|
524
|
+
if (!argsStructure.valid) {
|
|
525
|
+
return jsonResponse(
|
|
526
|
+
{
|
|
527
|
+
ok: false,
|
|
528
|
+
error: "Invalid server action argument structure.",
|
|
529
|
+
},
|
|
530
|
+
400,
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const args = [...boundArgs, ...extraArgs];
|
|
495
535
|
const validationResult = validateArgs?.(args);
|
|
496
536
|
|
|
497
537
|
if (validationResult !== undefined && validationResult !== true) {
|
|
@@ -507,11 +547,7 @@ export function createServerActionHandler(
|
|
|
507
547
|
);
|
|
508
548
|
}
|
|
509
549
|
|
|
510
|
-
const authorizationResult = await options.authorize?.(
|
|
511
|
-
request,
|
|
512
|
-
reference,
|
|
513
|
-
args,
|
|
514
|
-
);
|
|
550
|
+
const authorizationResult = await options.authorize?.(request, reference, args);
|
|
515
551
|
|
|
516
552
|
if (authorizationResult !== undefined && authorizationResult !== true) {
|
|
517
553
|
return jsonResponse(
|
|
@@ -586,9 +622,10 @@ export function toReactFlightRows(response: FlightResponse): string {
|
|
|
586
622
|
rows.push(
|
|
587
623
|
`${formatReactFlightId(wireId)}:F${JSON.stringify({
|
|
588
624
|
id: serverActionKey(reference.moduleId, reference.exportName),
|
|
589
|
-
bound:
|
|
590
|
-
|
|
591
|
-
|
|
625
|
+
bound:
|
|
626
|
+
reference.bound === undefined
|
|
627
|
+
? null
|
|
628
|
+
: reference.bound.map((value) => encodeReactFlightModel(value, state)),
|
|
592
629
|
name: reference.exportName,
|
|
593
630
|
})}`,
|
|
594
631
|
);
|
|
@@ -643,7 +680,9 @@ export function fromReactFlightRows(rows: string): FlightResponse {
|
|
|
643
680
|
}
|
|
644
681
|
|
|
645
682
|
if (row.tag === "F") {
|
|
646
|
-
serverReferences.push(
|
|
683
|
+
serverReferences.push(
|
|
684
|
+
parseReactFlightServerReference(row.id, row.payload, modelChunks, errorChunks),
|
|
685
|
+
);
|
|
647
686
|
continue;
|
|
648
687
|
}
|
|
649
688
|
|
|
@@ -693,10 +732,7 @@ export function fromReactFlightRows(rows: string): FlightResponse {
|
|
|
693
732
|
}
|
|
694
733
|
|
|
695
734
|
/** Merges additional React Flight row text into an existing Flight response. */
|
|
696
|
-
export function mergeReactFlightRows(
|
|
697
|
-
response: FlightResponse,
|
|
698
|
-
rows: string,
|
|
699
|
-
): FlightResponse {
|
|
735
|
+
export function mergeReactFlightRows(response: FlightResponse, rows: string): FlightResponse {
|
|
700
736
|
// Issue 081 note: same finding as `fromReactFlightRows` — the native
|
|
701
737
|
// merge path is slower than the JS one given the double-JSON tax.
|
|
702
738
|
const modelChunks = new Map<number, unknown>();
|
|
@@ -713,7 +749,9 @@ export function mergeReactFlightRows(
|
|
|
713
749
|
}
|
|
714
750
|
|
|
715
751
|
if (row.tag === "F") {
|
|
716
|
-
serverReferences.push(
|
|
752
|
+
serverReferences.push(
|
|
753
|
+
parseReactFlightServerReference(row.id, row.payload, modelChunks, errorChunks),
|
|
754
|
+
);
|
|
717
755
|
continue;
|
|
718
756
|
}
|
|
719
757
|
|
|
@@ -779,9 +817,7 @@ export function renderFlightPreloadLinks(
|
|
|
779
817
|
seen.add(chunk);
|
|
780
818
|
return true;
|
|
781
819
|
})
|
|
782
|
-
.map((chunk) =>
|
|
783
|
-
`<link rel="modulepreload" href="${escapeAttribute(chunk)}"${nonceAttribute}>`,
|
|
784
|
-
)
|
|
820
|
+
.map((chunk) => `<link rel="modulepreload" href="${escapeAttribute(chunk)}"${nonceAttribute}>`)
|
|
785
821
|
.join("");
|
|
786
822
|
}
|
|
787
823
|
|
|
@@ -891,11 +927,7 @@ interface ReactFlightEncodingState {
|
|
|
891
927
|
}
|
|
892
928
|
|
|
893
929
|
function encodeReactFlightModel(model: FlightModel, state: ReactFlightEncodingState): unknown {
|
|
894
|
-
if (
|
|
895
|
-
model === null ||
|
|
896
|
-
typeof model === "number" ||
|
|
897
|
-
typeof model === "boolean"
|
|
898
|
-
) {
|
|
930
|
+
if (model === null || typeof model === "number" || typeof model === "boolean") {
|
|
899
931
|
return model;
|
|
900
932
|
}
|
|
901
933
|
|
|
@@ -973,7 +1005,9 @@ function encodeReactFlightModel(model: FlightModel, state: ReactFlightEncodingSt
|
|
|
973
1005
|
if (model.kind === "error") {
|
|
974
1006
|
const id = state.nextWireId;
|
|
975
1007
|
state.nextWireId += 1;
|
|
976
|
-
state.outlineRows.push(
|
|
1008
|
+
state.outlineRows.push(
|
|
1009
|
+
`${formatReactFlightId(id)}:E${JSON.stringify(encodeReactFlightError(model))}`,
|
|
1010
|
+
);
|
|
977
1011
|
return `$Z${formatReactFlightId(id)}`;
|
|
978
1012
|
}
|
|
979
1013
|
|
|
@@ -1005,10 +1039,7 @@ function encodeReactFlightModel(model: FlightModel, state: ReactFlightEncodingSt
|
|
|
1005
1039
|
return encodeReactFlightProps(model, state);
|
|
1006
1040
|
}
|
|
1007
1041
|
|
|
1008
|
-
function allocateReactFlightOutlineRow(
|
|
1009
|
-
state: ReactFlightEncodingState,
|
|
1010
|
-
payload: unknown,
|
|
1011
|
-
): number {
|
|
1042
|
+
function allocateReactFlightOutlineRow(state: ReactFlightEncodingState, payload: unknown): number {
|
|
1012
1043
|
const id = state.nextWireId;
|
|
1013
1044
|
state.nextWireId += 1;
|
|
1014
1045
|
state.outlineRows.push(`${formatReactFlightId(id)}:${JSON.stringify(payload)}`);
|
|
@@ -1037,10 +1068,7 @@ function encodeReactFlightProps(
|
|
|
1037
1068
|
return Object.fromEntries(
|
|
1038
1069
|
Object.entries(props)
|
|
1039
1070
|
.filter((entry): entry is [string, FlightModel] => entry[1] !== undefined)
|
|
1040
|
-
.map(([key, value]) => [
|
|
1041
|
-
key,
|
|
1042
|
-
encodeReactFlightModel(value, state),
|
|
1043
|
-
]),
|
|
1071
|
+
.map(([key, value]) => [key, encodeReactFlightModel(value, state)]),
|
|
1044
1072
|
);
|
|
1045
1073
|
}
|
|
1046
1074
|
|
|
@@ -1089,7 +1117,7 @@ function parseReactFlightRow(line: string): ReactFlightRow {
|
|
|
1089
1117
|
}
|
|
1090
1118
|
|
|
1091
1119
|
function looksLikeUnsupportedReactFlightTag(tag: string, body: string): boolean {
|
|
1092
|
-
return /^[A-Z]$/.test(tag) && (body[1] === "{" || body[1] === "[" || body[1] === "
|
|
1120
|
+
return /^[A-Z]$/.test(tag) && (body[1] === "{" || body[1] === "[" || body[1] === '"');
|
|
1093
1121
|
}
|
|
1094
1122
|
|
|
1095
1123
|
function parseReactFlightTextChunk(payload: string): string {
|
|
@@ -1207,7 +1235,9 @@ function createReactFlightBinaryModel(
|
|
|
1207
1235
|
};
|
|
1208
1236
|
}
|
|
1209
1237
|
|
|
1210
|
-
function getReactFlightTypedArrayName(
|
|
1238
|
+
function getReactFlightTypedArrayName(
|
|
1239
|
+
tag: Exclude<ReactFlightBinaryRowTag, "A" | "V">,
|
|
1240
|
+
): FlightTypedArrayName {
|
|
1211
1241
|
switch (tag) {
|
|
1212
1242
|
case "O":
|
|
1213
1243
|
return "Int8Array";
|
|
@@ -1350,11 +1380,7 @@ function decodeReactFlightModel(
|
|
|
1350
1380
|
context: FlightDecodeContext = createFlightDecodeContext(),
|
|
1351
1381
|
): FlightModel {
|
|
1352
1382
|
if (depth > MAX_FLIGHT_DECODE_DEPTH) flightTooDeep();
|
|
1353
|
-
if (
|
|
1354
|
-
value === null ||
|
|
1355
|
-
typeof value === "number" ||
|
|
1356
|
-
typeof value === "boolean"
|
|
1357
|
-
) {
|
|
1383
|
+
if (value === null || typeof value === "number" || typeof value === "boolean") {
|
|
1358
1384
|
return value;
|
|
1359
1385
|
}
|
|
1360
1386
|
|
|
@@ -1463,10 +1489,18 @@ function decodeReactFlightString(
|
|
|
1463
1489
|
}
|
|
1464
1490
|
|
|
1465
1491
|
if (/^\$Q[0-9a-f]+$/i.test(value)) {
|
|
1466
|
-
const decoded = decodeReactFlightChunk(
|
|
1492
|
+
const decoded = decodeReactFlightChunk(
|
|
1493
|
+
value.slice(2),
|
|
1494
|
+
modelChunks,
|
|
1495
|
+
errorChunks,
|
|
1496
|
+
depth + 1,
|
|
1497
|
+
context,
|
|
1498
|
+
);
|
|
1467
1499
|
const entries = Array.isArray(decoded)
|
|
1468
1500
|
? decoded.map((entry): [FlightModel, FlightModel] =>
|
|
1469
|
-
Array.isArray(entry)
|
|
1501
|
+
Array.isArray(entry)
|
|
1502
|
+
? [entry[0] ?? { kind: "undefined" }, entry[1] ?? { kind: "undefined" }]
|
|
1503
|
+
: [entry, { kind: "undefined" }],
|
|
1470
1504
|
)
|
|
1471
1505
|
: [];
|
|
1472
1506
|
|
|
@@ -1477,7 +1511,13 @@ function decodeReactFlightString(
|
|
|
1477
1511
|
}
|
|
1478
1512
|
|
|
1479
1513
|
if (/^\$W[0-9a-f]+$/i.test(value)) {
|
|
1480
|
-
const decoded = decodeReactFlightChunk(
|
|
1514
|
+
const decoded = decodeReactFlightChunk(
|
|
1515
|
+
value.slice(2),
|
|
1516
|
+
modelChunks,
|
|
1517
|
+
errorChunks,
|
|
1518
|
+
depth + 1,
|
|
1519
|
+
context,
|
|
1520
|
+
);
|
|
1481
1521
|
|
|
1482
1522
|
return {
|
|
1483
1523
|
kind: "set",
|
|
@@ -1486,7 +1526,13 @@ function decodeReactFlightString(
|
|
|
1486
1526
|
}
|
|
1487
1527
|
|
|
1488
1528
|
if (/^\$K[0-9a-f]+$/i.test(value)) {
|
|
1489
|
-
const decoded = decodeReactFlightChunk(
|
|
1529
|
+
const decoded = decodeReactFlightChunk(
|
|
1530
|
+
value.slice(2),
|
|
1531
|
+
modelChunks,
|
|
1532
|
+
errorChunks,
|
|
1533
|
+
depth + 1,
|
|
1534
|
+
context,
|
|
1535
|
+
);
|
|
1490
1536
|
const entries = Array.isArray(decoded)
|
|
1491
1537
|
? decoded.flatMap((entry): [string, FlightModel][] =>
|
|
1492
1538
|
Array.isArray(entry) && typeof entry[0] === "string"
|
|
@@ -1502,7 +1548,13 @@ function decodeReactFlightString(
|
|
|
1502
1548
|
}
|
|
1503
1549
|
|
|
1504
1550
|
if (/^\$i[0-9a-f]+$/i.test(value)) {
|
|
1505
|
-
const decoded = decodeReactFlightChunk(
|
|
1551
|
+
const decoded = decodeReactFlightChunk(
|
|
1552
|
+
value.slice(2),
|
|
1553
|
+
modelChunks,
|
|
1554
|
+
errorChunks,
|
|
1555
|
+
depth + 1,
|
|
1556
|
+
context,
|
|
1557
|
+
);
|
|
1506
1558
|
|
|
1507
1559
|
return {
|
|
1508
1560
|
kind: "iterable",
|
|
@@ -1511,11 +1563,13 @@ function decodeReactFlightString(
|
|
|
1511
1563
|
}
|
|
1512
1564
|
|
|
1513
1565
|
if (/^\$Z[0-9a-f]+$/i.test(value)) {
|
|
1514
|
-
return
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1566
|
+
return (
|
|
1567
|
+
errorChunks.get(parseReactFlightId(value.slice(2))) ?? {
|
|
1568
|
+
kind: "error",
|
|
1569
|
+
name: "Error",
|
|
1570
|
+
message: "Unknown React Flight error.",
|
|
1571
|
+
}
|
|
1572
|
+
);
|
|
1519
1573
|
}
|
|
1520
1574
|
|
|
1521
1575
|
if (value === "$Y" || value.startsWith("$E")) {
|
|
@@ -1634,10 +1688,7 @@ function encodeReactFlightError(model: FlightErrorModel): Record<string, unknown
|
|
|
1634
1688
|
|
|
1635
1689
|
function isFlightErrorModel(model: FlightModel): model is FlightErrorModel {
|
|
1636
1690
|
return (
|
|
1637
|
-
typeof model === "object" &&
|
|
1638
|
-
model !== null &&
|
|
1639
|
-
!Array.isArray(model) &&
|
|
1640
|
-
model.kind === "error"
|
|
1691
|
+
typeof model === "object" && model !== null && !Array.isArray(model) && model.kind === "error"
|
|
1641
1692
|
);
|
|
1642
1693
|
}
|
|
1643
1694
|
|
|
@@ -1670,10 +1721,7 @@ async function serializeFlightValue(
|
|
|
1670
1721
|
return { kind: "undefined" };
|
|
1671
1722
|
}
|
|
1672
1723
|
|
|
1673
|
-
if (
|
|
1674
|
-
typeof awaited === "string" ||
|
|
1675
|
-
typeof awaited === "boolean"
|
|
1676
|
-
) {
|
|
1724
|
+
if (typeof awaited === "string" || typeof awaited === "boolean") {
|
|
1677
1725
|
return awaited;
|
|
1678
1726
|
}
|
|
1679
1727
|
|
|
@@ -1728,10 +1776,13 @@ async function serializeFlightValue(
|
|
|
1728
1776
|
return {
|
|
1729
1777
|
kind: "map",
|
|
1730
1778
|
entries: await Promise.all(
|
|
1731
|
-
Array.from(awaited.entries()).map(
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1779
|
+
Array.from(awaited.entries()).map(
|
|
1780
|
+
async ([key, value]) =>
|
|
1781
|
+
[
|
|
1782
|
+
await serializeFlightValue(key, state, depth + 1),
|
|
1783
|
+
await serializeFlightValue(value, state, depth + 1),
|
|
1784
|
+
] as [FlightModel, FlightModel],
|
|
1785
|
+
),
|
|
1735
1786
|
),
|
|
1736
1787
|
};
|
|
1737
1788
|
}
|
|
@@ -1749,10 +1800,10 @@ async function serializeFlightValue(
|
|
|
1749
1800
|
return {
|
|
1750
1801
|
kind: "form-data",
|
|
1751
1802
|
entries: await Promise.all(
|
|
1752
|
-
Array.from(awaited.entries()).map(
|
|
1753
|
-
key,
|
|
1754
|
-
|
|
1755
|
-
|
|
1803
|
+
Array.from(awaited.entries()).map(
|
|
1804
|
+
async ([key, value]) =>
|
|
1805
|
+
[key, await serializeFlightValue(value, state, depth + 1)] as [string, FlightModel],
|
|
1806
|
+
),
|
|
1756
1807
|
),
|
|
1757
1808
|
};
|
|
1758
1809
|
}
|
|
@@ -1829,10 +1880,9 @@ async function serializeProps(
|
|
|
1829
1880
|
depth: number,
|
|
1830
1881
|
): Promise<Record<string, FlightModel>> {
|
|
1831
1882
|
const entries = await Promise.all(
|
|
1832
|
-
Object.entries(props).map(
|
|
1833
|
-
key,
|
|
1834
|
-
|
|
1835
|
-
] as const),
|
|
1883
|
+
Object.entries(props).map(
|
|
1884
|
+
async ([key, value]) => [key, await serializeFlightValue(value, state, depth + 1)] as const,
|
|
1885
|
+
),
|
|
1836
1886
|
);
|
|
1837
1887
|
|
|
1838
1888
|
return Object.fromEntries(entries);
|
|
@@ -1845,18 +1895,14 @@ async function serializeObject(
|
|
|
1845
1895
|
): Promise<FlightObjectModel> {
|
|
1846
1896
|
return Object.fromEntries(
|
|
1847
1897
|
await Promise.all(
|
|
1848
|
-
Object.entries(object).map(
|
|
1849
|
-
key,
|
|
1850
|
-
|
|
1851
|
-
] as const),
|
|
1898
|
+
Object.entries(object).map(
|
|
1899
|
+
async ([key, value]) => [key, await serializeFlightValue(value, state, depth + 1)] as const,
|
|
1900
|
+
),
|
|
1852
1901
|
),
|
|
1853
1902
|
) as FlightObjectModel;
|
|
1854
1903
|
}
|
|
1855
1904
|
|
|
1856
|
-
function getClientReferenceId(
|
|
1857
|
-
reference: ClientReference,
|
|
1858
|
-
state: FlightSerializationState,
|
|
1859
|
-
): number {
|
|
1905
|
+
function getClientReferenceId(reference: ClientReference, state: FlightSerializationState): number {
|
|
1860
1906
|
const key = `${reference.moduleId}:${reference.exportName}`;
|
|
1861
1907
|
const existing = state.clientReferenceIndexes.get(key);
|
|
1862
1908
|
|
|
@@ -1879,9 +1925,10 @@ async function getServerReferenceId(
|
|
|
1879
1925
|
reference: ServerReference,
|
|
1880
1926
|
state: FlightSerializationState,
|
|
1881
1927
|
): Promise<number> {
|
|
1882
|
-
const serializedBound =
|
|
1883
|
-
|
|
1884
|
-
|
|
1928
|
+
const serializedBound =
|
|
1929
|
+
reference.bound === undefined
|
|
1930
|
+
? undefined
|
|
1931
|
+
: await Promise.all(reference.bound.map((value) => serializeFlightValue(value, state, 0)));
|
|
1885
1932
|
const key = `${reference.moduleId}:${reference.exportName}:${JSON.stringify(serializedBound ?? null)}`;
|
|
1886
1933
|
const existing = state.serverReferenceIndexes.get(key);
|
|
1887
1934
|
|
|
@@ -1977,6 +2024,62 @@ async function readServerActionPayload(
|
|
|
1977
2024
|
}
|
|
1978
2025
|
}
|
|
1979
2026
|
|
|
2027
|
+
function validateServerActionJsonArgumentStructure(
|
|
2028
|
+
boundArgs: readonly unknown[],
|
|
2029
|
+
args: readonly unknown[],
|
|
2030
|
+
): { valid: true } | { valid: false } {
|
|
2031
|
+
const stack: { depth: number; value: unknown }[] = [
|
|
2032
|
+
{ depth: 0, value: boundArgs },
|
|
2033
|
+
{ depth: 0, value: args },
|
|
2034
|
+
];
|
|
2035
|
+
const seen = new WeakSet<object>();
|
|
2036
|
+
|
|
2037
|
+
while (stack.length > 0) {
|
|
2038
|
+
const current = stack.pop();
|
|
2039
|
+
if (current === undefined) {
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
if (current.depth > MAX_SERVER_ACTION_ARGUMENT_DEPTH) {
|
|
2044
|
+
return { valid: false };
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
if (current.value === null || typeof current.value !== "object") {
|
|
2048
|
+
continue;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
if (seen.has(current.value)) {
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
seen.add(current.value);
|
|
2055
|
+
|
|
2056
|
+
if (Array.isArray(current.value)) {
|
|
2057
|
+
if (current.value.length > MAX_SERVER_ACTION_ARGUMENT_ARRAY_LENGTH) {
|
|
2058
|
+
return { valid: false };
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
for (const item of current.value) {
|
|
2062
|
+
stack.push({ depth: current.depth + 1, value: item });
|
|
2063
|
+
}
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
const entries = Object.entries(current.value);
|
|
2068
|
+
if (entries.length > MAX_SERVER_ACTION_ARGUMENT_OBJECT_KEYS) {
|
|
2069
|
+
return { valid: false };
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
for (const [key, value] of entries) {
|
|
2073
|
+
if (SERVER_ACTION_FORBIDDEN_JSON_KEYS.has(key)) {
|
|
2074
|
+
return { valid: false };
|
|
2075
|
+
}
|
|
2076
|
+
stack.push({ depth: current.depth + 1, value });
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
return { valid: true };
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1980
2083
|
function getServerAction(entry: ServerAction | ServerActionDescriptor): ServerAction {
|
|
1981
2084
|
return typeof entry === "function" ? entry : entry.action;
|
|
1982
2085
|
}
|
|
@@ -1999,49 +2102,9 @@ function getServerActionArgsValidator(
|
|
|
1999
2102
|
}
|
|
2000
2103
|
|
|
2001
2104
|
function runWithFlightCacheScope<T>(callback: () => T): T {
|
|
2002
|
-
const
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
controller: new AbortController(),
|
|
2006
|
-
ownerStack: ["renderToFlightResponse"],
|
|
2007
|
-
});
|
|
2008
|
-
|
|
2009
|
-
try {
|
|
2010
|
-
const result = callback();
|
|
2011
|
-
|
|
2012
|
-
if (isPromiseLike(result)) {
|
|
2013
|
-
return Promise.resolve(result).finally(() => {
|
|
2014
|
-
setGlobalCacheScope(previousScope);
|
|
2015
|
-
}) as T;
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
setGlobalCacheScope(previousScope);
|
|
2019
|
-
return result;
|
|
2020
|
-
} catch (error) {
|
|
2021
|
-
setGlobalCacheScope(previousScope);
|
|
2022
|
-
throw error;
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
2027
|
-
return (
|
|
2028
|
-
(typeof value === "object" || typeof value === "function") &&
|
|
2029
|
-
value !== null &&
|
|
2030
|
-
typeof (value as { then?: unknown }).then === "function"
|
|
2031
|
-
);
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
function getGlobalCacheScope(): ServerCacheScope | undefined {
|
|
2035
|
-
return (globalThis as { [CACHE_SCOPE_SYMBOL]?: ServerCacheScope })[CACHE_SCOPE_SYMBOL];
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
function setGlobalCacheScope(scope: ServerCacheScope | undefined): void {
|
|
2039
|
-
if (scope === undefined) {
|
|
2040
|
-
delete (globalThis as { [CACHE_SCOPE_SYMBOL]?: ServerCacheScope })[CACHE_SCOPE_SYMBOL];
|
|
2041
|
-
return;
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
(globalThis as { [CACHE_SCOPE_SYMBOL]?: ServerCacheScope })[CACHE_SCOPE_SYMBOL] = scope;
|
|
2105
|
+
const scope = createCacheScope();
|
|
2106
|
+
scope.ownerStack.push("renderToFlightResponse");
|
|
2107
|
+
return runWithCacheScope(scope, callback);
|
|
2045
2108
|
}
|
|
2046
2109
|
|
|
2047
2110
|
function validateRequestOrigin(
|
|
@@ -2124,10 +2187,7 @@ function validateServerActionNonce(
|
|
|
2124
2187
|
|
|
2125
2188
|
if (replayProtection.seen.has(nonce)) {
|
|
2126
2189
|
return {
|
|
2127
|
-
response: jsonResponse(
|
|
2128
|
-
{ ok: false, error: "Server action nonce was already used." },
|
|
2129
|
-
409,
|
|
2130
|
-
),
|
|
2190
|
+
response: jsonResponse({ ok: false, error: "Server action nonce was already used." }, 409),
|
|
2131
2191
|
};
|
|
2132
2192
|
}
|
|
2133
2193
|
|