@fluojs/runtime 1.0.0-beta.1 → 1.0.0-beta.10

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 (52) hide show
  1. package/README.ko.md +39 -3
  2. package/README.md +39 -3
  3. package/dist/adapters/request-response-factory.d.ts +9 -0
  4. package/dist/adapters/request-response-factory.d.ts.map +1 -1
  5. package/dist/adapters/request-response-factory.js +14 -0
  6. package/dist/bootstrap.d.ts.map +1 -1
  7. package/dist/bootstrap.js +327 -60
  8. package/dist/health/diagnostics.d.ts +38 -0
  9. package/dist/health/diagnostics.d.ts.map +1 -1
  10. package/dist/health/diagnostics.js +48 -0
  11. package/dist/health/health.d.ts +21 -0
  12. package/dist/health/health.d.ts.map +1 -1
  13. package/dist/health/health.js +27 -0
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/logging/json-logger.d.ts +5 -0
  18. package/dist/logging/json-logger.d.ts.map +1 -1
  19. package/dist/logging/json-logger.js +6 -0
  20. package/dist/logging/logger.d.ts +26 -1
  21. package/dist/logging/logger.d.ts.map +1 -1
  22. package/dist/logging/logger.js +54 -5
  23. package/dist/module-graph.d.ts +16 -0
  24. package/dist/module-graph.d.ts.map +1 -1
  25. package/dist/module-graph.js +304 -8
  26. package/dist/node/internal-node-compression.d.ts +15 -0
  27. package/dist/node/internal-node-compression.d.ts.map +1 -1
  28. package/dist/node/internal-node-compression.js +16 -0
  29. package/dist/node/internal-node-request.d.ts +128 -0
  30. package/dist/node/internal-node-request.d.ts.map +1 -1
  31. package/dist/node/internal-node-request.js +321 -40
  32. package/dist/node/internal-node-response.d.ts +21 -1
  33. package/dist/node/internal-node-response.d.ts.map +1 -1
  34. package/dist/node/internal-node-response.js +42 -3
  35. package/dist/node/internal-node.d.ts +43 -6
  36. package/dist/node/internal-node.d.ts.map +1 -1
  37. package/dist/node/internal-node.js +65 -9
  38. package/dist/node/node-request.d.ts +1 -1
  39. package/dist/node/node-request.d.ts.map +1 -1
  40. package/dist/node/node-request.js +1 -1
  41. package/dist/platform-shell.d.ts +4 -0
  42. package/dist/platform-shell.d.ts.map +1 -1
  43. package/dist/platform-shell.js +72 -20
  44. package/dist/request-transaction.d.ts +28 -0
  45. package/dist/request-transaction.d.ts.map +1 -1
  46. package/dist/request-transaction.js +33 -0
  47. package/dist/types.d.ts +29 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/web.d.ts +9 -1
  50. package/dist/web.d.ts.map +1 -1
  51. package/dist/web.js +207 -56
  52. package/package.json +6 -6
package/dist/web.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BadRequestException, createErrorResponse, HttpException, InternalServerErrorException, PayloadTooLargeException } from '@fluojs/http';
2
+ import { attachFrameworkRequestNativeRouteHandoff, consumeRawRequestNativeRouteHandoff } from '@fluojs/http/internal';
2
3
  import { parseMultipart } from './multipart.js';
3
4
  import { dispatchWithRequestResponseFactory } from './adapters/request-response-factory.js';
4
5
  const DEFAULT_MAX_BODY_SIZE = 1 * 1024 * 1024;
