@fluojs/runtime 2.0.1 → 3.0.0

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 (127) hide show
  1. package/README.ko.md +278 -20
  2. package/README.md +278 -20
  3. package/dist/adapters/internal-request-response-factory.d.ts +1 -1
  4. package/dist/adapters/internal-request-response-factory.d.ts.map +1 -1
  5. package/dist/adapters/internal-request-response-factory.js +1 -1
  6. package/dist/adapters/request-response-factory.d.ts +20 -1
  7. package/dist/adapters/request-response-factory.d.ts.map +1 -1
  8. package/dist/adapters/request-response-factory.js +75 -22
  9. package/dist/bootstrap.d.ts +3 -9
  10. package/dist/bootstrap.d.ts.map +1 -1
  11. package/dist/bootstrap.js +179 -75
  12. package/dist/devtools/contracts.d.ts +3 -135
  13. package/dist/devtools/contracts.d.ts.map +1 -1
  14. package/dist/devtools/index.d.ts +2 -3
  15. package/dist/devtools/index.d.ts.map +1 -1
  16. package/dist/devtools/index.js +1 -2
  17. package/dist/devtools/snapshot.d.ts +1 -1
  18. package/dist/devtools/snapshot.d.ts.map +1 -1
  19. package/dist/devtools/snapshot.js +18 -17
  20. package/dist/devtools/studio-request-observer.d.ts +14 -0
  21. package/dist/devtools/studio-request-observer.d.ts.map +1 -0
  22. package/dist/devtools/studio-request-observer.js +112 -0
  23. package/dist/devtools/studio-runtime-config.d.ts +25 -0
  24. package/dist/devtools/studio-runtime-config.d.ts.map +1 -0
  25. package/dist/devtools/studio-runtime-config.js +59 -0
  26. package/dist/devtools/studio-runtime.d.ts +12 -18
  27. package/dist/devtools/studio-runtime.d.ts.map +1 -1
  28. package/dist/devtools/studio-runtime.js +25 -169
  29. package/dist/errors.d.ts +26 -0
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js +38 -0
  32. package/dist/health/health.d.ts +6 -2
  33. package/dist/health/health.d.ts.map +1 -1
  34. package/dist/health/health.js +3 -2
  35. package/dist/http-adapter-shared.d.ts +1 -1
  36. package/dist/http-adapter-shared.d.ts.map +1 -1
  37. package/dist/http-adapter-shared.js +1 -1
  38. package/dist/index.d.ts +6 -3
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +4 -1
  41. package/dist/internal/route-inspection-metadata.d.ts +30 -0
  42. package/dist/internal/route-inspection-metadata.d.ts.map +1 -0
  43. package/dist/internal/route-inspection-metadata.js +68 -0
  44. package/dist/internal.d.ts +6 -1
  45. package/dist/internal.d.ts.map +1 -1
  46. package/dist/internal.js +4 -1
  47. package/dist/module-definition.d.ts +10 -0
  48. package/dist/module-definition.d.ts.map +1 -0
  49. package/dist/module-definition.js +12 -0
  50. package/dist/module-graph.d.ts +41 -0
  51. package/dist/module-graph.d.ts.map +1 -1
  52. package/dist/module-graph.js +108 -13
  53. package/dist/multipart.d.ts +67 -3
  54. package/dist/multipart.d.ts.map +1 -1
  55. package/dist/multipart.js +665 -103
  56. package/dist/platform-component-registry.d.ts +9 -0
  57. package/dist/platform-component-registry.d.ts.map +1 -0
  58. package/dist/platform-component-registry.js +70 -0
  59. package/dist/platform-diagnostic-retention.d.ts +13 -0
  60. package/dist/platform-diagnostic-retention.d.ts.map +1 -0
  61. package/dist/platform-diagnostic-retention.js +43 -0
  62. package/dist/platform-shell-probe-reports.d.ts +8 -0
  63. package/dist/platform-shell-probe-reports.d.ts.map +1 -0
  64. package/dist/platform-shell-probe-reports.js +95 -0
  65. package/dist/platform-shell-probes.d.ts +16 -0
  66. package/dist/platform-shell-probes.d.ts.map +1 -0
  67. package/dist/platform-shell-probes.js +141 -0
  68. package/dist/platform-shell.d.ts +7 -13
  69. package/dist/platform-shell.d.ts.map +1 -1
  70. package/dist/platform-shell.js +58 -310
  71. package/dist/retryable-shutdown.d.ts +7 -0
  72. package/dist/retryable-shutdown.d.ts.map +1 -0
  73. package/dist/retryable-shutdown.js +20 -0
  74. package/dist/route-inspection.d.ts +56 -0
  75. package/dist/route-inspection.d.ts.map +1 -0
  76. package/dist/route-inspection.js +80 -0
  77. package/dist/tokens.d.ts +6 -4
  78. package/dist/tokens.d.ts.map +1 -1
  79. package/dist/tokens.js +5 -0
  80. package/dist/types.d.ts +53 -8
  81. package/dist/types.d.ts.map +1 -1
  82. package/dist/web.d.ts +22 -3
  83. package/dist/web.d.ts.map +1 -1
  84. package/dist/web.js +80 -34
  85. package/package.json +13 -26
  86. package/dist/internal-node.d.ts +0 -2
  87. package/dist/internal-node.d.ts.map +0 -1
  88. package/dist/internal-node.js +0 -1
  89. package/dist/logging/json-logger.d.ts +0 -8
  90. package/dist/logging/json-logger.d.ts.map +0 -1
  91. package/dist/logging/json-logger.js +0 -45
  92. package/dist/logging/logger.d.ts +0 -37
  93. package/dist/logging/logger.d.ts.map +0 -1
  94. package/dist/logging/logger.js +0 -103
  95. package/dist/node/internal-node-compression.d.ts +0 -22
  96. package/dist/node/internal-node-compression.d.ts.map +0 -1
  97. package/dist/node/internal-node-compression.js +0 -84
  98. package/dist/node/internal-node-request.d.ts +0 -162
  99. package/dist/node/internal-node-request.d.ts.map +0 -1
  100. package/dist/node/internal-node-request.js +0 -478
  101. package/dist/node/internal-node-response.d.ts +0 -28
  102. package/dist/node/internal-node-response.d.ts.map +0 -1
  103. package/dist/node/internal-node-response.js +0 -205
  104. package/dist/node/internal-node-shutdown.d.ts +0 -34
  105. package/dist/node/internal-node-shutdown.d.ts.map +0 -1
  106. package/dist/node/internal-node-shutdown.js +0 -83
  107. package/dist/node/internal-node.d.ts +0 -111
  108. package/dist/node/internal-node.d.ts.map +0 -1
  109. package/dist/node/internal-node.js +0 -283
  110. package/dist/node/node-compression.d.ts +0 -2
  111. package/dist/node/node-compression.d.ts.map +0 -1
  112. package/dist/node/node-compression.js +0 -1
  113. package/dist/node/node-request.d.ts +0 -2
  114. package/dist/node/node-request.d.ts.map +0 -1
  115. package/dist/node/node-request.js +0 -1
  116. package/dist/node/node-response.d.ts +0 -2
  117. package/dist/node/node-response.d.ts.map +0 -1
  118. package/dist/node/node-response.js +0 -1
  119. package/dist/node/node-shutdown.d.ts +0 -2
  120. package/dist/node/node-shutdown.d.ts.map +0 -1
  121. package/dist/node/node-shutdown.js +0 -1
  122. package/dist/node/node.d.ts +0 -2
  123. package/dist/node/node.d.ts.map +0 -1
  124. package/dist/node/node.js +0 -1
  125. package/dist/node.d.ts +0 -5
  126. package/dist/node.d.ts.map +0 -1
  127. package/dist/node.js +0 -3
