@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.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/abort.js +65 -0
  3. package/dist/cjs/adapter.js +62 -0
  4. package/dist/cjs/data/body.js +78 -0
  5. package/dist/cjs/data/client.js +409 -0
  6. package/dist/cjs/data/codec.js +168 -0
  7. package/dist/cjs/data/index.js +125 -0
  8. package/dist/cjs/data/private.js +77 -0
  9. package/dist/cjs/data/routes.js +149 -0
  10. package/dist/cjs/data/server.js +791 -0
  11. package/dist/cjs/data/static.js +110 -0
  12. package/dist/cjs/data/tree.js +231 -0
  13. package/dist/cjs/data/types.js +56 -0
  14. package/dist/cjs/document.js +184 -0
  15. package/dist/cjs/entry-client.js +142 -0
  16. package/dist/cjs/entry-server.js +253 -0
  17. package/dist/cjs/federation.js +80 -0
  18. package/dist/cjs/identity.js +120 -0
  19. package/dist/cjs/index.js +46 -0
  20. package/dist/cjs/localization.js +48 -0
  21. package/dist/cjs/router.js +293 -0
  22. package/dist/cjs/server/assets.js +56 -0
  23. package/dist/cjs/server/dispatch.js +351 -0
  24. package/dist/cjs/server/index.js +57 -0
  25. package/dist/cjs/server/types.js +18 -0
  26. package/dist/cjs/server/worker.js +73 -0
  27. package/dist/cjs/session/cache.js +172 -0
  28. package/dist/cjs/session/index.js +63 -0
  29. package/dist/cjs/session/request.js +269 -0
  30. package/dist/cjs/session/types.js +18 -0
  31. package/dist/esm/abort.mjs +27 -0
  32. package/dist/esm/adapter.mjs +21 -0
  33. package/dist/esm/data/body.mjs +40 -0
  34. package/dist/esm/data/client.mjs +368 -0
  35. package/dist/esm/data/codec.mjs +112 -0
  36. package/dist/esm/data/index.mjs +7 -0
  37. package/dist/esm/data/private.mjs +39 -0
  38. package/dist/esm/data/routes.mjs +108 -0
  39. package/dist/esm/data/server.mjs +723 -0
  40. package/dist/esm/data/static.mjs +51 -0
  41. package/dist/esm/data/tree.mjs +193 -0
  42. package/dist/esm/data/types.mjs +6 -0
  43. package/dist/esm/document.mjs +131 -0
  44. package/dist/esm/entry-client.mjs +104 -0
  45. package/dist/esm/entry-server.mjs +215 -0
  46. package/dist/esm/federation.mjs +36 -0
  47. package/dist/esm/identity.mjs +63 -0
  48. package/dist/esm/index.mjs +1 -0
  49. package/dist/esm/localization.mjs +10 -0
  50. package/dist/esm/router.mjs +225 -0
  51. package/dist/esm/server/assets.mjs +15 -0
  52. package/dist/esm/server/dispatch.mjs +307 -0
  53. package/dist/esm/server/index.mjs +3 -0
  54. package/dist/esm/server/types.mjs +0 -0
  55. package/dist/esm/server/worker.mjs +35 -0
  56. package/dist/esm/session/cache.mjs +116 -0
  57. package/dist/esm/session/index.mjs +3 -0
  58. package/dist/esm/session/request.mjs +231 -0
  59. package/dist/esm/session/types.mjs +0 -0
  60. package/dist/esm-node/abort.mjs +28 -0
  61. package/dist/esm-node/adapter.mjs +22 -0
  62. package/dist/esm-node/data/body.mjs +41 -0
  63. package/dist/esm-node/data/client.mjs +369 -0
  64. package/dist/esm-node/data/codec.mjs +113 -0
  65. package/dist/esm-node/data/index.mjs +8 -0
  66. package/dist/esm-node/data/private.mjs +40 -0
  67. package/dist/esm-node/data/routes.mjs +109 -0
  68. package/dist/esm-node/data/server.mjs +724 -0
  69. package/dist/esm-node/data/static.mjs +52 -0
  70. package/dist/esm-node/data/tree.mjs +194 -0
  71. package/dist/esm-node/data/types.mjs +7 -0
  72. package/dist/esm-node/document.mjs +132 -0
  73. package/dist/esm-node/entry-client.mjs +105 -0
  74. package/dist/esm-node/entry-server.mjs +216 -0
  75. package/dist/esm-node/federation.mjs +37 -0
  76. package/dist/esm-node/identity.mjs +64 -0
  77. package/dist/esm-node/index.mjs +2 -0
  78. package/dist/esm-node/localization.mjs +11 -0
  79. package/dist/esm-node/router.mjs +226 -0
  80. package/dist/esm-node/server/assets.mjs +16 -0
  81. package/dist/esm-node/server/dispatch.mjs +308 -0
  82. package/dist/esm-node/server/index.mjs +4 -0
  83. package/dist/esm-node/server/types.mjs +1 -0
  84. package/dist/esm-node/server/worker.mjs +36 -0
  85. package/dist/esm-node/session/cache.mjs +117 -0
  86. package/dist/esm-node/session/index.mjs +4 -0
  87. package/dist/esm-node/session/request.mjs +232 -0
  88. package/dist/esm-node/session/types.mjs +1 -0
  89. package/dist/types/abort.d.ts +6 -0
  90. package/dist/types/adapter.d.ts +195 -0
  91. package/dist/types/data/body.d.ts +2 -0
  92. package/dist/types/data/client.d.ts +17 -0
  93. package/dist/types/data/codec.d.ts +14 -0
  94. package/dist/types/data/index.d.ts +8 -0
  95. package/dist/types/data/private.d.ts +6 -0
  96. package/dist/types/data/routes.d.ts +28 -0
  97. package/dist/types/data/server.d.ts +59 -0
  98. package/dist/types/data/static.d.ts +31 -0
  99. package/dist/types/data/tree.d.ts +3 -0
  100. package/dist/types/data/types.d.ts +104 -0
  101. package/dist/types/document.d.ts +76 -0
  102. package/dist/types/entry-client.d.ts +53 -0
  103. package/dist/types/entry-server.d.ts +63 -0
  104. package/dist/types/federation.d.ts +56 -0
  105. package/dist/types/identity.d.ts +27 -0
  106. package/dist/types/index.d.ts +1 -0
  107. package/dist/types/localization.d.ts +58 -0
  108. package/dist/types/router.d.ts +201 -0
  109. package/dist/types/server/assets.d.ts +14 -0
  110. package/dist/types/server/dispatch.d.ts +11 -0
  111. package/dist/types/server/index.d.ts +4 -0
  112. package/dist/types/server/types.d.ts +64 -0
  113. package/dist/types/server/worker.d.ts +25 -0
  114. package/dist/types/session/cache.d.ts +25 -0
  115. package/dist/types/session/index.d.ts +4 -0
  116. package/dist/types/session/request.d.ts +8 -0
  117. package/dist/types/session/types.d.ts +48 -0
  118. package/package.json +159 -4
  119. package/src/abort.ts +40 -0
  120. package/src/adapter.ts +255 -0
  121. package/src/data/body.ts +43 -0
  122. package/src/data/client.ts +501 -0
  123. package/src/data/codec.ts +247 -0
  124. package/src/data/index.ts +8 -0
  125. package/src/data/private.ts +68 -0
  126. package/src/data/routes.ts +201 -0
  127. package/src/data/server.ts +977 -0
  128. package/src/data/static.ts +105 -0
  129. package/src/data/tree.ts +244 -0
  130. package/src/data/types.ts +98 -0
  131. package/src/document.ts +338 -0
  132. package/src/entry-client.ts +236 -0
  133. package/src/entry-server.ts +393 -0
  134. package/src/federation.ts +111 -0
  135. package/src/identity.ts +128 -0
  136. package/src/index.ts +7 -0
  137. package/src/localization.ts +77 -0
  138. package/src/router.ts +475 -0
  139. package/src/server/assets.ts +60 -0
  140. package/src/server/dispatch.ts +523 -0
  141. package/src/server/index.ts +16 -0
  142. package/src/server/types.ts +94 -0
  143. package/src/server/worker.ts +94 -0
  144. package/src/session/cache.ts +177 -0
  145. package/src/session/index.ts +11 -0
  146. package/src/session/request.ts +324 -0
  147. package/src/session/types.ts +60 -0