@@ -93,12 +94,12 @@ class MutableWebFrameworkResponse {
93
94
  statusCode;
94
95
  statusSet;
95
96
  finalizedResponse;
96
- responseStream = new WebResponseStream(() => {
97
- this.streamActive = true;
98
- });
97
+ responseStream;
99
98
  responseBody;
100
99
  streamActive = false;
101
- stream = this.responseStream;
100
+ get stream() {
101
+ return this.getOrCreateResponseStream();
102
+ }
102
103
  redirect(status, location) {
103
104
  this.setStatus(status);
104
105
  this.setHeader('Location', location);
@@ -116,6 +117,17 @@ class MutableWebFrameworkResponse {
116
117
  this.responseBody = serialized.payload;
117
118
  this.committed = true;
118
119
  }
120
+ async sendSimpleJson(body) {
121
+ if (this.finalizedResponse) {
122
+ this.committed = true;
123
+ return;
124
+ }
125
+ if (!hasHeader(this.headers, 'content-type')) {
126
+ this.setHeader('Content-Type', 'application/json; charset=utf-8');
127
+ }
128
+ this.responseBody = JSON.stringify(body);
129
+ this.committed = true;
130
+ }
119
131
  setHeader(name, value) {
120
132
  const existingHeaderName = findHeaderName(this.headers, name) ?? name;
121
133
  if (name.toLowerCase() === 'set-cookie') {
@@ -134,13 +146,19 @@ class MutableWebFrameworkResponse {
134
146
  headers: toResponseHeaders(this.headers),
135
147
  status: this.statusCode ?? 200
136
148
  };
137
- const responseBody = this.responseBody instanceof Uint8Array ? this.responseBody.slice().buffer : this.responseBody;
138
- this.finalizedResponse = this.streamActive ? new Response(this.responseStream.readable, init) : new Response(responseBody ?? '', init);
149
+ const nativeResponseBody = isResponseBodyForbidden(init.status) ? undefined : this.responseBody === undefined ? '' : this.responseBody;
150
+ this.finalizedResponse = this.streamActive ? new Response(this.getOrCreateResponseStream().readable, init) : new Response(nativeResponseBody, init);
139
151
  this.raw = this.finalizedResponse;
140
152
  this.committed = true;
141
153
  }
142
154
  return this.finalizedResponse;
143
155
  }
156
+ getOrCreateResponseStream() {
157
+ this.responseStream ??= new WebResponseStream(() => {
158
+ this.streamActive = true;
159
+ });
160
+ return this.responseStream;
161
+ }
144
162
  }
145
163
 
146
164
  /**
@@ -152,7 +170,10 @@ class MutableWebFrameworkResponse {
152
170
  export function createWebRequestResponseFactory(options = {}) {
153
171
  return {
154
172
  async createRequest(request, signal) {
155
- return await createWebFrameworkRequest(request, signal, options.multipart, options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE, options.rawBody ?? false);
173
+ return createDeferredWebFrameworkRequest(request, signal, options.multipart, options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE, options.rawBody ?? false, options.preferNativeJsonBodyReader ?? false, options.consumeOriginalBody ?? false);
174
+ },
175
+ materializeRequest(request) {
176
+ return materializeWebFrameworkRequestBody(request);
156
177
  },
157
178
  createRequestSignal(signal) {
158
179
  return signal ?? new AbortController().signal;
@@ -180,13 +201,14 @@ export function createWebRequestResponseFactory(options = {}) {
180
201
  export async function dispatchWebRequest({
181
202
  dispatcher,
182
203
  dispatcherNotReadyMessage = 'Web adapter received a request before dispatcher binding completed.',
204
+ factory,
183
205
  request,
184
206
  ...options
185
207
  }) {
186
208
  const frameworkResponse = await dispatchWithRequestResponseFactory({
187
209
  dispatcher,
188
210
  dispatcherNotReadyMessage,
189
- factory: createWebRequestResponseFactory(options),
211
+ factory: factory ?? createWebRequestResponseFactory(options),
190
212
  rawRequest: request,
191
213
  rawResponse: request.signal
192
214
  });
@@ -204,61 +226,176 @@ export async function dispatchWebRequest({
204
226
  * @returns The normalized framework request used by the dispatcher.
205
227
  */
206
228
  export async function createWebFrameworkRequest(request, signal, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
229
+ const frameworkRequest = createDeferredWebFrameworkRequest(request, signal, multipartOptions, maxBodySize, preserveRawBody);
230
+ await materializeWebFrameworkRequestBody(frameworkRequest);
231
+ return frameworkRequest;
232
+ }
233
+
234
+ /**
235
+ * Creates the cheap Web framework request shell before consuming the body stream.
236
+ *
237
+ * @param request - Native Web request to normalize.
238
+ * @param signal - Abort signal propagated to the framework request.
239
+ * @param multipartOptions - Multipart parser options applied when materializing multipart requests.
240
+ * @param maxBodySize - Maximum allowed non-multipart body size in bytes.
241
+ * @param preserveRawBody - Whether materialization should retain raw request body bytes.
242
+ * @returns The framework request shell with metadata snapshotted and body materialization deferred.
243
+ */
244
+ function createDeferredWebFrameworkRequest(request, signal, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false, preferNativeJsonBodyReader = false, consumeOriginalBody = false) {
207
245
  const url = new URL(request.url);
208
- const headers = cloneWebHeaders(request.headers);
209
- const contentType = request.headers.get('content-type') ?? undefined;
246
+ const requestHeaders = new Headers(request.headers);
247
+ const method = request.method;
248
+ const headers = createMemoizedValue(() => cloneWebHeaders(requestHeaders));
249
+ const cookies = createMemoizedValue(() => parseCookieHeader(requestHeaders.get('cookie') ?? undefined));
250
+ const query = createMemoizedValue(() => parseQueryString(url.search));
251
+ const contentType = requestHeaders.get('content-type') ?? undefined;
210
252
  const isMultipart = typeof contentType === 'string' && contentType.includes('multipart/form-data');
211
- let body;
212
- let files;
213
- let rawBody;
214
- if (isMultipart) {
215
- const result = await parseMultipart(request.clone(), {
216
- ...multipartOptions,
217
- maxTotalSize: multipartOptions?.maxTotalSize ?? maxBodySize
218
- });
219
- body = result.fields;
220
- files = result.files;
221
- } else {
222
- const bodyResult = await readWebRequestBody(request.clone(), contentType, maxBodySize, preserveRawBody);
223
- body = bodyResult.body;
224
- rawBody = bodyResult.rawBody;
225
- }
253
+ const hasRequestBody = request.body !== null;
254
+ const materializeBody = hasRequestBody ? createMemoizedAsyncValue(async () => {
255
+ if (isMultipart) {
256
+ const materializedRequest = request.clone();
257
+ const result = await parseMultipart(createRequestWithSnapshotMetadata(materializedRequest, request.url, method, requestHeaders), {
258
+ ...multipartOptions,
259
+ maxTotalSize: multipartOptions?.maxTotalSize ?? maxBodySize
260
+ });
261
+ frameworkRequest.body = result.fields;
262
+ frameworkRequest.files = result.files;
263
+ return;
264
+ }
265
+ validateWebRequestContentLength(request, maxBodySize);
266
+ if (!request.body) {
267
+ frameworkRequest.body = undefined;
268
+ return;
269
+ }
270
+ const requestToRead = consumeOriginalBody ? request : request.clone();
271
+ const bodyResult = await readWebRequestBody(requestToRead, contentType, maxBodySize, preserveRawBody, preferNativeJsonBodyReader);
272
+ frameworkRequest.body = bodyResult.body;
273
+ if (bodyResult.rawBody) {
274
+ frameworkRequest.rawBody = bodyResult.rawBody;
275
+ }
276
+ }) : undefined;
226
277
  const frameworkRequest = {
227
- body,
228
- cookies: parseCookieHeader(request.headers.get('cookie') ?? undefined),
229
- headers,
230
- method: request.method,
278
+ get cookies() {
279
+ return cookies();
280
+ },
281
+ get headers() {
282
+ return headers();
283
+ },
284
+ method,
231
285
  params: {},
232
286
  path: url.pathname,
233
- query: parseQueryParams(url.searchParams),
287
+ get query() {
288
+ return query();
289
+ },
234
290
  raw: request,
291
+ requestId: requestHeaders.get('x-request-id') ?? undefined,
235
292
  signal,
236
- url: url.pathname + url.search
293
+ url: url.pathname + url.search,
294
+ materializeBody
237
295
  };
238
- if (files) {
239
- frameworkRequest.files = files;
296
+ if (!hasRequestBody) {
297
+ frameworkRequest.body = undefined;
240
298
  }
241
- if (rawBody) {
242
- frameworkRequest.rawBody = rawBody;
299
+ const nativeRouteHandoff = consumeRawRequestNativeRouteHandoff(request);
300
+ return nativeRouteHandoff ? attachFrameworkRequestNativeRouteHandoff(frameworkRequest, nativeRouteHandoff) : frameworkRequest;
301
+ }
302
+ function createRequestWithSnapshotMetadata(request, url, method, headers) {
303
+ const init = {
304
+ headers: new Headers(headers),
305
+ method
306
+ };
307
+ if (request.body) {
308
+ init.body = request.body;
309
+ init.duplex = 'half';
243
310
  }
244
- return frameworkRequest;
311
+ return new Request(url, init);
245
312
  }
246
- function parseQueryParams(searchParams) {
313
+ function validateWebRequestContentLength(request, maxBodySize) {
314
+ const contentLength = request.headers.get('content-length');
315
+ if (contentLength === null) {
316
+ return;
317
+ }
318
+ const parsedContentLength = Number(contentLength);
319
+ if (Number.isFinite(parsedContentLength) && parsedContentLength > maxBodySize) {
320
+ throw new PayloadTooLargeException(REQUEST_BODY_LIMIT_MESSAGE);
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Materializes a deferred Web framework request body exactly once.
326
+ *
327
+ * @param request - Framework request returned by {@link createDeferredWebFrameworkRequest}.
328
+ * @returns A promise that settles after body, rawBody, and files fields are populated when applicable.
329
+ */
330
+ async function materializeWebFrameworkRequestBody(request) {
331
+ await request.materializeBody?.();
332
+ delete request.materializeBody;
333
+ }
334
+ function createMemoizedValue(factory) {
335
+ let initialized = false;
336
+ let value;
337
+ return () => {
338
+ if (!initialized) {
339
+ value = factory();
340
+ initialized = true;
341
+ }
342
+ return value;
343
+ };
344
+ }
345
+ function createMemoizedAsyncValue(factory) {
346
+ let promise;
347
+ return () => {
348
+ promise ??= factory();
349
+ return promise;
350
+ };
351
+ }
352
+ function parseQueryString(search) {
247
353
  const query = {};
248
- for (const [key, value] of searchParams.entries()) {
249
- const current = query[key];
250
- if (current === undefined) {
251
- query[key] = value;
252
- continue;
354
+ if (search.length <= 1) {
355
+ return query;
356
+ }
357
+ let index = search.charCodeAt(0) === 63 ? 1 : 0;
358
+ while (index <= search.length) {
359
+ let nextDelimiter = search.indexOf('&', index);
360
+ if (nextDelimiter === -1) {
361
+ nextDelimiter = search.length;
253
362
  }
254
- if (Array.isArray(current)) {
255
- current.push(value);
256
- continue;
363
+ const entry = search.slice(index, nextDelimiter);
364
+ if (entry.length > 0) {
365
+ const separatorIndex = entry.indexOf('=');
366
+ const rawKey = separatorIndex === -1 ? entry : entry.slice(0, separatorIndex);
367
+ const rawValue = separatorIndex === -1 ? '' : entry.slice(separatorIndex + 1);
368
+ const key = decodeQueryComponent(rawKey, 'key');
369
+ const value = decodeQueryComponent(rawValue, 'value');
370
+ const current = query[key];
371
+ if (current === undefined) {
372
+ query[key] = value;
373
+ } else if (Array.isArray(current)) {
374
+ current.push(value);
375
+ } else {
376
+ query[key] = [current, value];
377
+ }
257
378
  }
258
- query[key] = [current, value];
379
+ index = nextDelimiter + 1;
259
380
  }
260
381
  return query;
261
382
  }
383
+ function decodeQueryComponent(value, kind) {
384
+ const normalizedValue = value.includes('+') ? value.replaceAll('+', ' ') : value;
385
+ try {
386
+ return decodeURIComponent(normalizedValue);
387
+ } catch {
388
+ return decodeQueryComponentLikeUrlSearchParams(value, kind);
389
+ }
390
+ }
391
+ function decodeQueryComponentLikeUrlSearchParams(value, kind) {
392
+ if (kind === 'key') {
393
+ const params = new URLSearchParams(`${value}=`);
394
+ return params.keys().next().value ?? '';
395
+ }
396
+ const params = new URLSearchParams(`x=${value}`);
397
+ return params.get('x') ?? '';
398
+ }
262
399
  function cloneWebHeaders(headers) {
263
400
  const clonedHeaders = {};
264
401
  for (const [name, value] of headers.entries()) {
@@ -285,20 +422,23 @@ function parseCookieHeader(cookieHeader) {
285
422
  return [pair.slice(0, index).trim(), decodeCookieValue(pair.slice(index + 1).trim())];
286
423
  }));
287
424
  }
288
- async function readWebRequestBody(request, contentType, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
289
- const contentLength = request.headers.get('content-length');
290
- if (contentLength !== null) {
291
- const parsedContentLength = Number(contentLength);
292
- if (Number.isFinite(parsedContentLength) && parsedContentLength > maxBodySize) {
293
- throw new PayloadTooLargeException(REQUEST_BODY_LIMIT_MESSAGE);
294
- }
295
- }
425
+ async function readWebRequestBody(request, contentType, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false, preferNativeJsonBodyReader = false) {
426
+ validateWebRequestContentLength(request, maxBodySize);
296
427
  if (!request.body) {
297
428
  return {
298
429
  body: undefined
299
430
  };
300
431
  }
301
- const rawBody = await readByteLimitedStream(request.body, maxBodySize);
432
+ if (!preserveRawBody && isJsonContentType(contentType) && (preferNativeJsonBodyReader || isContentLengthWithinLimit(request, maxBodySize))) {
433
+ const rawBody = new Uint8Array(await request.arrayBuffer());
434
+ if (rawBody.byteLength > maxBodySize) {
435
+ throw new PayloadTooLargeException(REQUEST_BODY_LIMIT_MESSAGE);
436
+ }
437
+ return parseWebRequestRawBody(rawBody, contentType, preserveRawBody);
438
+ }
439
+ return parseWebRequestRawBody(await readByteLimitedStream(request.body, maxBodySize), contentType, preserveRawBody);
440
+ }
441
+ function parseWebRequestRawBody(rawBody, contentType, preserveRawBody) {
302
442
  if (rawBody.byteLength === 0) {
303
443
  return {
304
444
  body: undefined
@@ -311,7 +451,7 @@ async function readWebRequestBody(request, contentType, maxBodySize = DEFAULT_MA
311
451
  rawBody: preserveRawBody ? rawBody : undefined
312
452
  };
313
453
  }
314
- if (typeof contentType === 'string' && contentType.includes('application/json')) {
454
+ if (isJsonContentType(contentType)) {
315
455
  try {
316
456
  return {
317
457
  body: JSON.parse(bodyText),
@@ -326,6 +466,14 @@ async function readWebRequestBody(request, contentType, maxBodySize = DEFAULT_MA
326
466
  rawBody: preserveRawBody ? rawBody : undefined
327
467
  };
328
468
  }
469
+ function isContentLengthWithinLimit(request, maxBodySize) {
470
+ const contentLength = request.headers.get('content-length');
471
+ if (contentLength === null) {
472
+ return false;
473
+ }
474
+ const parsedContentLength = Number(contentLength);
475
+ return Number.isFinite(parsedContentLength) && parsedContentLength > 0 && parsedContentLength <= maxBodySize;
476
+ }
329
477
  async function readByteLimitedStream(stream, maxBodySize) {
330
478
  const reader = stream.getReader();
331
479
  const chunks = [];
@@ -376,6 +524,9 @@ function findHeaderName(headers, name) {
376
524
  function hasHeader(headers, name) {
377
525
  return findHeaderName(headers, name) !== undefined;
378
526
  }
527
+ function isResponseBodyForbidden(status) {
528
+ return status === 204 || status === 205 || status === 304;
529
+ }
379
530
  function toResponseHeaders(headers) {
380
531
  const responseHeaders = new Headers();
381
532
  for (const [name, value] of Object.entries(headers)) {
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "module-graph",
10
10
  "orchestration"
11
11
  ],
12
- "version": "1.0.0-beta.1",
12
+ "version": "1.0.0-beta.10",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -67,14 +67,14 @@
67
67
  "dist"
68
68
  ],
69
69
  "dependencies": {
70
- "@fluojs/core": "^1.0.0-beta.1",
71
- "@fluojs/config": "^1.0.0-beta.1",
72
- "@fluojs/di": "^1.0.0-beta.1",
73
- "@fluojs/http": "^1.0.0-beta.1"
70
+ "@fluojs/di": "^1.0.0-beta.6",
71
+ "@fluojs/config": "^1.0.0-beta.6",
72
+ "@fluojs/http": "^1.0.0-beta.9",
73
+ "@fluojs/core": "^1.0.0-beta.3"
74
74
  },
75
75
  "devDependencies": {
76
76
  "vitest": "^3.2.4",
77
- "@fluojs/serialization": "^1.0.0-beta.1"
77
+ "@fluojs/serialization": "^1.0.0-beta.4"
78
78
  },
79
79
  "scripts": {
80
80
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",