package/dist/multipart.js CHANGED
@@ -1,11 +1,11 @@
1
- import { PayloadTooLargeException } from '@fluojs/http';
1
+ import { PayloadTooLargeException } from '@fluojs/http/portable';
2
2
 
3
3
  /**
4
4
  * Represents a single uploaded multipart file buffered in memory.
5
5
  */
6
6
 
7
7
  /**
8
- * Configures multipart parsing limits for file size, file count, and total payload size.
8
+ * Configures multipart parsing limits for fields, files, headers, and total payload size.
9
9
  */
10
10
 
11
11
  /**
@@ -16,53 +16,62 @@ import { PayloadTooLargeException } from '@fluojs/http';
16
16
  * Describes request-like multipart inputs accepted by the runtime parsers.
17
17
  */
18
18
 
19
+ /**
20
+ * A decoded non-file multipart field.
21
+ */
22
+
23
+ /**
24
+ * A multipart file whose bytes are read exactly once from a portable Web stream.
25
+ */
26
+
27
+ /**
28
+ * A typed item yielded by {@link parseMultipartStream}.
29
+ */
30
+
31
+ /**
32
+ * Signals that a multipart body has already been selected for buffered or streaming consumption.
33
+ */
34
+ export class MultipartBodyConsumedError extends Error {
35
+ constructor() {
36
+ super('Multipart request body has already been consumed.');
37
+ this.name = 'MultipartBodyConsumedError';
38
+ }
39
+ }
19
40
  const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
41
+ const DEFAULT_MAX_FIELD_SIZE = 1 * 1024 * 1024;
42
+ const DEFAULT_MAX_FIELDS = 100;
20
43
  const DEFAULT_MAX_FILES = 10;
44
+ const DEFAULT_MAX_HEADER_SIZE = 8 * 1024;
21
45
  const DEFAULT_MAX_TOTAL_SIZE = 10 * 1024 * 1024;
22
46
  const MULTIPART_BODY_LIMIT_MESSAGE = 'Multipart body exceeds the maximum size of';
23
47
  const TEXT_ENCODER = new TextEncoder();
48
+ const TEXT_DECODER = new TextDecoder();
49
+ const CONSUMED_MULTIPART_INPUTS = new WeakSet();
24
50
 
25
51
  /**
26
52
  * Parses a multipart request into string fields and in-memory uploaded files.
27
53
  *
28
54
  * @param request - Web `Request` or request-like input carrying a multipart body.
29
- * @param options - Multipart limits for file size, file count, and total payload size.
55
+ * @param options - Multipart limits for file size, file count, total size, field count, field size, and header size.
30
56
  * @returns Parsed string fields plus uploaded files buffered in memory.
31
- * @throws {PayloadTooLargeException} When the multipart payload, file count, or file size exceeds the configured limits.
57
+ * @throws {PayloadTooLargeException} When any configured file-size, file-count, total-size, field-count, field-size,
58
+ * or header-size limit is exceeded.
32
59
  */