@@ -0,0 +1,43 @@
1
+ import { DataProtocolError, MAX_DATA_BYTES } from './codec';
2
+
3
+ /** Bound actual bytes while reading, rather than buffering an arbitrary body. */
4
+ export async function readBoundedDataText(
5
+ response: Response,
6
+ signal?: AbortSignal,
7
+ maxBytes = MAX_DATA_BYTES,
8
+ ): Promise<string> {
9
+ if (signal?.aborted) {
10
+ // A response that arrived as its request aborted is released unread.
11
+ void response.body?.cancel(signal.reason).catch(() => undefined);
12
+ signal.throwIfAborted();
13
+ }
14
+ if (!response.body) return '';
15
+ const reader = response.body.getReader();
16
+ const decoder = new TextDecoder('utf-8', { fatal: true });
17
+ let bytes = 0;
18
+ let text = '';
19
+ const abort = () => {
20
+ void reader.cancel(signal?.reason).catch(() => undefined);
21
+ };
22
+ signal?.addEventListener('abort', abort, { once: true });
23
+ try {
24
+ while (true) {
25
+ const result = await reader.read();
26
+ signal?.throwIfAborted();
27
+ if (result.done) break;
28
+ bytes += result.value.byteLength;
29
+ if (bytes > maxBytes) {
30
+ throw new DataProtocolError('Public data exceeds its byte limit');
31
+ }
32
+ text += decoder.decode(result.value, { stream: true });
33
+ }
34
+ return text + decoder.decode();
35
+ } catch (error) {
36
+ // Release the producer without waiting on its cancel hook.
37
+ void reader.cancel(error).catch(() => undefined);
38
+ throw error;
39
+ } finally {
40
+ signal?.removeEventListener('abort', abort);
41
+ reader.releaseLock();
42
+ }
43
+ }
@@ -0,0 +1,501 @@
1
+ import { untilAborted } from '../abort';
2
+ import { assertRendererIdentity, type RendererIdentity } from '../identity';
3
+ import { readBoundedDataText } from './body';
4
+ import { DataProtocolError, MAX_DATA_BYTES, parsePublicData } from './codec';
5
+ import {
6
+ isPrerenderedDocument,
7
+ isStaticDataPayload,
8
+ STATIC_DATA_PAYLOAD_MAX_BYTES,
9
+ staticDataPayloadPath,
10
+ } from './static';
11
+ import {
12
+ DATA_CONTENT_TYPE,
13
+ DATA_PROTOCOL_VERSION,
14
+ DATA_STREAM_CONTENT_TYPE,
15
+ type DataOperation,
16
+ type DataWireEnvelope,
17
+ type DecodedDataOutcome,
18
+ DIRECT_PARAM,
19
+ LOADER_ID_PARAM,
20
+ type PublicDataError,
21
+ } from './types';
22
+
23
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
24
+ value !== null && typeof value === 'object' && !Array.isArray(value);
25
+
26
+ function assertKeys(
27
+ record: Record<string, unknown>,
28
+ keys: string[],
29
+ required: string[] = [],
30
+ ): void {
31
+ if (Object.keys(record).some(key => !keys.includes(key))) {
32
+ throw new DataProtocolError('Unknown data protocol field');
33
+ }
34
+ // A missing field must not decode as an `undefined` result.
35
+ if (required.some(key => !Object.hasOwn(record, key))) {
36
+ throw new DataProtocolError('Missing data protocol field');
37
+ }
38
+ }
39
+
40
+ function assertError(value: unknown): asserts value is PublicDataError {
41
+ if (!isRecord(value)) throw new DataProtocolError('Malformed public error');
42
+ assertKeys(value, ['name', 'message', 'stack']);
43
+ if (
44
+ typeof value['name'] !== 'string' ||
45
+ typeof value['message'] !== 'string' ||
46
+ (Object.hasOwn(value, 'stack') && typeof value['stack'] !== 'string')
47
+ ) {
48
+ throw new DataProtocolError('Malformed public error');
49
+ }
50
+ }
51
+
52
+ function assertEnvelope(
53
+ value: unknown,
54
+ expected: {
55
+ identity: RendererIdentity;
56
+ routeId: string;
57
+ operation: DataOperation;
58
+ },
59
+ ): asserts value is DataWireEnvelope {
60
+ if (
61
+ !isRecord(value) ||
62
+ value['version'] !== DATA_PROTOCOL_VERSION ||
63
+ value['routeId'] !== expected.routeId ||
64
+ value['operation'] !== expected.operation ||
65
+ !isRecord(value['identity']) ||
66
+ !isRecord(value['outcome'])
67
+ ) {
68
+ throw new DataProtocolError('Data protocol or route identity mismatch');
69
+ }
70
+ assertKeys(value, [
71
+ 'version',
72
+ 'identity',
73
+ 'routeId',
74
+ 'operation',
75
+ 'outcome',
76
+ 'deferredKeys',
77
+ ]);
78
+ assertKeys(value['identity'], [
79
+ 'renderer',
80
+ 'appId',
81
+ 'entryName',
82
+ 'protocolVersion',
83
+ 'buildId',
84
+ ]);
85
+ try {
86
+ assertRendererIdentity(
87
+ value['identity'] as unknown as RendererIdentity,
88
+ expected.identity,
89
+ );
90
+ } catch {
91
+ throw new DataProtocolError('Data renderer or build identity mismatch');
92
+ }
93
+ const outcome = value['outcome'];
94
+ if (
95
+ !Number.isInteger(outcome['status']) ||
96
+ (outcome['status'] as number) < 200 ||
97
+ (outcome['status'] as number) > 599
98
+ ) {
99
+ throw new DataProtocolError('Malformed data response status');
100
+ }
101
+ switch (outcome['kind']) {
102
+ case 'success':
103
+ assertKeys(outcome, ['kind', 'value', 'status'], ['value']);
104
+ break;
105
+ case 'redirect':
106
+ assertKeys(outcome, ['kind', 'location', 'status']);
107
+ if (
108
+ typeof outcome['location'] !== 'string' ||
109
+ ![301, 302, 303, 307, 308].includes(outcome['status'] as number)
110
+ ) {
111
+ throw new DataProtocolError('Malformed data redirect');
112
+ }
113
+ break;
114
+ case 'not-found':
115
+ assertKeys(outcome, ['kind', 'value', 'thrown', 'status'], ['value']);
116
+ if (outcome['status'] !== 404 || typeof outcome['thrown'] !== 'boolean')
117
+ throw new DataProtocolError('Malformed not-found outcome');
118
+ break;
119
+ case 'error':
120
+ assertKeys(outcome, ['kind', 'error', 'data', 'thrown', 'status']);
121
+ assertError(outcome['error']);
122
+ if (typeof outcome['thrown'] !== 'boolean')
123
+ throw new DataProtocolError('Malformed error outcome');
124
+ break;
125
+ default:
126
+ throw new DataProtocolError('Unknown data outcome');
127
+ }
128
+ if (Object.hasOwn(value, 'deferredKeys')) {
129
+ if (
130
+ !Array.isArray(value['deferredKeys']) ||
131
+ value['deferredKeys'].some(key => typeof key !== 'string') ||
132
+ new Set(value['deferredKeys']).size !== value['deferredKeys'].length ||
133
+ outcome['kind'] !== 'success' ||
134
+ !isRecord(outcome['value']) ||
135
+ value['deferredKeys'].some(key =>
136
+ Object.hasOwn(outcome['value'] as object, key),
137
+ )
138
+ ) {
139
+ throw new DataProtocolError('Malformed deferred data keys');
140
+ }
141
+ }
142
+ }
143
+
144
+ function dataError(error: PublicDataError): Error {
145
+ const result = new Error(error.message);
146
+ result.name = error.name;
147
+ if (error.stack !== undefined) result.stack = error.stack;
148
+ else delete result.stack;
149
+ return result;
150
+ }
151
+
152
+ async function readDeferredResponse(
153
+ response: Response,
154
+ expected: {
155
+ identity: RendererIdentity;
156
+ routeId: string;
157
+ operation: DataOperation;
158
+ },
159
+ signal?: AbortSignal,
160
+ ): Promise<DecodedDataOutcome> {
161
+ if (!response.body) throw new DataProtocolError('Missing deferred data body');
162
+ const reader = response.body.getReader();
163
+ const decoder = new TextDecoder('utf-8', { fatal: true });
164
+ let buffer = '';
165
+ let initialized = false;
166
+ let completed = false;
167
+ let initialResolve!: (outcome: DecodedDataOutcome) => void;
168
+ let initialReject!: (reason: unknown) => void;
169
+ const initial = new Promise<DecodedDataOutcome>((resolve, reject) => {
170
+ initialResolve = resolve;
171
+ initialReject = reject;
172
+ });
173
+ let completionResolve!: () => void;
174
+ let completionReject!: (reason: unknown) => void;
175
+ const completion = new Promise<void>((resolve, reject) => {
176
+ completionResolve = resolve;
177
+ completionReject = reject;
178
+ });
179
+ void completion.catch(() => undefined);
180
+ const pending = new Map<
181
+ string,
182
+ { resolve(value: unknown): void; reject(error: unknown): void }
183
+ >();
184
+ const reject = (error: unknown) => {
185
+ completionReject(error);
186
+ if (!initialized) initialReject(error);
187
+ for (const promise of pending.values()) promise.reject(error);
188
+ pending.clear();
189
+ };
190
+ const abort = () => {
191
+ const reason =
192
+ signal?.reason ??
193
+ new DOMException('The data request was aborted', 'AbortError');
194
+ reject(reason);
195
+ void reader.cancel(reason).catch(() => undefined);
196
+ };
197
+ if (signal?.aborted) abort();
198
+ else signal?.addEventListener('abort', abort, { once: true });
199
+ const consume = (text: string) => {
200
+ if (completed)
201
+ throw new DataProtocolError('Data frames follow stream completion');
202
+ const frame = parsePublicData(text);
203
+ if (!isRecord(frame))
204
+ throw new DataProtocolError('Malformed data stream frame');
205
+ if (frame['type'] === 'initial') {
206
+ assertKeys(frame, ['type', 'envelope']);
207
+ if (initialized)
208
+ throw new DataProtocolError('Duplicate initial data frame');
209
+ assertEnvelope(frame['envelope'], expected);
210
+ const envelope = frame['envelope'];
211
+ if (
212
+ !Array.isArray(envelope.deferredKeys) ||
213
+ envelope.outcome.kind !== 'success'
214
+ ) {
215
+ throw new DataProtocolError('Initial stream frame needs deferred keys');
216
+ }
217
+ // The decoded critical record itself receives the promises, keeping
218
+ // its identity, self-references and prototype.
219
+ const value = envelope.outcome.value as Record<string, unknown>;
220
+ for (const key of envelope.deferredKeys) {
221
+ const promise = new Promise<unknown>((resolve, rejectPromise) => {
222
+ pending.set(key, { resolve, reject: rejectPromise });
223
+ });
224
+ void promise.catch(() => undefined);
225
+ Object.defineProperty(value, key, {
226
+ value: promise,
227
+ writable: true,
228
+ enumerable: true,
229
+ configurable: true,
230
+ });
231
+ }
232
+ initialized = true;
233
+ const outcome: DecodedDataOutcome = { ...envelope.outcome, value };
234
+ Object.defineProperty(outcome, 'completion', { value: completion });
235
+ initialResolve(outcome);
236
+ return;
237
+ }
238
+ if (!initialized)
239
+ throw new DataProtocolError(
240
+ 'Deferred data arrived before the initial frame',
241
+ );
242
+ if (frame['type'] === 'complete') {
243
+ assertKeys(frame, ['type']);
244
+ if (pending.size !== 0)
245
+ throw new DataProtocolError(
246
+ 'Deferred stream completed with unresolved values',
247
+ );
248
+ completed = true;
249
+ return;
250
+ }
251
+ if (frame['type'] !== 'resolve' && frame['type'] !== 'reject')
252
+ throw new DataProtocolError('Unknown data stream frame');
253
+ if (frame['type'] === 'resolve')
254
+ assertKeys(frame, ['type', 'key', 'value'], ['value']);
255
+ else assertKeys(frame, ['type', 'key', 'error']);
256
+ if (typeof frame['key'] !== 'string' || !pending.has(frame['key']))
257
+ throw new DataProtocolError('Unknown or duplicate deferred data key');
258
+ const promise = pending.get(frame['key'])!;
259
+ if (frame['type'] === 'resolve') promise.resolve(frame['value']);
260
+ else {
261
+ assertError(frame['error']);
262
+ promise.reject(dataError(frame['error']));
263
+ }
264
+ pending.delete(frame['key']);
265
+ };
266
+ const append = (text: string) => {
267
+ buffer += text;
268
+ let lineEnd = buffer.indexOf('\n');
269
+ while (lineEnd !== -1) {
270
+ const line = buffer.slice(0, lineEnd);
271
+ buffer = buffer.slice(lineEnd + 1);
272
+ if (!line) throw new DataProtocolError('Empty data stream frame');
273
+ consume(line);
274
+ lineEnd = buffer.indexOf('\n');
275
+ }
276
+ if (new TextEncoder().encode(buffer).byteLength > MAX_DATA_BYTES) {
277
+ throw new DataProtocolError('Deferred data frame exceeds its size limit');
278
+ }
279
+ };
280
+ void (async () => {
281
+ try {
282
+ while (true) {
283
+ const chunk = await reader.read();
284
+ if (chunk.done) {
285
+ append(decoder.decode());
286
+ break;
287
+ }
288
+ // A transport may combine many frames in one chunk. Decode bounded pieces.
289
+ for (let offset = 0; offset < chunk.value.byteLength; offset += 65536) {
290
+ append(
291
+ decoder.decode(chunk.value.subarray(offset, offset + 65536), {
292
+ stream: true,
293
+ }),
294
+ );
295
+ }
296
+ }
297
+ if (buffer !== '' || !completed)
298
+ throw new DataProtocolError('Truncated deferred data stream');
299
+ completionResolve();
300
+ } catch (error) {
301
+ reject(error);
302
+ void reader.cancel(error).catch(() => undefined);
303
+ } finally {
304
+ signal?.removeEventListener('abort', abort);
305
+ reader.releaseLock();
306
+ }
307
+ })();
308
+ return initial;
309
+ }
310
+
311
+ export async function readDataResponse(
312
+ response: Response,
313
+ expected: {
314
+ identity: RendererIdentity;
315
+ routeId: string;
316
+ operation: DataOperation;
317
+ },
318
+ signal?: AbortSignal,
319
+ ): Promise<DecodedDataOutcome> {
320
+ // A response rejected before its body is read is released, never left
321
+ // downloading.
322
+ const discard = (reason: unknown) => {
323
+ void response.body?.cancel(reason).catch(() => undefined);
324
+ };
325
+ if (signal?.aborted) {
326
+ discard(signal.reason);
327
+ signal.throwIfAborted();
328
+ }
329
+ const contentType = response.headers
330
+ .get('content-type')
331
+ ?.split(';')[0]
332
+ ?.trim();
333
+ if (contentType === DATA_STREAM_CONTENT_TYPE)
334
+ return readDeferredResponse(response, expected, signal);
335
+ if (contentType !== DATA_CONTENT_TYPE) {
336
+ const error = new DataProtocolError(
337
+ `Expected an UltraModern data response, received HTTP ${response.status}`,
338
+ );
339
+ discard(error);
340
+ throw error;
341
+ }
342
+ const value = parsePublicData(await readBoundedDataText(response, signal));
343
+ signal?.throwIfAborted();
344
+ assertEnvelope(value, expected);
345
+ if (Object.hasOwn(value, 'deferredKeys'))
346
+ throw new DataProtocolError('Deferred values require the stream protocol');
347
+ return value.outcome;
348
+ }
349
+
350
+ /**
351
+ * A prerendered document can be hosted without a server. Its search-free
352
+ * loader payloads were captured beside it at build time; anything missing
353
+ * falls through to the server data request.
354
+ */
355
+ async function readStaticPayload(
356
+ url: URL,
357
+ routeId: string,
358
+ fetchData: typeof globalThis.fetch,
359
+ request: Request,
360
+ ): Promise<Response | undefined> {
361
+ if (url.search || !isPrerenderedDocument()) return undefined;
362
+ let response: Response;
363
+ try {
364
+ // An injected fetch that ignores the signal must not hold the loader.
365
+ response = await untilAborted(
366
+ fetchData(new URL(staticDataPayloadPath(url.pathname, routeId), url), {
367
+ credentials: 'same-origin',
368
+ signal: request.signal,
369
+ }),
370
+ request.signal,
371
+ );
372
+ } catch {
373
+ request.signal.throwIfAborted();
374
+ return undefined;
375
+ }
376
+ if (!response.ok) {
377
+ void response.body?.cancel().catch(() => undefined);
378
+ return undefined;
379
+ }
380
+ let payload: unknown;
381
+ try {
382
+ // Read like any data response: bounded, and cancelled with the request.
383
+ payload = JSON.parse(
384
+ await readBoundedDataText(
385
+ response,
386
+ request.signal,
387
+ STATIC_DATA_PAYLOAD_MAX_BYTES,
388
+ ),
389
+ );
390
+ } catch {
391
+ request.signal.throwIfAborted();
392
+ return undefined;
393
+ }
394
+ if (!isStaticDataPayload(payload)) return undefined;
395
+ return new Response(payload.body, {
396
+ status: payload.status,
397
+ headers: { 'content-type': payload.contentType },
398
+ });
399
+ }
400
+
401
+ /**
402
+ * Buffer a request body through its own reader, so aborting the request
403
+ * cancels a slow or never-ending source stream instead of waiting on it.
404
+ * A multipart form is the browser's own FormData encoding, which always
405
+ * ends: blob() lets the engine keep its file parts file-backed (Chromium
406
+ * drains FormData bodies as blob handles) instead of copying every upload
407
+ * chunk into memory.
408
+ */
409
+ async function bufferRequestBody(request: Request): Promise<Blob | null> {
410
+ if (!request.body) return null;
411
+ const { signal } = request;
412
+ if (
413
+ /^multipart\/form-data\b/iu.test(request.headers.get('content-type') ?? '')
414
+ )
415
+ return untilAborted(request.blob(), signal);
416
+ const reader = request.body.getReader();
417
+ const cancel = () => {
418
+ void reader.cancel(signal.reason).catch(() => {});
419
+ };
420
+ signal.addEventListener('abort', cancel, { once: true });
421
+ try {
422
+ const chunks: Uint8Array[] = [];
423
+ for (;;) {
424
+ const part = await reader.read();
425
+ signal.throwIfAborted();
426
+ if (part.done) break;
427
+ chunks.push(part.value);
428
+ }
429
+ return new Blob(chunks as BlobPart[], {
430
+ type: request.headers.get('content-type') ?? '',
431
+ });
432
+ } finally {
433
+ signal.removeEventListener('abort', cancel);
434
+ }
435
+ }
436
+
437
+ export function createDataClient(
438
+ routeId: string,
439
+ identity: RendererIdentity,
440
+ options: { fetch?: typeof globalThis.fetch } = {},
441
+ ): {
442
+ loader(input: { request: Request }): Promise<DecodedDataOutcome>;
443
+ action(input: { request: Request }): Promise<DecodedDataOutcome>;
444
+ } {
445
+ if (!routeId)
446
+ throw new DataProtocolError('A data client requires a route ID');
447
+ const fetchData = options.fetch ?? globalThis.fetch;
448
+ const execute = async (
449
+ operation: DataOperation,
450
+ input: { request: Request },
451
+ ) => {
452
+ const request = input.request;
453
+ request.signal.throwIfAborted();
454
+ if (operation === 'loader' && request.method !== 'GET')
455
+ throw new DataProtocolError('A loader client needs a GET Request');
456
+ if (operation === 'action' && ['GET', 'HEAD'].includes(request.method))
457
+ throw new DataProtocolError('An action client needs a mutation Request');
458
+ const url = new URL(request.url);
459
+ if (operation === 'loader') {
460
+ const replayed = await readStaticPayload(
461
+ url,
462
+ routeId,
463
+ fetchData,
464
+ request,
465
+ );
466
+ if (replayed)
467
+ return readDataResponse(
468
+ replayed,
469
+ { identity, routeId, operation },
470
+ request.signal,
471
+ );
472
+ }
473
+ url.searchParams.set(LOADER_ID_PARAM, routeId);
474
+ url.searchParams.set(DIRECT_PARAM, 'true');
475
+ // Browsers only stream request bodies over HTTP/2, so the action body
476
+ // is buffered instead of forwarding the source request's stream.
477
+ const proxyRequest = new Request(url, {
478
+ method: request.method,
479
+ headers: request.headers,
480
+ body: operation === 'action' ? await bufferRequestBody(request) : null,
481
+ signal: request.signal,
482
+ });
483
+ const response = await untilAborted(
484
+ fetchData(proxyRequest, {
485
+ credentials: 'same-origin',
486
+ redirect: 'manual',
487
+ signal: request.signal,
488
+ }),
489
+ request.signal,
490
+ );
491
+ return readDataResponse(
492
+ response,
493
+ { identity, routeId, operation },
494
+ request.signal,
495
+ );
496
+ };
497
+ return {
498
+ loader: input => execute('loader', input),
499
+ action: input => execute('action', input),
500
+ };
501
+ }