@bleedingdev/modern-js-renderer-core 0.0.0-bootstrap → 3.9.3-ultramodern.1
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/LICENSE +21 -0
- package/dist/cjs/abort.js +65 -0
- package/dist/cjs/adapter.js +62 -0
- package/dist/cjs/data/body.js +78 -0
- package/dist/cjs/data/client.js +409 -0
- package/dist/cjs/data/codec.js +168 -0
- package/dist/cjs/data/index.js +125 -0
- package/dist/cjs/data/private.js +77 -0
- package/dist/cjs/data/routes.js +149 -0
- package/dist/cjs/data/server.js +791 -0
- package/dist/cjs/data/static.js +110 -0
- package/dist/cjs/data/tree.js +231 -0
- package/dist/cjs/data/types.js +56 -0
- package/dist/cjs/document.js +184 -0
- package/dist/cjs/entry-client.js +142 -0
- package/dist/cjs/entry-server.js +253 -0
- package/dist/cjs/federation.js +80 -0
- package/dist/cjs/identity.js +120 -0
- package/dist/cjs/index.js +46 -0
- package/dist/cjs/localization.js +48 -0
- package/dist/cjs/router.js +293 -0
- package/dist/cjs/server/assets.js +56 -0
- package/dist/cjs/server/dispatch.js +351 -0
- package/dist/cjs/server/index.js +57 -0
- package/dist/cjs/server/types.js +18 -0
- package/dist/cjs/server/worker.js +73 -0
- package/dist/cjs/session/cache.js +172 -0
- package/dist/cjs/session/index.js +63 -0
- package/dist/cjs/session/request.js +269 -0
- package/dist/cjs/session/types.js +18 -0
- package/dist/esm/abort.mjs +27 -0
- package/dist/esm/adapter.mjs +21 -0
- package/dist/esm/data/body.mjs +40 -0
- package/dist/esm/data/client.mjs +368 -0
- package/dist/esm/data/codec.mjs +112 -0
- package/dist/esm/data/index.mjs +7 -0
- package/dist/esm/data/private.mjs +39 -0
- package/dist/esm/data/routes.mjs +108 -0
- package/dist/esm/data/server.mjs +723 -0
- package/dist/esm/data/static.mjs +51 -0
- package/dist/esm/data/tree.mjs +193 -0
- package/dist/esm/data/types.mjs +6 -0
- package/dist/esm/document.mjs +131 -0
- package/dist/esm/entry-client.mjs +104 -0
- package/dist/esm/entry-server.mjs +215 -0
- package/dist/esm/federation.mjs +36 -0
- package/dist/esm/identity.mjs +63 -0
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/localization.mjs +10 -0
- package/dist/esm/router.mjs +225 -0
- package/dist/esm/server/assets.mjs +15 -0
- package/dist/esm/server/dispatch.mjs +307 -0
- package/dist/esm/server/index.mjs +3 -0
- package/dist/esm/server/types.mjs +0 -0
- package/dist/esm/server/worker.mjs +35 -0
- package/dist/esm/session/cache.mjs +116 -0
- package/dist/esm/session/index.mjs +3 -0
- package/dist/esm/session/request.mjs +231 -0
- package/dist/esm/session/types.mjs +0 -0
- package/dist/esm-node/abort.mjs +28 -0
- package/dist/esm-node/adapter.mjs +22 -0
- package/dist/esm-node/data/body.mjs +41 -0
- package/dist/esm-node/data/client.mjs +369 -0
- package/dist/esm-node/data/codec.mjs +113 -0
- package/dist/esm-node/data/index.mjs +8 -0
- package/dist/esm-node/data/private.mjs +40 -0
- package/dist/esm-node/data/routes.mjs +109 -0
- package/dist/esm-node/data/server.mjs +724 -0
- package/dist/esm-node/data/static.mjs +52 -0
- package/dist/esm-node/data/tree.mjs +194 -0
- package/dist/esm-node/data/types.mjs +7 -0
- package/dist/esm-node/document.mjs +132 -0
- package/dist/esm-node/entry-client.mjs +105 -0
- package/dist/esm-node/entry-server.mjs +216 -0
- package/dist/esm-node/federation.mjs +37 -0
- package/dist/esm-node/identity.mjs +64 -0
- package/dist/esm-node/index.mjs +2 -0
- package/dist/esm-node/localization.mjs +11 -0
- package/dist/esm-node/router.mjs +226 -0
- package/dist/esm-node/server/assets.mjs +16 -0
- package/dist/esm-node/server/dispatch.mjs +308 -0
- package/dist/esm-node/server/index.mjs +4 -0
- package/dist/esm-node/server/types.mjs +1 -0
- package/dist/esm-node/server/worker.mjs +36 -0
- package/dist/esm-node/session/cache.mjs +117 -0
- package/dist/esm-node/session/index.mjs +4 -0
- package/dist/esm-node/session/request.mjs +232 -0
- package/dist/esm-node/session/types.mjs +1 -0
- package/dist/types/abort.d.ts +6 -0
- package/dist/types/adapter.d.ts +195 -0
- package/dist/types/data/body.d.ts +2 -0
- package/dist/types/data/client.d.ts +17 -0
- package/dist/types/data/codec.d.ts +14 -0
- package/dist/types/data/index.d.ts +8 -0
- package/dist/types/data/private.d.ts +6 -0
- package/dist/types/data/routes.d.ts +28 -0
- package/dist/types/data/server.d.ts +59 -0
- package/dist/types/data/static.d.ts +31 -0
- package/dist/types/data/tree.d.ts +3 -0
- package/dist/types/data/types.d.ts +104 -0
- package/dist/types/document.d.ts +76 -0
- package/dist/types/entry-client.d.ts +53 -0
- package/dist/types/entry-server.d.ts +63 -0
- package/dist/types/federation.d.ts +56 -0
- package/dist/types/identity.d.ts +27 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/localization.d.ts +58 -0
- package/dist/types/router.d.ts +201 -0
- package/dist/types/server/assets.d.ts +14 -0
- package/dist/types/server/dispatch.d.ts +11 -0
- package/dist/types/server/index.d.ts +4 -0
- package/dist/types/server/types.d.ts +64 -0
- package/dist/types/server/worker.d.ts +25 -0
- package/dist/types/session/cache.d.ts +25 -0
- package/dist/types/session/index.d.ts +4 -0
- package/dist/types/session/request.d.ts +8 -0
- package/dist/types/session/types.d.ts +48 -0
- package/package.json +159 -4
- package/src/abort.ts +40 -0
- package/src/adapter.ts +255 -0
- package/src/data/body.ts +43 -0
- package/src/data/client.ts +501 -0
- package/src/data/codec.ts +247 -0
- package/src/data/index.ts +8 -0
- package/src/data/private.ts +68 -0
- package/src/data/routes.ts +201 -0
- package/src/data/server.ts +977 -0
- package/src/data/static.ts +105 -0
- package/src/data/tree.ts +244 -0
- package/src/data/types.ts +98 -0
- package/src/document.ts +338 -0
- package/src/entry-client.ts +236 -0
- package/src/entry-server.ts +393 -0
- package/src/federation.ts +111 -0
- package/src/identity.ts +128 -0
- package/src/index.ts +7 -0
- package/src/localization.ts +77 -0
- package/src/router.ts +475 -0
- package/src/server/assets.ts +60 -0
- package/src/server/dispatch.ts +523 -0
- package/src/server/index.ts +16 -0
- package/src/server/types.ts +94 -0
- package/src/server/worker.ts +94 -0
- package/src/session/cache.ts +177 -0
- package/src/session/index.ts +11 -0
- package/src/session/request.ts +324 -0
- package/src/session/types.ts +60 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { MAX_DATA_BYTES } from './codec';
|
|
2
|
+
import { DATA_CONTENT_TYPE, DATA_STREAM_CONTENT_TYPE } from './types';
|
|
3
|
+
|
|
4
|
+
/** Head marker written into documents produced by static prerendering. */
|
|
5
|
+
export const PRERENDERED_DOCUMENT_META = 'ultramodern-prerendered';
|
|
6
|
+
/** Directory beside a prerendered document that holds its loader payloads. */
|
|
7
|
+
export const STATIC_DATA_DIRECTORY = '__ultramodern-data';
|
|
8
|
+
|
|
9
|
+
/** A loader response captured at build time, replayed by the data client. */
|
|
10
|
+
export interface StaticDataPayload {
|
|
11
|
+
status: number;
|
|
12
|
+
contentType: typeof DATA_CONTENT_TYPE | typeof DATA_STREAM_CONTENT_TYPE;
|
|
13
|
+
body: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function encodeRouteId(routeId: string): string {
|
|
17
|
+
let binary = '';
|
|
18
|
+
for (const byte of new TextEncoder().encode(routeId))
|
|
19
|
+
binary += String.fromCharCode(byte);
|
|
20
|
+
return btoa(binary)
|
|
21
|
+
.replaceAll('+', '-')
|
|
22
|
+
.replaceAll('/', '_')
|
|
23
|
+
.replace(/=+$/u, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** URL path of one route's loader payload for a search-free document path. */
|
|
27
|
+
export function staticDataPayloadPath(
|
|
28
|
+
pathname: string,
|
|
29
|
+
routeId: string,
|
|
30
|
+
): string {
|
|
31
|
+
if (!routeId) throw new TypeError('A static data payload needs a route ID');
|
|
32
|
+
const directory = pathname.endsWith('/') ? pathname : `${pathname}/`;
|
|
33
|
+
return `${directory}${STATIC_DATA_DIRECTORY}/${encodeRouteId(routeId)}.json`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The client reads a payload file whole before replaying it. One response
|
|
38
|
+
* wrapped as a JSON string can grow up to six times through escaping; a
|
|
39
|
+
* deferred stream's frames are bounded only one by one, so prerendering
|
|
40
|
+
* checks the whole file against the same limit.
|
|
41
|
+
*/
|
|
42
|
+
export const STATIC_DATA_PAYLOAD_MAX_BYTES = MAX_DATA_BYTES * 6 + 1024;
|
|
43
|
+
|
|
44
|
+
const payloadTooLarge = (bytes: number | string) =>
|
|
45
|
+
new RangeError(
|
|
46
|
+
`A static loader payload of ${bytes} bytes exceeds the ${STATIC_DATA_PAYLOAD_MAX_BYTES} bytes the data client replays; reduce its (deferred) data or serve the route dynamically.`,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read a captured loader response with a running byte bound. Encoding only
|
|
51
|
+
* grows the body, so a body over the limit fails as soon as it crosses it,
|
|
52
|
+
* before an unbounded deferred stream is buffered.
|
|
53
|
+
*/
|
|
54
|
+
export async function readStaticDataPayloadBody(
|
|
55
|
+
response: Response,
|
|
56
|
+
): Promise<string> {
|
|
57
|
+
if (!response.body) return '';
|
|
58
|
+
const reader = response.body.getReader();
|
|
59
|
+
const decoder = new TextDecoder();
|
|
60
|
+
let bytes = 0;
|
|
61
|
+
let text = '';
|
|
62
|
+
for (;;) {
|
|
63
|
+
const part = await reader.read();
|
|
64
|
+
if (part.done) return text + decoder.decode();
|
|
65
|
+
bytes += part.value.byteLength;
|
|
66
|
+
if (bytes > STATIC_DATA_PAYLOAD_MAX_BYTES) {
|
|
67
|
+
const error = payloadTooLarge(`more than ${bytes}`);
|
|
68
|
+
void reader.cancel(error).catch(() => {});
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
text += decoder.decode(part.value, { stream: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The payload file's text, or an error when the client could not read it. */
|
|
76
|
+
export function encodeStaticDataPayload(payload: StaticDataPayload): string {
|
|
77
|
+
const text = JSON.stringify(payload);
|
|
78
|
+
const bytes = new TextEncoder().encode(text).byteLength;
|
|
79
|
+
if (bytes > STATIC_DATA_PAYLOAD_MAX_BYTES) throw payloadTooLarge(bytes);
|
|
80
|
+
return text;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isStaticDataPayload(
|
|
84
|
+
value: unknown,
|
|
85
|
+
): value is StaticDataPayload {
|
|
86
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
87
|
+
return false;
|
|
88
|
+
const payload = value as Record<string, unknown>;
|
|
89
|
+
return (
|
|
90
|
+
Number.isInteger(payload.status) &&
|
|
91
|
+
(payload.status as number) >= 200 &&
|
|
92
|
+
(payload.status as number) <= 599 &&
|
|
93
|
+
(payload.contentType === DATA_CONTENT_TYPE ||
|
|
94
|
+
payload.contentType === DATA_STREAM_CONTENT_TYPE) &&
|
|
95
|
+
typeof payload.body === 'string'
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether the current browser document was emitted by static prerendering. */
|
|
100
|
+
export function isPrerenderedDocument(): boolean {
|
|
101
|
+
return (
|
|
102
|
+
typeof document !== 'undefined' &&
|
|
103
|
+
document.querySelector(`meta[name="${PRERENDERED_DOCUMENT_META}"]`) !== null
|
|
104
|
+
);
|
|
105
|
+
}
|
package/src/data/tree.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { SerovalJSON } from 'seroval';
|
|
2
|
+
|
|
3
|
+
const MAX_NODES = 20_000;
|
|
4
|
+
const MAX_DEPTH = 300;
|
|
5
|
+
type NodeRecord = Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
function record(value: unknown): NodeRecord {
|
|
8
|
+
if (
|
|
9
|
+
value === null ||
|
|
10
|
+
typeof value !== 'object' ||
|
|
11
|
+
(Object.getPrototypeOf(value) !== Object.prototype &&
|
|
12
|
+
Object.getPrototypeOf(value) !== null)
|
|
13
|
+
) {
|
|
14
|
+
throw new TypeError('Malformed public data tree record');
|
|
15
|
+
}
|
|
16
|
+
return value as NodeRecord;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function exactFields(value: NodeRecord, fields: string[]): void {
|
|
20
|
+
if (
|
|
21
|
+
Reflect.ownKeys(value).length !== fields.length ||
|
|
22
|
+
fields.some(field => {
|
|
23
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, field);
|
|
24
|
+
return descriptor === undefined || !('value' in descriptor);
|
|
25
|
+
})
|
|
26
|
+
) {
|
|
27
|
+
throw new TypeError('Malformed public data node fields');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function integer(value: unknown, maximum: number): value is number {
|
|
32
|
+
return (
|
|
33
|
+
typeof value === 'number' &&
|
|
34
|
+
Number.isInteger(value) &&
|
|
35
|
+
value >= 0 &&
|
|
36
|
+
value <= maximum
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nodeArray(value: unknown): unknown[] {
|
|
41
|
+
if (
|
|
42
|
+
!Array.isArray(value) ||
|
|
43
|
+
Object.getPrototypeOf(value) !== Array.prototype ||
|
|
44
|
+
value.length > MAX_NODES ||
|
|
45
|
+
Reflect.ownKeys(value).length !== value.length + 1
|
|
46
|
+
) {
|
|
47
|
+
throw new TypeError('Malformed or oversized public data node array');
|
|
48
|
+
}
|
|
49
|
+
for (let index = 0; index < value.length; index++) {
|
|
50
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
51
|
+
if (!descriptor || !('value' in descriptor)) {
|
|
52
|
+
throw new TypeError('Public data node arrays must be dense values');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function pairs(value: unknown): { k: unknown[]; v: unknown[] } {
|
|
59
|
+
const entry = record(value);
|
|
60
|
+
exactFields(entry, ['k', 'v']);
|
|
61
|
+
const keys = nodeArray(entry.k);
|
|
62
|
+
const values = nodeArray(entry.v);
|
|
63
|
+
if (keys.length !== values.length) {
|
|
64
|
+
throw new TypeError('Public data keys and values have different lengths');
|
|
65
|
+
}
|
|
66
|
+
return { k: keys, v: values };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Validate inert, pure-data Seroval JSON before the decoder can coerce it. */
|
|
70
|
+
export function validateSerializedData(
|
|
71
|
+
value: unknown,
|
|
72
|
+
): asserts value is SerovalJSON {
|
|
73
|
+
const tree = record(value);
|
|
74
|
+
exactFields(tree, ['t', 'f', 'm']);
|
|
75
|
+
if (!integer(tree.f, 127)) {
|
|
76
|
+
throw new TypeError('Malformed public data feature flags');
|
|
77
|
+
}
|
|
78
|
+
const metadata = nodeArray(tree.m);
|
|
79
|
+
if (
|
|
80
|
+
metadata.some(id => !integer(id, MAX_NODES)) ||
|
|
81
|
+
new Set(metadata).size !== metadata.length
|
|
82
|
+
) {
|
|
83
|
+
throw new TypeError('Malformed public data reference metadata');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const declarations = new Map<number, number>();
|
|
87
|
+
let count = 0;
|
|
88
|
+
const declare = (id: unknown, tag: number): void => {
|
|
89
|
+
if (!integer(id, MAX_NODES)) {
|
|
90
|
+
throw new TypeError('Malformed public data reference id');
|
|
91
|
+
}
|
|
92
|
+
if (declarations.has(id)) {
|
|
93
|
+
throw new TypeError('Duplicate public data reference id');
|
|
94
|
+
}
|
|
95
|
+
declarations.set(id, tag);
|
|
96
|
+
};
|
|
97
|
+
const visit = (input: unknown, depth: number, mapSentinel = false): void => {
|
|
98
|
+
if (++count > MAX_NODES || depth > MAX_DEPTH) {
|
|
99
|
+
throw new TypeError('Public data tree exceeds its size or depth limit');
|
|
100
|
+
}
|
|
101
|
+
const node = record(input);
|
|
102
|
+
const tagDescriptor = Object.getOwnPropertyDescriptor(node, 't');
|
|
103
|
+
if (
|
|
104
|
+
!tagDescriptor ||
|
|
105
|
+
!('value' in tagDescriptor) ||
|
|
106
|
+
!integer(tagDescriptor.value, 26)
|
|
107
|
+
) {
|
|
108
|
+
throw new TypeError('Malformed public data node tag');
|
|
109
|
+
}
|
|
110
|
+
const tag = tagDescriptor.value as number;
|
|
111
|
+
|
|
112
|
+
if (mapSentinel && tag !== 4 && tag !== 26) {
|
|
113
|
+
throw new TypeError('Malformed public data Map sentinel');
|
|
114
|
+
}
|
|
115
|
+
switch (tag) {
|
|
116
|
+
case 0:
|
|
117
|
+
exactFields(node, ['t', 's']);
|
|
118
|
+
if (typeof node.s !== 'number' || !Number.isFinite(node.s)) {
|
|
119
|
+
throw new TypeError('Malformed public data number');
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
case 1:
|
|
123
|
+
exactFields(node, ['t', 's']);
|
|
124
|
+
if (typeof node.s !== 'string')
|
|
125
|
+
throw new TypeError('Malformed public data string');
|
|
126
|
+
return;
|
|
127
|
+
case 2:
|
|
128
|
+
exactFields(node, ['t', 's']);
|
|
129
|
+
if (!integer(node.s, 7))
|
|
130
|
+
throw new TypeError('Malformed public data constant');
|
|
131
|
+
return;
|
|
132
|
+
case 3:
|
|
133
|
+
exactFields(node, ['t', 's']);
|
|
134
|
+
if (
|
|
135
|
+
typeof node.s !== 'string' ||
|
|
136
|
+
node.s.length > 10_000 ||
|
|
137
|
+
!/^-?(?:0|[1-9]\d*)$/.test(node.s)
|
|
138
|
+
) {
|
|
139
|
+
throw new TypeError('Malformed public data bigint');
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
case 4: {
|
|
143
|
+
exactFields(node, ['t', 'i']);
|
|
144
|
+
if (!integer(node.i, MAX_NODES) || !declarations.has(node.i)) {
|
|
145
|
+
throw new TypeError('Missing or forward public data reference');
|
|
146
|
+
}
|
|
147
|
+
if ((declarations.get(node.i) === 26) !== mapSentinel) {
|
|
148
|
+
throw new TypeError(
|
|
149
|
+
'Public data references a Map sentinel as a value',
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
case 5:
|
|
155
|
+
exactFields(node, ['t', 'i', 's']);
|
|
156
|
+
if (typeof node.s !== 'string')
|
|
157
|
+
throw new TypeError('Malformed public data date');
|
|
158
|
+
if (node.s !== '') {
|
|
159
|
+
const timestamp = new Date(node.s);
|
|
160
|
+
if (
|
|
161
|
+
!Number.isFinite(timestamp.getTime()) ||
|
|
162
|
+
timestamp.toISOString() !== node.s
|
|
163
|
+
) {
|
|
164
|
+
throw new TypeError('Malformed public data date');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
declare(node.i, tag);
|
|
168
|
+
return;
|
|
169
|
+
case 6:
|
|
170
|
+
exactFields(node, ['t', 'i', 'c', 'm']);
|
|
171
|
+
if (
|
|
172
|
+
typeof node.c !== 'string' ||
|
|
173
|
+
typeof node.m !== 'string' ||
|
|
174
|
+
!/^[dgimsuvy]*$/.test(node.m) ||
|
|
175
|
+
new Set(node.m).size !== node.m.length ||
|
|
176
|
+
(node.m.includes('u') && node.m.includes('v'))
|
|
177
|
+
) {
|
|
178
|
+
throw new TypeError('Malformed public data regular expression');
|
|
179
|
+
}
|
|
180
|
+
declare(node.i, tag);
|
|
181
|
+
return;
|
|
182
|
+
case 7:
|
|
183
|
+
exactFields(node, ['t', 'i', 'a']);
|
|
184
|
+
declare(node.i, tag);
|
|
185
|
+
for (const item of nodeArray(node.a)) visit(item, depth + 1);
|
|
186
|
+
return;
|
|
187
|
+
case 8: {
|
|
188
|
+
exactFields(node, ['t', 'i', 'e', 'f']);
|
|
189
|
+
declare(node.i, tag);
|
|
190
|
+
const entries = pairs(node.e);
|
|
191
|
+
// The native decoder visits each key then its value, not all keys first.
|
|
192
|
+
for (let index = 0; index < entries.k.length; index++) {
|
|
193
|
+
visit(entries.k[index], depth + 1);
|
|
194
|
+
visit(entries.v[index], depth + 1);
|
|
195
|
+
}
|
|
196
|
+
visit(node.f, depth + 1, true);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
case 9:
|
|
200
|
+
exactFields(node, ['t', 'i', 'a', 'o']);
|
|
201
|
+
if (!integer(node.o, 3))
|
|
202
|
+
throw new TypeError('Malformed public data object flags');
|
|
203
|
+
declare(node.i, tag);
|
|
204
|
+
for (const item of nodeArray(node.a)) {
|
|
205
|
+
// Seroval encodes a sparse array hole as the literal 0, not a node.
|
|
206
|
+
if (item === 0) {
|
|
207
|
+
if (++count > MAX_NODES)
|
|
208
|
+
throw new TypeError('Public data tree exceeds its size limit');
|
|
209
|
+
} else visit(item, depth + 1);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
case 10:
|
|
213
|
+
case 11: {
|
|
214
|
+
exactFields(node, ['t', 'i', 'p', 'o']);
|
|
215
|
+
if (!integer(node.o, 3))
|
|
216
|
+
throw new TypeError('Malformed public data object flags');
|
|
217
|
+
declare(node.i, tag);
|
|
218
|
+
const properties = pairs(node.p);
|
|
219
|
+
if (
|
|
220
|
+
properties.k.some(key => typeof key !== 'string') ||
|
|
221
|
+
new Set(properties.k).size !== properties.k.length
|
|
222
|
+
) {
|
|
223
|
+
throw new TypeError('Malformed or duplicate public data object keys');
|
|
224
|
+
}
|
|
225
|
+
for (const item of properties.v) visit(item, depth + 1);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
case 26:
|
|
229
|
+
exactFields(node, ['t', 'i', 's']);
|
|
230
|
+
if (!mapSentinel || node.s !== 0) {
|
|
231
|
+
throw new TypeError('Unsupported public data special reference');
|
|
232
|
+
}
|
|
233
|
+
declare(node.i, tag);
|
|
234
|
+
return;
|
|
235
|
+
default:
|
|
236
|
+
throw new TypeError('Unsupported public data node');
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
visit(tree.t, 0);
|
|
241
|
+
if (metadata.some(id => !declarations.has(id as number))) {
|
|
242
|
+
throw new TypeError('Missing public data metadata reference');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { RendererIdentity } from '../identity';
|
|
2
|
+
|
|
3
|
+
export const DATA_PROTOCOL_VERSION = 1 as const;
|
|
4
|
+
export const DATA_CONTENT_TYPE = 'application/vnd.ultramodern.data+json';
|
|
5
|
+
export const DATA_STREAM_CONTENT_TYPE =
|
|
6
|
+
'application/vnd.ultramodern.data-stream+json';
|
|
7
|
+
export const LOADER_ID_PARAM = '__loader';
|
|
8
|
+
export const DIRECT_PARAM = '__ssrDirect';
|
|
9
|
+
|
|
10
|
+
export type DataOperation = 'loader' | 'action';
|
|
11
|
+
export type DataHeaders = [string, string][];
|
|
12
|
+
|
|
13
|
+
export interface DataResponseMetadata {
|
|
14
|
+
status: number;
|
|
15
|
+
statusText: string;
|
|
16
|
+
headers: DataHeaders;
|
|
17
|
+
cachePolicy: 'no-store' | 'private' | 'public';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PublicDataError {
|
|
21
|
+
name: string;
|
|
22
|
+
message: string;
|
|
23
|
+
stack?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type DataOutcome =
|
|
27
|
+
| { kind: 'success'; value: unknown; response: DataResponseMetadata }
|
|
28
|
+
| { kind: 'redirect'; location: string; response: DataResponseMetadata }
|
|
29
|
+
| {
|
|
30
|
+
kind: 'not-found';
|
|
31
|
+
value: unknown;
|
|
32
|
+
thrown: boolean;
|
|
33
|
+
response: DataResponseMetadata;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
kind: 'error';
|
|
37
|
+
error: PublicDataError;
|
|
38
|
+
data?: unknown;
|
|
39
|
+
thrown: boolean;
|
|
40
|
+
response: DataResponseMetadata;
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
kind: 'deferred';
|
|
44
|
+
critical: Record<string, unknown>;
|
|
45
|
+
deferred: Record<string, Promise<unknown>>;
|
|
46
|
+
response: DataResponseMetadata;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export interface DataHandlerInput<Context = unknown> {
|
|
50
|
+
request: Request;
|
|
51
|
+
routeId: string;
|
|
52
|
+
params: Readonly<Record<string, string>>;
|
|
53
|
+
context: Context;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type DataHandler<Context = unknown> = (
|
|
57
|
+
input: DataHandlerInput<Context>,
|
|
58
|
+
) => unknown | Promise<unknown>;
|
|
59
|
+
|
|
60
|
+
/** The native router supplies an authorized match, never a shared matcher. */
|
|
61
|
+
export interface SelectedDataRoute<Context = unknown> {
|
|
62
|
+
routeId: string;
|
|
63
|
+
params: Readonly<Record<string, string>>;
|
|
64
|
+
handler: DataHandler<Context>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DataWireEnvelope {
|
|
68
|
+
version: typeof DATA_PROTOCOL_VERSION;
|
|
69
|
+
identity: RendererIdentity;
|
|
70
|
+
routeId: string;
|
|
71
|
+
operation: DataOperation;
|
|
72
|
+
outcome: PublicDataOutcome;
|
|
73
|
+
deferredKeys?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Header values and request-owned context never enter a public payload. */
|
|
77
|
+
export type PublicDataOutcome =
|
|
78
|
+
| { kind: 'success'; value: unknown; status: number }
|
|
79
|
+
| { kind: 'redirect'; location: string; status: number }
|
|
80
|
+
| { kind: 'not-found'; value: unknown; thrown: boolean; status: number }
|
|
81
|
+
| {
|
|
82
|
+
kind: 'error';
|
|
83
|
+
error: PublicDataError;
|
|
84
|
+
data?: unknown;
|
|
85
|
+
thrown: boolean;
|
|
86
|
+
status: number;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** A deferred body is successful only when its terminal framing completes. */
|
|
90
|
+
export type DecodedDataOutcome = PublicDataOutcome & {
|
|
91
|
+
completion?: Promise<void>;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type DataStreamFrame =
|
|
95
|
+
| { type: 'initial'; envelope: DataWireEnvelope }
|
|
96
|
+
| { type: 'resolve'; key: string; value: unknown }
|
|
97
|
+
| { type: 'reject'; key: string; error: PublicDataError }
|
|
98
|
+
| { type: 'complete' };
|