33
60
  export async function parseMultipart(request, options = {}) {
34
- const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
35
- const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
36
- const maxTotalSize = options.maxTotalSize ?? DEFAULT_MAX_TOTAL_SIZE;
37
- const formData = await toWebRequest(request, maxTotalSize).formData();
38
61
  const fields = {};
39
62
  const files = [];
40
- let totalSize = 0;
41
- for (const [fieldname, value] of formData.entries()) {
42
- if (typeof value === 'string') {
43
- totalSize += TEXT_ENCODER.encode(value).byteLength;
44
- if (totalSize > maxTotalSize) {
45
- throw new PayloadTooLargeException(`${MULTIPART_BODY_LIMIT_MESSAGE} ${String(maxTotalSize)} bytes.`);
46
- }
47
- appendMultipartField(fields, fieldname, value);
63
+ for await (const part of createMultipartParts(request, options, false)) {
64
+ if (part.kind === 'field') {
65
+ appendMultipartField(fields, part.name, part.value);
48
66
  continue;
49
67
  }
50
- totalSize += value.size;
51
- if (totalSize > maxTotalSize) {
52
- throw new PayloadTooLargeException(`${MULTIPART_BODY_LIMIT_MESSAGE} ${String(maxTotalSize)} bytes.`);
53
- }
54
- if (files.length >= maxFiles) {
55
- throw new PayloadTooLargeException(`Exceeded maximum file count of ${String(maxFiles)}.`);
56
- }
57
- if (value.size > maxFileSize) {
58
- throw new PayloadTooLargeException(`File "${fieldname}" exceeds the maximum size of ${String(maxFileSize)} bytes.`);
59
- }
68
+ const buffer = new Uint8Array(await new Response(part.stream).arrayBuffer());
60
69
  files.push({
61
- buffer: new Uint8Array(await value.arrayBuffer()),
62
- fieldname,
63
- mimetype: value.type,
64
- originalname: value.name,
65
- size: value.size
70
+ buffer,
71
+ fieldname: part.name,
72
+ mimetype: part.contentType,
73
+ originalname: part.filename,
74
+ size: buffer.byteLength
66
75
  });
67
76
  }
68
77
  return {
@@ -70,110 +79,663 @@ export async function parseMultipart(request, options = {}) {
70
79
  files
71
80
  };
72
81
  }
73
- function appendMultipartField(fields, name, value) {
74
- const existing = fields[name];
75
- if (existing === undefined) {
76
- fields[name] = value;
77
- return;
82
+
83
+ /**
84
+ * Streams multipart fields and file parts from a request body.
85
+ *
86
+ * A yielded file stream must finish or be cancelled before requesting the next part. Calling
87
+ * this function selects the request body for streaming consumption; callers cannot also use
88
+ * {@link parseMultipart} for the same request body.
89
+ *
90
+ * @param request - Web `Request` or request-like input carrying a multipart body.
91
+ * @param options - Limits enforced while parsing headers, fields, files, and total input bytes.
92
+ * @returns An async iterator of typed field and file parts.
93
+ * @throws {MultipartBodyConsumedError} When buffered or streaming parsing already selected the body.
94
+ */
95
+ export function parseMultipartStream(request, options = {}) {
96
+ return createMultipartParts(request, options, true);
97
+ }
98
+ function createMultipartParts(request, options, useStreamingDefaults) {
99
+ markMultipartBodyConsumed(request);
100
+ return new MultipartPartIterator(new MultipartStreamParser(request, options, useStreamingDefaults));
101
+ }
102
+
103
+ /**
104
+ * Marks an input as selected for buffered or streaming multipart consumption.
105
+ *
106
+ * @param request - Request input whose body cannot be selected again.
107
+ * @internal
108
+ */
109
+ export function markMultipartBodyConsumed(request) {
110
+ const body = request instanceof Request ? request.body : request.body;
111
+ const inputs = [request];
112
+ if (body !== null && typeof body === 'object') {
113
+ inputs.push(body);
78
114
  }
79
- if (Array.isArray(existing)) {
80
- existing.push(value);
81
- return;
115
+ if (request instanceof Request && request.bodyUsed || inputs.some(input => CONSUMED_MULTIPART_INPUTS.has(input))) {
116
+ throw new MultipartBodyConsumedError();
117
+ }
118
+ for (const input of inputs) {
119
+ CONSUMED_MULTIPART_INPUTS.add(input);
82
120
  }
83
- fields[name] = [existing, value];
84
121
  }
85
- function toWebRequest(request, maxTotalSize) {
86
- if (request instanceof Request) {
87
- return createMultipartRequest(request.url, request.method, new Headers(request.headers), request.body, maxTotalSize);
122
+ class MultipartPartIterator {
123
+ closed = false;
124
+ constructor(parser) {
125
+ this.parser = parser;
126
+ }
127
+ [Symbol.asyncIterator]() {
128
+ return this;
129
+ }
130
+ async next() {
131
+ if (this.closed) {
132
+ return {
133
+ done: true,
134
+ value: undefined
135
+ };
136
+ }
137
+ const part = await this.parser.nextPart();
138
+ if (part === undefined) {
139
+ this.closed = true;
140
+ return {
141
+ done: true,
142
+ value: undefined
143
+ };
144
+ }
145
+ return {
146
+ done: false,
147
+ value: part
148
+ };
149
+ }
150
+ async return() {
151
+ this.closed = true;
152
+ this.parser.closeIfIncomplete();
153
+ return {
154
+ done: true,
155
+ value: undefined
156
+ };
88
157
  }
89
- const method = request.method ?? 'POST';
90
- const body = supportsRequestBody(method) ? resolveRequestBody(request) : undefined;
91
- return createMultipartRequest(request.url ?? 'http://localhost/', method, normalizeRequestHeaders(request.headers), body, maxTotalSize);
92
158
  }
93
- function resolveRequestBody(request) {
94
- if (request.body !== undefined) {
95
- return isAsyncIterableBody(request.body) ? createReadableStreamFromAsyncIterable(request.body) : request.body;
159
+ class MultipartStreamParser {
160
+ activeFile = false;
161
+ buffer = new Uint8Array();
162
+ cancellationStarted = false;
163
+ done = false;
164
+ failure;
165
+ fileController;
166
+ fileDelimiter;
167
+ initialDelimiter;
168
+ initialized = false;
169
+ maxFieldSize;
170
+ maxFields;
171
+ maxFileSize;
172
+ maxFiles;
173
+ maxHeaderSize;
174
+ maxTotalSize;
175
+ reader;
176
+ readerReadPending = false;
177
+ released = false;
178
+ signal;
179
+ terminalDrained = false;
180
+ totalSize = 0;
181
+ fieldCount = 0;
182
+ fileCount = 0;
183
+ partEnded = false;
184
+ constructor(request, options, useStreamingDefaults) {
185
+ const headers = normalizeRequestHeaders(request.headers);
186
+ const boundary = extractMultipartBoundary(headers.get('content-type'));
187
+ this.initialDelimiter = TEXT_ENCODER.encode(`--${boundary}`);
188
+ this.fileDelimiter = TEXT_ENCODER.encode(`\r\n--${boundary}`);
189
+ this.maxTotalSize = options.maxTotalSize ?? DEFAULT_MAX_TOTAL_SIZE;
190
+ this.maxFieldSize = options.maxFieldSize ?? (useStreamingDefaults ? DEFAULT_MAX_FIELD_SIZE : this.maxTotalSize);
191
+ this.maxFields = options.maxFields ?? (useStreamingDefaults ? DEFAULT_MAX_FIELDS : Number.MAX_SAFE_INTEGER);
192
+ this.maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
193
+ this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
194
+ this.maxHeaderSize = options.maxHeaderSize ?? (useStreamingDefaults ? DEFAULT_MAX_HEADER_SIZE : this.maxTotalSize);
195
+ this.signal = request instanceof Request ? request.signal : request.signal;
196
+ this.reader = resolveMultipartStreamBody(request).getReader();
197
+ this.signal?.addEventListener('abort', this.handleAbort, {
198
+ once: true
199
+ });
200
+ if (this.signal?.aborted) {
201
+ this.handleAbort();
202
+ }
203
+ const contentLengthError = getContentLengthError(headers, this.maxTotalSize);
204
+ if (contentLengthError) {
205
+ this.fail(contentLengthError);
206
+ } else if (this.signal?.aborted) {
207
+ this.fail(toAbortError(this.signal.reason));
208
+ }
96
209
  }
97
- if (typeof request[Symbol.asyncIterator] === 'function') {
98
- return createReadableStreamFromAsyncIterable(request);
210
+ async nextPart() {
211
+ this.throwIfFailed();
212
+ if (this.activeFile) {
213
+ throw new Error('Consume or cancel the active multipart file stream before reading the next part.');
214
+ }
215
+ if (!this.initialized) {
216
+ await this.consumeInitialDelimiter();
217
+ this.initialized = true;
218
+ }
219
+ if (this.done) {
220
+ await this.drainAfterClosingBoundary();
221
+ this.release();
222
+ return undefined;
223
+ }
224
+ const headers = await this.readHeaders();
225
+ let disposition;
226
+ try {
227
+ disposition = parseContentDisposition(headers['content-disposition']);
228
+ } catch (error) {
229
+ this.fail(error);
230
+ throw error;
231
+ }
232
+ if (disposition.filename === undefined) {
233
+ this.fieldCount += 1;
234
+ if (this.fieldCount > this.maxFields) {
235
+ this.fail(new PayloadTooLargeException(`Exceeded maximum field count of ${String(this.maxFields)}.`));
236
+ this.throwIfFailed();
237
+ }
238
+ const value = await this.readFieldValue();
239
+ return {
240
+ headers,
241
+ kind: 'field',
242
+ name: disposition.name,
243
+ value
244
+ };
245
+ }
246
+ this.fileCount += 1;
247
+ if (this.fileCount > this.maxFiles) {
248
+ this.fail(new PayloadTooLargeException(`Exceeded maximum file count of ${String(this.maxFiles)}.`));
249
+ this.throwIfFailed();
250
+ }
251
+ this.activeFile = true;
252
+ return {
253
+ contentType: headers['content-type'] ?? 'application/octet-stream',
254
+ filename: disposition.filename,
255
+ headers,
256
+ kind: 'file',
257
+ name: disposition.name,
258
+ stream: this.createFileStream(disposition.name)
259
+ };
260
+ }
261
+ closeIfIncomplete() {
262
+ if (!this.failure && (this.activeFile || !this.terminalDrained)) {
263
+ this.fail(new Error('Multipart parser has been cancelled.'));
264
+ }
265
+ }
266
+ handleAbort = () => {
267
+ this.fail(toAbortError(this.signal?.reason));
268
+ };
269
+ createFileStream(name) {
270
+ let size = 0;
271
+ return new ReadableStream({
272
+ cancel: async reason => {
273
+ this.fail(reason);
274
+ },
275
+ pull: async controller => {
276
+ this.fileController = controller;
277
+ try {
278
+ const chunk = await this.readPartChunk();
279
+ if (chunk === undefined) {
280
+ this.activeFile = false;
281
+ this.fileController = undefined;
282
+ controller.close();
283
+ return;
284
+ }
285
+ size += chunk.byteLength;
286
+ if (size > this.maxFileSize) {
287
+ const error = new PayloadTooLargeException(`File "${name}" exceeds the maximum size of ${String(this.maxFileSize)} bytes.`);
288
+ this.fail(error);
289
+ throw error;
290
+ }
291
+ controller.enqueue(chunk);
292
+ } catch (error) {
293
+ const parserError = toError(error);
294
+ this.activeFile = false;
295
+ this.fileController = undefined;
296
+ controller.error(parserError);
297
+ throw parserError;
298
+ }
299
+ }
300
+ }, {
301
+ highWaterMark: 0
302
+ });
303
+ }
304
+ async consumeInitialDelimiter() {
305
+ await this.ensure(this.initialDelimiter.byteLength + 2);
306
+ if (!matchesAt(this.buffer, this.initialDelimiter, 0)) {
307
+ this.fail(new Error('Multipart body does not start with its declared boundary.'));
308
+ this.throwIfFailed();
309
+ }
310
+ this.consume(this.initialDelimiter.byteLength);
311
+ const ending = this.consume(2);
312
+ if (equalsBytes(ending, TEXT_ENCODER.encode('--'))) {
313
+ this.done = true;
314
+ return;
315
+ }
316
+ if (!equalsBytes(ending, TEXT_ENCODER.encode('\r\n'))) {
317
+ this.fail(new Error('Multipart boundary is malformed.'));
318
+ this.throwIfFailed();
319
+ }
320
+ }
321
+ async readHeaders() {
322
+ const headerBytes = await this.readUntil(TEXT_ENCODER.encode('\r\n\r\n'), this.maxHeaderSize, `Multipart part headers exceed the maximum size of ${String(this.maxHeaderSize)} bytes.`);
323
+ const headers = {};
324
+ for (const line of TEXT_DECODER.decode(headerBytes).split('\r\n')) {
325
+ const separator = line.indexOf(':');
326
+ if (separator <= 0) {
327
+ this.fail(new Error('Multipart part contains a malformed header.'));
328
+ this.throwIfFailed();
329
+ }
330
+ const name = line.slice(0, separator).trim().toLowerCase();
331
+ const value = line.slice(separator + 1).trim();
332
+ if (headers[name] !== undefined) {
333
+ this.fail(new Error(`Multipart part contains a repeated "${name}" header.`));
334
+ this.throwIfFailed();
335
+ }
336
+ headers[name] = value;
337
+ }
338
+ return Object.freeze(headers);
339
+ }
340
+ async readFieldValue() {
341
+ const chunks = [];
342
+ let size = 0;
343
+ while (true) {
344
+ const chunk = await this.readPartChunk();
345
+ if (chunk === undefined) {
346
+ break;
347
+ }
348
+ size += chunk.byteLength;
349
+ if (size > this.maxFieldSize) {
350
+ this.fail(new PayloadTooLargeException(`Field exceeds the maximum size of ${String(this.maxFieldSize)} bytes.`));
351
+ this.throwIfFailed();
352
+ }
353
+ chunks.push(chunk);
354
+ }
355
+ return TEXT_DECODER.decode(concatBytes(chunks, size));
356
+ }
357
+ async readPartChunk() {
358
+ if (this.partEnded) {
359
+ this.partEnded = false;
360
+ if (this.done) {
361
+ await this.drainAfterClosingBoundary();
362
+ }
363
+ return undefined;
364
+ }
365
+ while (true) {
366
+ this.throwIfFailed();
367
+ const delimiterIndex = indexOfBytes(this.buffer, this.fileDelimiter);
368
+ if (delimiterIndex !== -1) {
369
+ const suffixOffset = delimiterIndex + this.fileDelimiter.byteLength;
370
+ const suffixLength = 2;
371
+ if (this.buffer.byteLength < suffixOffset + suffixLength) {
372
+ if (delimiterIndex > 0) {
373
+ return this.consume(delimiterIndex);
374
+ }
375
+ await this.readMore();
376
+ continue;
377
+ }
378
+ const ending = this.buffer.slice(suffixOffset, suffixOffset + suffixLength);
379
+ if (equalsBytes(ending, TEXT_ENCODER.encode('--'))) {
380
+ const chunk = this.consume(delimiterIndex);
381
+ this.consume(this.fileDelimiter.byteLength + suffixLength);
382
+ this.done = true;
383
+ this.partEnded = chunk.byteLength > 0;
384
+ if (chunk.byteLength === 0) {
385
+ await this.drainAfterClosingBoundary();
386
+ return undefined;
387
+ }
388
+ return chunk;
389
+ } else if (equalsBytes(ending, TEXT_ENCODER.encode('\r\n'))) {
390
+ const chunk = this.consume(delimiterIndex);
391
+ this.consume(this.fileDelimiter.byteLength + suffixLength);
392
+ this.partEnded = chunk.byteLength > 0;
393
+ return chunk.byteLength === 0 ? undefined : chunk;
394
+ }
395
+ return this.consume(suffixOffset);
396
+ }
397
+ const retainedBytes = this.fileDelimiter.byteLength - 1;
398
+ if (this.buffer.byteLength > retainedBytes) {
399
+ return this.consume(this.buffer.byteLength - retainedBytes);
400
+ }
401
+ await this.readMore();
402
+ }
403
+ }
404
+ async readUntil(delimiter, limit, message) {
405
+ while (true) {
406
+ this.throwIfFailed();
407
+ const delimiterIndex = indexOfBytes(this.buffer, delimiter);
408
+ if (delimiterIndex !== -1) {
409
+ const value = this.consume(delimiterIndex);
410
+ this.consume(delimiter.byteLength);
411
+ if (value.byteLength > limit) {
412
+ this.fail(new PayloadTooLargeException(message));
413
+ this.throwIfFailed();
414
+ }
415
+ return value;
416
+ }
417
+ const confirmedSize = this.buffer.byteLength - trailingDelimiterPrefixLength(this.buffer, delimiter);
418
+ if (confirmedSize > limit) {
419
+ this.fail(new PayloadTooLargeException(message));
420
+ this.throwIfFailed();
421
+ }
422
+ await this.readMore();
423
+ }
424
+ }
425
+ async ensure(size) {
426
+ while (this.buffer.byteLength < size) {
427
+ await this.readMore();
428
+ }
429
+ }
430
+ async readMore(allowEof = false) {
431
+ this.throwIfFailed();
432
+ this.readerReadPending = true;
433
+ let result;
434
+ try {
435
+ result = await this.reader.read();
436
+ } catch (error) {
437
+ this.fail(error);
438
+ throw error;
439
+ } finally {
440
+ this.readerReadPending = false;
441
+ if (this.failure) {
442
+ this.release();
443
+ }
444
+ }
445
+ this.throwIfFailed();
446
+ if (result.done) {
447
+ if (allowEof) {
448
+ return false;
449
+ }
450
+ const error = new Error('Multipart body ended before its closing boundary.');
451
+ this.fail(error);
452
+ throw error;
453
+ }
454
+ const chunk = result.value;
455
+ this.totalSize += chunk.byteLength;
456
+ if (this.totalSize > this.maxTotalSize) {
457
+ this.fail(new PayloadTooLargeException(`${MULTIPART_BODY_LIMIT_MESSAGE} ${String(this.maxTotalSize)} bytes.`));
458
+ this.throwIfFailed();
459
+ }
460
+ this.buffer = concatBytes([this.buffer, chunk], this.buffer.byteLength + chunk.byteLength);
461
+ return true;
462
+ }
463
+ consume(size) {
464
+ const value = this.buffer.slice(0, size);
465
+ this.buffer = this.buffer.slice(size);
466
+ return value;
467
+ }
468
+ async drainAfterClosingBoundary() {
469
+ if (this.terminalDrained) {
470
+ return;
471
+ }
472
+ while (true) {
473
+ this.buffer = new Uint8Array();
474
+ if (!(await this.readMore(true))) {
475
+ this.terminalDrained = true;
476
+ this.release();
477
+ return;
478
+ }
479
+ }
480
+ }
481
+ fail(reason) {
482
+ if (this.failure) {
483
+ return;
484
+ }
485
+ const error = toError(reason);
486
+ this.failure = error;
487
+ this.done = true;
488
+ this.fileController?.error(error);
489
+ this.fileController = undefined;
490
+ this.activeFile = false;
491
+ this.signal?.removeEventListener('abort', this.handleAbort);
492
+ if (!this.cancellationStarted) {
493
+ this.cancellationStarted = true;
494
+ void this.reader.cancel(reason).then(() => this.release(), () => this.release());
495
+ }
496
+ this.release();
497
+ }
498
+ release() {
499
+ if (this.released || this.readerReadPending) {
500
+ return;
501
+ }
502
+ this.released = true;
503
+ this.signal?.removeEventListener('abort', this.handleAbort);
504
+ this.reader.releaseLock();
505
+ }
506
+ throwIfFailed() {
507
+ if (this.failure) {
508
+ throw this.failure;
509
+ }
510
+ }
511
+ }
512
+ function extractMultipartBoundary(contentType) {
513
+ const match = contentType?.match(/(?:^|;)\s*boundary=(?:"([^"]+)"|([^;]+))/i);
514
+ const boundary = match?.[1] ?? match?.[2]?.trim();
515
+ if (!boundary) {
516
+ throw new Error('Multipart requests require a boundary parameter.');
517
+ }
518
+ return boundary;
519
+ }
520
+ function parseContentDisposition(value) {
521
+ if (value === undefined) {
522
+ throw new Error('Multipart parts require a form-data Content-Disposition header.');
523
+ }
524
+ let index = skipWhitespace(value, 0);
525
+ const dispositionType = readToken(value, index);
526
+ if (!dispositionType || dispositionType.value.toLowerCase() !== 'form-data') {
527
+ throw new Error('Multipart parts require a form-data Content-Disposition header.');
528
+ }
529
+ index = dispositionType.end;
530
+ const parameters = new Map();
531
+ while (index < value.length) {
532
+ index = skipWhitespace(value, index);
533
+ if (index === value.length) {
534
+ break;
535
+ }
536
+ if (value[index] !== ';') {
537
+ throw new Error('Multipart Content-Disposition parameters must be separated by semicolons.');
538
+ }
539
+ index = skipWhitespace(value, index + 1);
540
+ const parameterName = readToken(value, index);
541
+ if (!parameterName) {
542
+ throw new Error('Multipart Content-Disposition parameters require token names.');
543
+ }
544
+ index = skipWhitespace(value, parameterName.end);
545
+ if (value[index] !== '=') {
546
+ throw new Error('Multipart Content-Disposition parameters require values.');
547
+ }
548
+ index = skipWhitespace(value, index + 1);
549
+ const parameterValue = value[index] === '"' ? readQuotedString(value, index) : readToken(value, index);
550
+ if (!parameterValue) {
551
+ throw new Error('Multipart Content-Disposition parameters require token or quoted-string values.');
552
+ }
553
+ if (!parameters.has(parameterName.value.toLowerCase())) {
554
+ parameters.set(parameterName.value.toLowerCase(), parameterValue.value);
555
+ }
556
+ index = parameterValue.end;
557
+ }
558
+ const name = parameters.get('name');
559
+ if (name === undefined) {
560
+ throw new Error('Multipart parts require a name parameter.');
561
+ }
562
+ return {
563
+ filename: parameters.get('filename'),
564
+ name
565
+ };
566
+ }
567
+ function readQuotedString(value, start) {
568
+ let index = start + 1;
569
+ let result = '';
570
+ while (index < value.length) {
571
+ const character = value[index];
572
+ if (character === '\r' || character === '\n') {
573
+ return undefined;
574
+ }
575
+ if (character === '"') {
576
+ return {
577
+ end: index + 1,
578
+ value: result
579
+ };
580
+ }
581
+ if (character === '\\') {
582
+ index += 1;
583
+ const escaped = value[index];
584
+ if (escaped === undefined || escaped === '\r' || escaped === '\n') {
585
+ return undefined;
586
+ }
587
+ result += escaped;
588
+ index += 1;
589
+ continue;
590
+ }
591
+ result += character;
592
+ index += 1;
99
593
  }
100
594
  return undefined;
101
595
  }
102
- function supportsRequestBody(method) {
103
- return method !== 'GET' && method !== 'HEAD';
596
+ function readToken(value, start) {
597
+ let end = start;
598
+ while (end < value.length && isTokenCharacter(value[end])) {
599
+ end += 1;
600
+ }
601
+ return end === start ? undefined : {
602
+ end,
603
+ value: value.slice(start, end)
604
+ };
104
605
  }
105
- function isAsyncIterableBody(body) {
106
- return body !== null && typeof body === 'object' && Symbol.asyncIterator in body;
606
+ function isTokenCharacter(character) {
607
+ return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]$/.test(character);
107
608
  }
108
- function isStreamingBody(body) {
109
- return body instanceof ReadableStream || isAsyncIterableBody(body);
609
+ function skipWhitespace(value, start) {
610
+ let index = start;
611
+ while (value[index] === ' ' || value[index] === '\t') {
612
+ index += 1;
613
+ }
614
+ return index;
110
615
  }
111
- function createReadableStreamFromAsyncIterable(source) {
112
- const iterator = source[Symbol.asyncIterator]();
113
- return new ReadableStream({
114
- async cancel() {
115
- await iterator.return?.();
116
- },
117
- async pull(controller) {
118
- const {
119
- done,
120
- value
121
- } = await iterator.next();
122
- if (done) {
123
- controller.close();
124
- return;
125
- }
126
- controller.enqueue(value);
616
+ function resolveMultipartStreamBody(request) {
617
+ if (request instanceof Request) {
618
+ if (!request.body) {
619
+ throw new Error('Multipart requests require a body.');
127
620
  }
128
- });
621
+ return request.body;
622
+ }
623
+ const body = request.body ?? (typeof request[Symbol.asyncIterator] === 'function' ? request : undefined);
624
+ if (body instanceof ReadableStream) {
625
+ return body;
626
+ }
627
+ if (body && isAsyncIterableBody(body)) {
628
+ return createReadableStreamFromAsyncIterable(body);
629
+ }
630
+ if (body === undefined || body === null) {
631
+ throw new Error('Multipart requests require a body.');
632
+ }
633
+ const converted = new Request(request.url ?? 'http://localhost/', {
634
+ body: body,
635
+ method: request.method ?? 'POST'
636
+ }).body;
637
+ if (!converted) {
638
+ throw new Error('Multipart requests require a body.');
639
+ }
640
+ return converted;
129
641
  }
130
- function createMultipartRequest(url, method, headers, body, maxTotalSize) {
642
+ function getContentLengthError(headers, maxTotalSize) {
131
643
  const contentLength = headers.get('content-length');
132
- if (contentLength !== null) {
133
- const parsedContentLength = Number(contentLength);
134
- if (Number.isFinite(parsedContentLength) && parsedContentLength > maxTotalSize) {
135
- throw new PayloadTooLargeException(`${MULTIPART_BODY_LIMIT_MESSAGE} ${String(maxTotalSize)} bytes.`);
644
+ if (contentLength === null) {
645
+ return undefined;
646
+ }
647
+ const parsedContentLength = Number(contentLength);
648
+ if (Number.isFinite(parsedContentLength) && parsedContentLength > maxTotalSize) {
649
+ return new PayloadTooLargeException(`${MULTIPART_BODY_LIMIT_MESSAGE} ${String(maxTotalSize)} bytes.`);
650
+ }
651
+ return undefined;
652
+ }
653
+ function indexOfBytes(input, needle) {
654
+ if (needle.byteLength === 0 || needle.byteLength > input.byteLength) {
655
+ return -1;
656
+ }
657
+ outer: for (let index = 0; index <= input.byteLength - needle.byteLength; index += 1) {
658
+ for (let offset = 0; offset < needle.byteLength; offset += 1) {
659
+ if (input[index + offset] !== needle[offset]) {
660
+ continue outer;
661
+ }
136
662
  }
663
+ return index;
137
664
  }
138
- const init = {
139
- headers,
140
- method
141
- };
142
- if (body !== undefined) {
143
- const limitedBody = body !== null && isStreamingBody(body) ? createByteLimitedReadableStream(body, maxTotalSize) : body;
144
- init.body = limitedBody;
145
- if (limitedBody instanceof ReadableStream) {
146
- init.duplex = 'half';
665
+ return -1;
666
+ }
667
+ function matchesAt(input, needle, start) {
668
+ if (start + needle.byteLength > input.byteLength) {
669
+ return false;
670
+ }
671
+ for (let offset = 0; offset < needle.byteLength; offset += 1) {
672
+ if (input[start + offset] !== needle[offset]) {
673
+ return false;
674
+ }
675
+ }
676
+ return true;
677
+ }
678
+ function equalsBytes(left, right) {
679
+ return left.byteLength === right.byteLength && matchesAt(left, right, 0);
680
+ }
681
+ function concatBytes(chunks, size) {
682
+ const result = new Uint8Array(size);
683
+ let offset = 0;
684
+ for (const chunk of chunks) {
685
+ result.set(chunk, offset);
686
+ offset += chunk.byteLength;
687
+ }
688
+ return result;
689
+ }
690
+ function trailingDelimiterPrefixLength(buffer, delimiter) {
691
+ const maximum = Math.min(buffer.byteLength, delimiter.byteLength - 1);
692
+ for (let size = maximum; size > 0; size -= 1) {
693
+ if (equalsBytes(buffer.slice(buffer.byteLength - size), delimiter.slice(0, size))) {
694
+ return size;
147
695
  }
148
696
  }
149
- return new Request(url, init);
697
+ return 0;
150
698
  }
151
- function createByteLimitedReadableStream(stream, maxTotalSize) {
152
- const source = stream instanceof ReadableStream ? stream : createReadableStreamFromAsyncIterable(stream);
153
- const reader = source.getReader();
154
- let totalSize = 0;
699
+ function toAbortError(reason) {
700
+ return reason instanceof Error ? reason : new Error('Multipart request was aborted.');
701
+ }
702
+ function toError(error) {
703
+ return error instanceof Error ? error : new Error(String(error));
704
+ }
705
+ function appendMultipartField(fields, name, value) {
706
+ const existing = fields[name];
707
+ if (existing === undefined) {
708
+ fields[name] = value;
709
+ return;
710
+ }
711
+ if (Array.isArray(existing)) {
712
+ existing.push(value);
713
+ return;
714
+ }
715
+ fields[name] = [existing, value];
716
+ }
717
+ function isAsyncIterableBody(body) {
718
+ return body !== null && typeof body === 'object' && Symbol.asyncIterator in body;
719
+ }
720
+ function createReadableStreamFromAsyncIterable(source) {
721
+ const iterator = source[Symbol.asyncIterator]();
155
722
  return new ReadableStream({
156
- async cancel(reason) {
157
- await reader.cancel(reason);
723
+ async cancel() {
724
+ await iterator.return?.();
158
725
  },
159
726
  async pull(controller) {
160
727
  const {
161
728
  done,
162
729
  value
163
- } = await reader.read();
730
+ } = await iterator.next();
164
731
  if (done) {
165
732
  controller.close();
166
733
  return;
167
734
  }
168
- totalSize += value.byteLength;
169
- if (totalSize > maxTotalSize) {
170
- const error = new PayloadTooLargeException(`${MULTIPART_BODY_LIMIT_MESSAGE} ${String(maxTotalSize)} bytes.`);
171
- await reader.cancel(error.message);
172
- controller.error(error);
173
- return;
174
- }
175
735
  controller.enqueue(value);
176
736
  }
737
+ }, {
738
+ highWaterMark: 0
177
739
  });
178
740
  }
179
741
  function normalizeRequestHeaders(headers) {