@daloyjs/core 0.35.0 → 0.35.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 (188) hide show
  1. package/README.md +145 -29
  2. package/dist/adapters/bun.d.ts +0 -1
  3. package/dist/adapters/bun.js +0 -1
  4. package/dist/adapters/cloudflare.d.ts +0 -1
  5. package/dist/adapters/cloudflare.js +0 -1
  6. package/dist/adapters/deno.d.ts +0 -1
  7. package/dist/adapters/deno.js +0 -1
  8. package/dist/adapters/fastly.d.ts +0 -1
  9. package/dist/adapters/fastly.js +0 -1
  10. package/dist/adapters/lambda.d.ts +0 -1
  11. package/dist/adapters/lambda.js +0 -1
  12. package/dist/adapters/node.d.ts +0 -1
  13. package/dist/adapters/node.js +175 -35
  14. package/dist/adapters/vercel.d.ts +0 -1
  15. package/dist/adapters/vercel.js +0 -1
  16. package/dist/app.d.ts +18 -1
  17. package/dist/app.js +396 -118
  18. package/dist/banner.d.ts +0 -1
  19. package/dist/banner.js +0 -1
  20. package/dist/cli.d.ts +0 -1
  21. package/dist/cli.js +0 -1
  22. package/dist/client.d.ts +0 -1
  23. package/dist/client.js +0 -1
  24. package/dist/combine.d.ts +0 -1
  25. package/dist/combine.js +0 -1
  26. package/dist/compression.d.ts +0 -1
  27. package/dist/compression.js +0 -1
  28. package/dist/config.d.ts +0 -1
  29. package/dist/config.js +0 -1
  30. package/dist/conn-info.d.ts +0 -1
  31. package/dist/conn-info.js +0 -1
  32. package/dist/contract.d.ts +0 -1
  33. package/dist/contract.js +0 -1
  34. package/dist/cookie.d.ts +0 -1
  35. package/dist/cookie.js +0 -1
  36. package/dist/dependency.d.ts +0 -1
  37. package/dist/dependency.js +0 -1
  38. package/dist/discriminator.d.ts +0 -1
  39. package/dist/discriminator.js +0 -1
  40. package/dist/docs.d.ts +0 -1
  41. package/dist/docs.js +0 -1
  42. package/dist/errors.d.ts +0 -1
  43. package/dist/errors.js +0 -1
  44. package/dist/etag.d.ts +0 -1
  45. package/dist/etag.js +0 -1
  46. package/dist/fetch-guard.d.ts +0 -1
  47. package/dist/fetch-guard.js +0 -1
  48. package/dist/hashing.d.ts +0 -1
  49. package/dist/hashing.js +0 -1
  50. package/dist/index.d.ts +0 -1
  51. package/dist/index.js +0 -1
  52. package/dist/ip-restriction.d.ts +0 -1
  53. package/dist/ip-restriction.js +0 -1
  54. package/dist/jwk.d.ts +0 -1
  55. package/dist/jwk.js +0 -1
  56. package/dist/jwt.d.ts +0 -1
  57. package/dist/jwt.js +0 -1
  58. package/dist/load-shedding.d.ts +0 -1
  59. package/dist/load-shedding.js +0 -1
  60. package/dist/logger.d.ts +0 -1
  61. package/dist/logger.js +0 -1
  62. package/dist/middleware.d.ts +0 -1
  63. package/dist/middleware.js +16 -13
  64. package/dist/multipart.d.ts +0 -1
  65. package/dist/multipart.js +0 -1
  66. package/dist/openapi.d.ts +0 -1
  67. package/dist/openapi.js +0 -1
  68. package/dist/rate-limit-redis.d.ts +0 -1
  69. package/dist/rate-limit-redis.js +0 -1
  70. package/dist/router.d.ts +0 -1
  71. package/dist/router.js +0 -1
  72. package/dist/safe-redirect.d.ts +2 -3
  73. package/dist/safe-redirect.js +2 -3
  74. package/dist/sbom.cdx.json +9 -9
  75. package/dist/sbom.spdx.json +5 -5
  76. package/dist/schema.d.ts +0 -1
  77. package/dist/schema.js +0 -1
  78. package/dist/security-schemes.d.ts +0 -1
  79. package/dist/security-schemes.js +0 -1
  80. package/dist/security.d.ts +4 -5
  81. package/dist/security.js +4 -5
  82. package/dist/session.d.ts +0 -1
  83. package/dist/session.js +0 -1
  84. package/dist/streaming.d.ts +0 -1
  85. package/dist/streaming.js +0 -1
  86. package/dist/subdomains.d.ts +0 -1
  87. package/dist/subdomains.js +0 -1
  88. package/dist/time-claims.d.ts +0 -1
  89. package/dist/time-claims.js +0 -1
  90. package/dist/tracing.d.ts +0 -1
  91. package/dist/tracing.js +0 -1
  92. package/dist/types.d.ts +0 -1
  93. package/dist/types.js +0 -1
  94. package/dist/websocket.d.ts +0 -1
  95. package/dist/websocket.js +0 -1
  96. package/package.json +6 -2
  97. package/dist/adapters/bun.d.ts.map +0 -1
  98. package/dist/adapters/bun.js.map +0 -1
  99. package/dist/adapters/cloudflare.d.ts.map +0 -1
  100. package/dist/adapters/cloudflare.js.map +0 -1
  101. package/dist/adapters/deno.d.ts.map +0 -1
  102. package/dist/adapters/deno.js.map +0 -1
  103. package/dist/adapters/fastly.d.ts.map +0 -1
  104. package/dist/adapters/fastly.js.map +0 -1
  105. package/dist/adapters/lambda.d.ts.map +0 -1
  106. package/dist/adapters/lambda.js.map +0 -1
  107. package/dist/adapters/node.d.ts.map +0 -1
  108. package/dist/adapters/node.js.map +0 -1
  109. package/dist/adapters/vercel.d.ts.map +0 -1
  110. package/dist/adapters/vercel.js.map +0 -1
  111. package/dist/app.d.ts.map +0 -1
  112. package/dist/app.js.map +0 -1
  113. package/dist/banner.d.ts.map +0 -1
  114. package/dist/banner.js.map +0 -1
  115. package/dist/cli.d.ts.map +0 -1
  116. package/dist/cli.js.map +0 -1
  117. package/dist/client.d.ts.map +0 -1
  118. package/dist/client.js.map +0 -1
  119. package/dist/combine.d.ts.map +0 -1
  120. package/dist/combine.js.map +0 -1
  121. package/dist/compression.d.ts.map +0 -1
  122. package/dist/compression.js.map +0 -1
  123. package/dist/config.d.ts.map +0 -1
  124. package/dist/config.js.map +0 -1
  125. package/dist/conn-info.d.ts.map +0 -1
  126. package/dist/conn-info.js.map +0 -1
  127. package/dist/contract.d.ts.map +0 -1
  128. package/dist/contract.js.map +0 -1
  129. package/dist/cookie.d.ts.map +0 -1
  130. package/dist/cookie.js.map +0 -1
  131. package/dist/dependency.d.ts.map +0 -1
  132. package/dist/dependency.js.map +0 -1
  133. package/dist/discriminator.d.ts.map +0 -1
  134. package/dist/discriminator.js.map +0 -1
  135. package/dist/docs.d.ts.map +0 -1
  136. package/dist/docs.js.map +0 -1
  137. package/dist/errors.d.ts.map +0 -1
  138. package/dist/errors.js.map +0 -1
  139. package/dist/etag.d.ts.map +0 -1
  140. package/dist/etag.js.map +0 -1
  141. package/dist/fetch-guard.d.ts.map +0 -1
  142. package/dist/fetch-guard.js.map +0 -1
  143. package/dist/hashing.d.ts.map +0 -1
  144. package/dist/hashing.js.map +0 -1
  145. package/dist/index.d.ts.map +0 -1
  146. package/dist/index.js.map +0 -1
  147. package/dist/ip-restriction.d.ts.map +0 -1
  148. package/dist/ip-restriction.js.map +0 -1
  149. package/dist/jwk.d.ts.map +0 -1
  150. package/dist/jwk.js.map +0 -1
  151. package/dist/jwt.d.ts.map +0 -1
  152. package/dist/jwt.js.map +0 -1
  153. package/dist/load-shedding.d.ts.map +0 -1
  154. package/dist/load-shedding.js.map +0 -1
  155. package/dist/logger.d.ts.map +0 -1
  156. package/dist/logger.js.map +0 -1
  157. package/dist/middleware.d.ts.map +0 -1
  158. package/dist/middleware.js.map +0 -1
  159. package/dist/multipart.d.ts.map +0 -1
  160. package/dist/multipart.js.map +0 -1
  161. package/dist/openapi.d.ts.map +0 -1
  162. package/dist/openapi.js.map +0 -1
  163. package/dist/rate-limit-redis.d.ts.map +0 -1
  164. package/dist/rate-limit-redis.js.map +0 -1
  165. package/dist/router.d.ts.map +0 -1
  166. package/dist/router.js.map +0 -1
  167. package/dist/safe-redirect.d.ts.map +0 -1
  168. package/dist/safe-redirect.js.map +0 -1
  169. package/dist/schema.d.ts.map +0 -1
  170. package/dist/schema.js.map +0 -1
  171. package/dist/security-schemes.d.ts.map +0 -1
  172. package/dist/security-schemes.js.map +0 -1
  173. package/dist/security.d.ts.map +0 -1
  174. package/dist/security.js.map +0 -1
  175. package/dist/session.d.ts.map +0 -1
  176. package/dist/session.js.map +0 -1
  177. package/dist/streaming.d.ts.map +0 -1
  178. package/dist/streaming.js.map +0 -1
  179. package/dist/subdomains.d.ts.map +0 -1
  180. package/dist/subdomains.js.map +0 -1
  181. package/dist/time-claims.d.ts.map +0 -1
  182. package/dist/time-claims.js.map +0 -1
  183. package/dist/tracing.d.ts.map +0 -1
  184. package/dist/tracing.js.map +0 -1
  185. package/dist/types.d.ts.map +0 -1
  186. package/dist/types.js.map +0 -1
  187. package/dist/websocket.d.ts.map +0 -1
  188. package/dist/websocket.js.map +0 -1
package/dist/app.js CHANGED
@@ -79,6 +79,16 @@ const DEFAULTS = {
79
79
  requestTimeoutMs: 30_000,
80
80
  validateResponses: true,
81
81
  };
82
+ const TEXT_ENCODER = new TextEncoder();
83
+ /**
84
+ * Internal Symbol used to stash the raw Uint8Array (or null) body on a
85
+ * `Response` produced by {@link serializeResult}. Adapters that want to
86
+ * avoid the cost of `await response.arrayBuffer()` for buffer-backed
87
+ * responses can read this property and write the bytes directly. The
88
+ * Symbol is intentionally module-private (not exported) to keep this an
89
+ * implementation detail — userland code should never depend on it.
90
+ */
91
+ export const DALOY_RAW_BODY = Symbol.for("daloyjs.response.rawBody");
82
92
  /**
83
93
  * Contract-first HTTP application.
84
94
  *
@@ -181,6 +191,25 @@ export class App {
181
191
  * dashboards see the misconfiguration without flooding on every retry.
182
192
  */
183
193
  trustProxyWarned = false;
194
+ /**
195
+ * Cached merge of `options.hooks` only. Used on the cold 404/405 path
196
+ * and as the baseline for cross-origin guard decisions when no route
197
+ * matches.
198
+ */
199
+ _globalHooksCache;
200
+ _globalCorsAllowsCache;
201
+ get globalHooks() {
202
+ if (this._globalHooksCache === undefined) {
203
+ this._globalHooksCache = mergeHooks([this.options.hooks ?? {}]);
204
+ }
205
+ return this._globalHooksCache;
206
+ }
207
+ get globalCorsAllows() {
208
+ if (this._globalCorsAllowsCache === undefined) {
209
+ this._globalCorsAllowsCache = corsOriginAllowsFromHooks([this.options.hooks ?? {}]);
210
+ }
211
+ return this._globalCorsAllowsCache;
212
+ }
184
213
  constructor(options = {}) {
185
214
  this.options = {
186
215
  validateResponses: options.validateResponses ?? DEFAULTS.validateResponses,
@@ -377,7 +406,7 @@ export class App {
377
406
  * routes whose hook chain includes a `cors()` policy allowing the origin
378
407
  * are unaffected.
379
408
  */
380
- assertCrossOriginAllowed(request, url, method, corsOriginAllows) {
409
+ assertCrossOriginAllowed(request, requestUrl, method, corsOriginAllows) {
381
410
  if (this.options.secureDefaults === false)
382
411
  return;
383
412
  if (this.options.corsCrossOriginGuard === false)
@@ -399,7 +428,8 @@ export class App {
399
428
  // Malformed Origin header — refuse loudly.
400
429
  throw new ForbiddenError("Cross-origin state-changing request rejected: malformed Origin header.");
401
430
  }
402
- if (originUrl.origin === url.origin)
431
+ const reqOrigin = typeof requestUrl === "string" ? new URL(requestUrl).origin : requestUrl.origin;
432
+ if (originUrl.origin === reqOrigin)
403
433
  return;
404
434
  if (corsOriginAllows.some((allows) => allows(origin)))
405
435
  return;
@@ -728,11 +758,18 @@ export class App {
728
758
  const sources = [...this.groupHooks, def.hooks ?? {}];
729
759
  const hooks = mergeHooks(sources);
730
760
  const corsOriginAllows = corsOriginAllowsFromHooks(sources);
761
+ const globalHookLayer = this.options.hooks ?? {};
762
+ const mergedHooks = mergeHooks([globalHookLayer, ...sources]);
763
+ const hasFinalizeHook = mergedHooks.onSend !== undefined || mergedHooks.onResponse !== undefined;
764
+ const fullCorsOriginAllows = [
765
+ ...corsOriginAllowsFromHooks([globalHookLayer]),
766
+ ...corsOriginAllows,
767
+ ];
731
768
  const securityMarkers = securityMarkersFromHooks([
732
- this.options.hooks ?? {},
769
+ globalHookLayer,
733
770
  ...sources,
734
771
  ]);
735
- this.router.add(def.method, fullPath, { def: merged, hooks, corsOriginAllows }, def.operationId);
772
+ this.router.add(def.method, fullPath, { def: merged, hooks, mergedHooks, hasFinalizeHook, corsOriginAllows, fullCorsOriginAllows }, def.operationId);
736
773
  this.routes.push(merged);
737
774
  this.routeSecurityMarkers.push({
738
775
  method: merged.method,
@@ -1426,7 +1463,7 @@ export class App {
1426
1463
  });
1427
1464
  const stripFingerprint = this.options.stripServerHeaders !== false;
1428
1465
  let ctx;
1429
- const globalHooks = mergeHooks([this.options.hooks ?? {}]);
1466
+ const globalHooks = this.globalHooks;
1430
1467
  let activeErrorHook = globalHooks.onError;
1431
1468
  let activeResponseHook = globalHooks.onResponse;
1432
1469
  let activeSendHook = globalHooks.onSend;
@@ -1435,26 +1472,36 @@ export class App {
1435
1472
  assertNoReservedInternalHeaders(request.headers);
1436
1473
  this.assertTrustProxyConfigured(request);
1437
1474
  this.assertBootGuards();
1438
- await globalHooks.onRequest?.(request);
1439
- const url = new URL(request.url);
1475
+ if (globalHooks.onRequest !== undefined) {
1476
+ const onRequestResult = globalHooks.onRequest(request);
1477
+ if (isPromiseLike(onRequestResult))
1478
+ await onRequestResult;
1479
+ }
1440
1480
  const method = request.method;
1481
+ const requestUrl = request.url;
1482
+ const pathname = getPathnameFast(requestUrl);
1483
+ let url;
1484
+ const getUrl = () => (url ??= new URL(requestUrl));
1441
1485
  const headFallback = method === "HEAD";
1442
- const match = this.router.find(method, url.pathname) ??
1443
- (headFallback ? this.router.find("GET", url.pathname) : undefined);
1486
+ const match = this.router.find(method, pathname) ??
1487
+ (headFallback ? this.router.find("GET", pathname) : undefined);
1444
1488
  // Hide internal routes from the public adapter surface. The router
1445
1489
  // still finds them so app.inject() can dispatch normally, but
1446
1490
  // app.fetch() responds 404 to avoid leaking existence.
1447
1491
  const internalHidden = match?.handler.def.internal === true && opts.allowInternal !== true;
1448
- this.assertCrossOriginAllowed(request, url, method, [
1449
- ...corsOriginAllowsFromHooks([this.options.hooks ?? {}]),
1450
- ...(match && !internalHidden ? match.handler.corsOriginAllows : this.corsOriginAllows),
1451
- ]);
1492
+ if (match && !internalHidden) {
1493
+ this.assertCrossOriginAllowed(request, requestUrl, method, match.handler.fullCorsOriginAllows);
1494
+ }
1495
+ else {
1496
+ this.assertCrossOriginAllowed(request, requestUrl, method, [...this.globalCorsAllows, ...this.corsOriginAllows]);
1497
+ }
1452
1498
  if (!match || internalHidden) {
1499
+ const url404 = getUrl();
1453
1500
  if (internalHidden) {
1454
1501
  // Don't leak existence via 405/Allow header. Always 404.
1455
- throw new NotFoundError(`No route for ${request.method} ${url.pathname}`);
1502
+ throw new NotFoundError(`No route for ${request.method} ${url404.pathname}`);
1456
1503
  }
1457
- const rawAllowed = this.router.allowedMethods(url.pathname);
1504
+ const rawAllowed = this.router.allowedMethods(url404.pathname);
1458
1505
  // Filter out methods whose route definitions are marked
1459
1506
  // `internal: true` unless the caller explicitly opted in via
1460
1507
  // app.inject(). This prevents 405/Allow from leaking the
@@ -1462,13 +1509,13 @@ export class App {
1462
1509
  const allowed = opts.allowInternal
1463
1510
  ? rawAllowed
1464
1511
  : rawAllowed.filter((m) => {
1465
- const candidate = this.router.find(m, url.pathname);
1512
+ const candidate = this.router.find(m, url404.pathname);
1466
1513
  return candidate?.handler.def.internal !== true;
1467
1514
  });
1468
1515
  ctx = {
1469
1516
  request,
1470
1517
  params: {},
1471
- query: Object.fromEntries(url.searchParams.entries()),
1518
+ query: Object.fromEntries(url404.searchParams.entries()),
1472
1519
  headers: headersToObject(request.headers),
1473
1520
  body: undefined,
1474
1521
  state: { ...this.decorations, requestId, log },
@@ -1492,10 +1539,12 @@ export class App {
1492
1539
  this.options.hooks ?? {},
1493
1540
  ...this.groupHooks,
1494
1541
  ]);
1495
- const intercepted = await preflightHooks.beforeHandle?.(synthCtx);
1542
+ const interceptedResult = preflightHooks.beforeHandle?.(synthCtx);
1543
+ const intercepted = isPromiseLike(interceptedResult) ? await interceptedResult : interceptedResult;
1496
1544
  if (intercepted instanceof Response) {
1497
1545
  copyContextHeaders(synthCtx, intercepted);
1498
- return await finalizeResponse(intercepted, synthCtx, preflightHooks, stripFingerprint);
1546
+ const fin = finalizeResponse(intercepted, synthCtx, preflightHooks, stripFingerprint);
1547
+ return isPromiseLike(fin) ? await fin : fin;
1499
1548
  }
1500
1549
  const res = new Response(null, {
1501
1550
  status: 204,
@@ -1503,35 +1552,61 @@ export class App {
1503
1552
  });
1504
1553
  copyContextHeaders(synthCtx, res);
1505
1554
  res.headers.set("x-request-id", requestId);
1506
- return await finalizeResponse(res, synthCtx, preflightHooks, stripFingerprint);
1555
+ const fin2 = finalizeResponse(res, synthCtx, preflightHooks, stripFingerprint);
1556
+ return isPromiseLike(fin2) ? await fin2 : fin2;
1507
1557
  }
1508
1558
  throw new MethodNotAllowedError(allowed);
1509
1559
  }
1510
- throw new NotFoundError(`No route for ${request.method} ${url.pathname}`);
1560
+ throw new NotFoundError(`No route for ${request.method} ${url404.pathname}`);
1511
1561
  }
1512
- const { def, hooks } = match.handler;
1513
- const allHooks = mergeHooks([globalHooks, hooks]);
1562
+ const { def, hooks, mergedHooks: allHooks, hasFinalizeHook } = match.handler;
1514
1563
  activeErrorHook = allHooks.onError;
1515
1564
  activeResponseHook = allHooks.onResponse;
1516
1565
  activeSendHook = allHooks.onSend;
1517
- await hooks.onRequest?.(request);
1518
- ctx = await buildContext(request, url, match.params, def, this.options);
1566
+ if (hooks.onRequest !== undefined) {
1567
+ const routeOnRequestResult = hooks.onRequest(request);
1568
+ if (isPromiseLike(routeOnRequestResult))
1569
+ await routeOnRequestResult;
1570
+ }
1571
+ ctx = await buildContext(request, getUrl, match.params, def, this.options);
1519
1572
  Object.assign(ctx.state, this.decorations, { requestId, log });
1520
- ctx.set.headers.set("x-request-id", requestId);
1521
- const before = await allHooks.beforeHandle?.(ctx);
1522
- if (before instanceof Response) {
1523
- copyContextHeaders(ctx, before);
1524
- return await finalizeResponse(before, ctx, allHooks, stripFingerprint);
1573
+ if (allHooks.beforeHandle !== undefined) {
1574
+ const beforeResult = allHooks.beforeHandle(ctx);
1575
+ const before = isPromiseLike(beforeResult) ? await beforeResult : beforeResult;
1576
+ if (before instanceof Response) {
1577
+ copyContextHeaders(ctx, before);
1578
+ if (!before.headers.has("x-request-id"))
1579
+ before.headers.set("x-request-id", requestId);
1580
+ if (hasFinalizeHook) {
1581
+ const fin = finalizeResponse(before, ctx, allHooks, stripFingerprint);
1582
+ return isPromiseLike(fin) ? await fin : fin;
1583
+ }
1584
+ return finalizeFast(before, stripFingerprint);
1585
+ }
1525
1586
  }
1526
- let result = this.options.mockMode
1587
+ const runResult = this.options.mockMode
1527
1588
  ? mockResponseFor(def)
1528
- : await runHandler(def, ctx, this.options.requestTimeoutMs);
1529
- const afterReturn = await allHooks.afterHandle?.(ctx, result);
1530
- if (afterReturn !== undefined)
1531
- result = afterReturn;
1532
- let response = await serializeResult(result, def, this.options.validateResponses ?? true);
1589
+ : runHandler(def, ctx, this.options.requestTimeoutMs);
1590
+ let result = isPromiseLike(runResult) ? await runResult : runResult;
1591
+ if (allHooks.afterHandle !== undefined) {
1592
+ const afterResult = allHooks.afterHandle(ctx, result);
1593
+ const afterReturn = isPromiseLike(afterResult) ? await afterResult : afterResult;
1594
+ if (afterReturn !== undefined)
1595
+ result = afterReturn;
1596
+ }
1597
+ const serializeResultRes = serializeResult(result, def, this.options.validateResponses ?? true);
1598
+ let response = isPromiseLike(serializeResultRes) ? await serializeResultRes : serializeResultRes;
1533
1599
  copyContextHeaders(ctx, response);
1534
- const finalized = await finalizeResponse(response, ctx, allHooks, stripFingerprint);
1600
+ if (!response.headers.has("x-request-id"))
1601
+ response.headers.set("x-request-id", requestId);
1602
+ let finalized;
1603
+ if (hasFinalizeHook) {
1604
+ const fin = finalizeResponse(response, ctx, allHooks, stripFingerprint);
1605
+ finalized = isPromiseLike(fin) ? await fin : fin;
1606
+ }
1607
+ else {
1608
+ finalized = finalizeFast(response, stripFingerprint);
1609
+ }
1535
1610
  if (method === "HEAD") {
1536
1611
  return new Response(null, {
1537
1612
  status: finalized.status,
@@ -1903,6 +1978,34 @@ function isStateChangingMethod(method) {
1903
1978
  method === "PATCH" ||
1904
1979
  method === "DELETE");
1905
1980
  }
1981
+ /**
1982
+ * Extract the pathname from a fully-qualified request URL without
1983
+ * constructing a `URL` object. ~10x cheaper than `new URL(...).pathname`
1984
+ * and used on the dispatch hot path; full `URL` parsing is deferred until
1985
+ * something actually reads `searchParams`. Mirrors Hono's `getPath`
1986
+ * approach.
1987
+ *
1988
+ * Handles the standard `http(s)://host[:port]/path[?query][#hash]` shape
1989
+ * produced by the Node adapter and the Fetch standard. Falls back to a
1990
+ * `URL` parse for inputs that don't match (e.g. opaque schemes), so
1991
+ * correctness is preserved even when the fast path doesn't apply.
1992
+ */
1993
+ function getPathnameFast(url) {
1994
+ const schemeEnd = url.indexOf("://");
1995
+ if (schemeEnd === -1)
1996
+ return new URL(url).pathname;
1997
+ const pathStart = url.indexOf("/", schemeEnd + 3);
1998
+ if (pathStart === -1)
1999
+ return "/";
2000
+ let end = url.length;
2001
+ const q = url.indexOf("?", pathStart);
2002
+ if (q !== -1)
2003
+ end = q;
2004
+ const h = url.indexOf("#", pathStart);
2005
+ if (h !== -1 && h < end)
2006
+ end = h;
2007
+ return url.slice(pathStart, end);
2008
+ }
1906
2009
  function mergeHooks(layers) {
1907
2010
  const pick = (key) => layers
1908
2011
  .map((h) => h[key])
@@ -1941,6 +2044,11 @@ function stampRequiredScopes(hooks, scopes) {
1941
2044
  function mergeBeforeHandle(beforeHandle, requiredScopes) {
1942
2045
  if (!beforeHandle && requiredScopes.length === 0)
1943
2046
  return undefined;
2047
+ // No scope aggregation needed → return the inner fn as-is so the dispatch
2048
+ // hot path can use the single-fn fast path and avoid an extra closure +
2049
+ // microtask wrapper around a possibly-sync beforeHandle.
2050
+ if (requiredScopes.length === 0)
2051
+ return beforeHandle;
1944
2052
  return async (ctx) => {
1945
2053
  if (requiredScopes.length > 0) {
1946
2054
  const state = ctx.state;
@@ -1958,6 +2066,8 @@ function mergeBeforeHandle(beforeHandle, requiredScopes) {
1958
2066
  function responsePipeline(fns) {
1959
2067
  if (fns.length === 0)
1960
2068
  return undefined;
2069
+ if (fns.length === 1)
2070
+ return fns[0];
1961
2071
  return async (res, ctx) => {
1962
2072
  let current = res;
1963
2073
  for (const fn of fns) {
@@ -1968,19 +2078,59 @@ function responsePipeline(fns) {
1968
2078
  return current;
1969
2079
  };
1970
2080
  }
1971
- async function finalizeResponse(res, ctx, hooks, stripFingerprint = true) {
1972
- const sent = await hooks.onSend?.(res, ctx);
1973
- const final = sent instanceof Response ? sent : res;
2081
+ function finalizeResponse(res, ctx, hooks, stripFingerprint = true) {
2082
+ let final = res;
2083
+ const finish = (f) => {
2084
+ if (stripFingerprint) {
2085
+ f.headers.delete("server");
2086
+ f.headers.delete("x-powered-by");
2087
+ }
2088
+ if (hooks.onResponse !== undefined) {
2089
+ const onResponseResult = hooks.onResponse(f);
2090
+ if (isPromiseLike(onResponseResult)) {
2091
+ return onResponseResult.then(() => f);
2092
+ }
2093
+ }
2094
+ return f;
2095
+ };
2096
+ if (hooks.onSend !== undefined) {
2097
+ const sentResult = hooks.onSend(res, ctx);
2098
+ if (isPromiseLike(sentResult)) {
2099
+ return sentResult.then((sent) => {
2100
+ if (sent instanceof Response)
2101
+ final = sent;
2102
+ return finish(final);
2103
+ });
2104
+ }
2105
+ else {
2106
+ if (sentResult instanceof Response)
2107
+ final = sentResult;
2108
+ }
2109
+ }
2110
+ return finish(final);
2111
+ }
2112
+ function isPromiseLike(value) {
2113
+ return value !== null && typeof value === "object" && typeof value.then === "function";
2114
+ }
2115
+ /**
2116
+ * Allocation-free finalizer for the common case (no `onSend`/`onResponse`
2117
+ * hooks). Avoids the two `await undefined` microtasks per request that
2118
+ * dominate sub-millisecond dispatch overhead in the no-middleware
2119
+ * benchmark. Behaviour parity with {@link finalizeResponse}: only the
2120
+ * server-fingerprint headers are stripped.
2121
+ */
2122
+ function finalizeFast(res, stripFingerprint) {
1974
2123
  if (stripFingerprint) {
1975
- final.headers.delete("server");
1976
- final.headers.delete("x-powered-by");
2124
+ res.headers.delete("server");
2125
+ res.headers.delete("x-powered-by");
1977
2126
  }
1978
- await hooks.onResponse?.(final);
1979
- return final;
2127
+ return res;
1980
2128
  }
1981
2129
  function chain(fns) {
1982
2130
  if (fns.length === 0)
1983
2131
  return undefined;
2132
+ if (fns.length === 1)
2133
+ return fns[0];
1984
2134
  return (async (...args) => {
1985
2135
  for (const fn of fns)
1986
2136
  await fn(...args);
@@ -1989,6 +2139,8 @@ function chain(fns) {
1989
2139
  function firstResponse(fns) {
1990
2140
  if (fns.length === 0)
1991
2141
  return undefined;
2142
+ if (fns.length === 1)
2143
+ return fns[0];
1992
2144
  return (async (...args) => {
1993
2145
  for (const fn of fns) {
1994
2146
  const r = await fn(...args);
@@ -2001,6 +2153,8 @@ function firstResponse(fns) {
2001
2153
  function pipeline(fns) {
2002
2154
  if (fns.length === 0)
2003
2155
  return undefined;
2156
+ if (fns.length === 1)
2157
+ return fns[0];
2004
2158
  return (async (ctx, value) => {
2005
2159
  let v = value;
2006
2160
  for (const fn of fns) {
@@ -2011,7 +2165,37 @@ function pipeline(fns) {
2011
2165
  return v;
2012
2166
  });
2013
2167
  }
2168
+ /**
2169
+ * Marker stamped on `ctx.set` once the `headers` getter has been
2170
+ * materialised. {@link copyContextHeaders} consults this to skip the
2171
+ * forEach loop entirely on requests where no middleware or handler ever
2172
+ * touched `ctx.set.headers` — the default case for the no-middleware
2173
+ * benchmark.
2174
+ */
2175
+ const SET_HEADERS_TOUCHED = Symbol.for("daloyjs.app.setHeadersTouched");
2176
+ function makeLazySet() {
2177
+ let h;
2178
+ const set = {};
2179
+ Object.defineProperty(set, "headers", {
2180
+ get() {
2181
+ if (h === undefined) {
2182
+ h = new Headers();
2183
+ set[SET_HEADERS_TOUCHED] = true;
2184
+ }
2185
+ return h;
2186
+ },
2187
+ set(v) {
2188
+ h = v;
2189
+ set[SET_HEADERS_TOUCHED] = true;
2190
+ },
2191
+ enumerable: true,
2192
+ configurable: true,
2193
+ });
2194
+ return set;
2195
+ }
2014
2196
  function copyContextHeaders(ctx, res) {
2197
+ if (!ctx.set[SET_HEADERS_TOUCHED])
2198
+ return;
2015
2199
  ctx.set.headers.forEach((v, k) => {
2016
2200
  if (!res.headers.has(k))
2017
2201
  res.headers.set(k, v);
@@ -2020,49 +2204,100 @@ function copyContextHeaders(ctx, res) {
2020
2204
  function hasRequestSchema(request, key) {
2021
2205
  return !!request && !!request[key];
2022
2206
  }
2023
- async function buildContext(request, url, rawParams, def, opts) {
2024
- const set = { headers: new Headers() };
2025
- const headersObj = headersToObject(request.headers);
2026
- const queryObj = queryToObject(url.searchParams);
2207
+ function buildContext(request, getUrl, rawParams, def, opts) {
2208
+ const set = makeLazySet();
2209
+ const hasHeadersSchema = !!def.request?.headers;
2210
+ const hasQuerySchema = !!def.request?.query;
2211
+ let headersObj;
2212
+ let queryObj;
2213
+ const buildHeaders = () => (headersObj ??= headersToObject(request.headers));
2214
+ const buildQuery = () => (queryObj ??= queryToObject(getUrl().searchParams));
2027
2215
  let params = rawParams;
2028
- let query = queryObj;
2029
- let headers = headersObj;
2216
+ let query;
2217
+ let headers;
2030
2218
  let body = undefined;
2031
- if (def.request?.params) {
2032
- const r = await validate(def.request.params, rawParams);
2033
- if (r.issues)
2034
- throw new ValidationError("params", toIssues(r.issues));
2035
- params = r.value;
2036
- }
2037
- if (def.request?.query) {
2038
- const r = await validate(def.request.query, queryObj);
2039
- if (r.issues)
2040
- throw new ValidationError("query", toIssues(r.issues));
2041
- query = r.value;
2042
- }
2043
- if (def.request?.headers) {
2044
- const r = await validate(def.request.headers, headersObj);
2045
- if (r.issues)
2046
- throw new ValidationError("headers", toIssues(r.issues));
2047
- headers = r.value;
2048
- }
2049
- if (def.request?.body) {
2050
- const ct = (request.headers.get("content-type") ?? "").toLowerCase();
2051
- const allowed = def.accepts ?? opts.allowedContentTypes ?? [
2052
- "application/json",
2053
- "application/x-www-form-urlencoded",
2054
- "multipart/form-data",
2055
- ];
2056
- if (!allowed.some((a) => ct.includes(a))) {
2057
- throw new UnsupportedMediaTypeError(ct || "(none)", allowed);
2058
- }
2059
- const raw = await readBody(request, ct, opts.bodyLimitBytes, opts.multipart);
2060
- const r = await validate(def.request.body, raw);
2061
- if (r.issues)
2062
- throw new ValidationError("body", toIssues(r.issues));
2063
- body = r.value;
2064
- }
2065
- return { request, params, query, headers, body, state: {}, set };
2219
+ const hasSchema = def.request?.params || def.request?.query || def.request?.headers || def.request?.body;
2220
+ const finishContext = () => {
2221
+ const ctx = {
2222
+ request,
2223
+ params,
2224
+ body,
2225
+ state: {},
2226
+ set,
2227
+ };
2228
+ if (hasQuerySchema) {
2229
+ ctx.query = query;
2230
+ }
2231
+ else {
2232
+ defineLazyContextProperty(ctx, "query", buildQuery);
2233
+ }
2234
+ if (hasHeadersSchema) {
2235
+ ctx.headers = headers;
2236
+ }
2237
+ else {
2238
+ defineLazyContextProperty(ctx, "headers", buildHeaders);
2239
+ }
2240
+ return ctx;
2241
+ };
2242
+ if (!hasSchema) {
2243
+ return finishContext();
2244
+ }
2245
+ return (async () => {
2246
+ if (def.request?.params) {
2247
+ const r = await validate(def.request.params, rawParams);
2248
+ if (r.issues)
2249
+ throw new ValidationError("params", toIssues(r.issues));
2250
+ params = r.value;
2251
+ }
2252
+ if (hasQuerySchema) {
2253
+ const r = await validate(def.request.query, buildQuery());
2254
+ if (r.issues)
2255
+ throw new ValidationError("query", toIssues(r.issues));
2256
+ query = r.value;
2257
+ }
2258
+ if (hasHeadersSchema) {
2259
+ const r = await validate(def.request.headers, buildHeaders());
2260
+ if (r.issues)
2261
+ throw new ValidationError("headers", toIssues(r.issues));
2262
+ headers = r.value;
2263
+ }
2264
+ if (def.request?.body) {
2265
+ const ct = (request.headers.get("content-type") ?? "").toLowerCase();
2266
+ const allowed = def.accepts ?? opts.allowedContentTypes ?? [
2267
+ "application/json",
2268
+ "application/x-www-form-urlencoded",
2269
+ "multipart/form-data",
2270
+ ];
2271
+ if (!allowed.some((a) => ct.includes(a))) {
2272
+ throw new UnsupportedMediaTypeError(ct || "(none)", allowed);
2273
+ }
2274
+ const raw = await readBody(request, ct, opts.bodyLimitBytes, opts.multipart);
2275
+ const r = await validate(def.request.body, raw);
2276
+ if (r.issues)
2277
+ throw new ValidationError("body", toIssues(r.issues));
2278
+ body = r.value;
2279
+ }
2280
+ return finishContext();
2281
+ })();
2282
+ }
2283
+ function defineLazyContextProperty(ctx, key, build) {
2284
+ let initialized = false;
2285
+ let value;
2286
+ Object.defineProperty(ctx, key, {
2287
+ get() {
2288
+ if (!initialized) {
2289
+ value = build();
2290
+ initialized = true;
2291
+ }
2292
+ return value;
2293
+ },
2294
+ set(next) {
2295
+ value = next;
2296
+ initialized = true;
2297
+ },
2298
+ enumerable: true,
2299
+ configurable: true,
2300
+ });
2066
2301
  }
2067
2302
  function headersToObject(h) {
2068
2303
  const o = {};
@@ -2155,41 +2390,84 @@ async function readBody(req, ct, limit, multipart) {
2155
2390
  const bytes = await readBodyLimited(req, limit);
2156
2391
  return new TextDecoder().decode(bytes);
2157
2392
  }
2158
- async function serializeResult(result, def, validateResponses) {
2393
+ function serializeResult(result, def, validateResponses) {
2159
2394
  const spec = def.responses[result.status];
2160
2395
  if (!spec) {
2161
2396
  throw new InternalError(`Handler returned status ${result.status} which is not declared in responses for ${def.method} ${def.path}`);
2162
2397
  }
2398
+ const finish = () => {
2399
+ const headers = new Headers(result.headers);
2400
+ const explicitCt = headers.get("content-type");
2401
+ const treatAsJson = !explicitCt || explicitCt.includes("application/json");
2402
+ if (!explicitCt)
2403
+ headers.set("content-type", "application/json");
2404
+ let body;
2405
+ let rawBody = null;
2406
+ let isStream = false;
2407
+ if (result.body === undefined || result.body === null) {
2408
+ body = null;
2409
+ }
2410
+ else if (!treatAsJson && typeof result.body === "string") {
2411
+ const bytes = TEXT_ENCODER.encode(result.body);
2412
+ setContentLength(headers, bytes.byteLength);
2413
+ body = bytes;
2414
+ rawBody = bytes;
2415
+ }
2416
+ else if (!treatAsJson && result.body instanceof Uint8Array) {
2417
+ setContentLength(headers, result.body.byteLength);
2418
+ body = result.body;
2419
+ rawBody = result.body;
2420
+ }
2421
+ else if (!treatAsJson && result.body instanceof ArrayBuffer) {
2422
+ setContentLength(headers, result.body.byteLength);
2423
+ body = result.body;
2424
+ rawBody = new Uint8Array(result.body);
2425
+ }
2426
+ else if (!treatAsJson && result.body instanceof ReadableStream) {
2427
+ body = result.body;
2428
+ isStream = true;
2429
+ }
2430
+ else {
2431
+ const bytes = TEXT_ENCODER.encode(JSON.stringify(result.body));
2432
+ setContentLength(headers, bytes.byteLength);
2433
+ body = bytes;
2434
+ rawBody = bytes;
2435
+ }
2436
+ const response = new Response(body, { status: result.status, headers });
2437
+ if (!isStream) {
2438
+ response[DALOY_RAW_BODY] = rawBody;
2439
+ }
2440
+ return response;
2441
+ };
2163
2442
  if (validateResponses && spec.body) {
2164
- const r = await validate(spec.body, result.body);
2165
- if (r.issues) {
2166
- throw new InternalError(`Response body for ${def.method} ${def.path} failed schema validation: ${r.issues
2167
- .map((i) => i.message)
2168
- .join("; ")}`);
2443
+ // Call the Standard Schema validator directly so a sync validator
2444
+ // stays on the sync fast path. The public `validate()` helper always
2445
+ // returns a Promise (stable API surface), but here we own the schema
2446
+ // and can branch on whether validation actually needed to suspend.
2447
+ const r = spec.body["~standard"].validate(result.body);
2448
+ if (isPromiseLike(r)) {
2449
+ return r.then((resolved) => {
2450
+ if (resolved.issues) {
2451
+ throw new InternalError(`Response body for ${def.method} ${def.path} failed schema validation: ${resolved.issues
2452
+ .map((i) => i.message)
2453
+ .join("; ")}`);
2454
+ }
2455
+ return finish();
2456
+ });
2457
+ }
2458
+ else {
2459
+ if (r.issues) {
2460
+ throw new InternalError(`Response body for ${def.method} ${def.path} failed schema validation: ${r.issues
2461
+ .map((i) => i.message)
2462
+ .join("; ")}`);
2463
+ }
2169
2464
  }
2170
2465
  }
2171
- const headers = new Headers(result.headers);
2172
- const explicitCt = headers.get("content-type");
2173
- const treatAsJson = !explicitCt || explicitCt.includes("application/json");
2174
- if (!explicitCt)
2175
- headers.set("content-type", "application/json");
2176
- let body;
2177
- if (result.body === undefined || result.body === null) {
2178
- body = null;
2179
- }
2180
- else if (!treatAsJson &&
2181
- (typeof result.body === "string" ||
2182
- result.body instanceof Uint8Array ||
2183
- result.body instanceof ArrayBuffer)) {
2184
- body = result.body;
2185
- }
2186
- else if (!treatAsJson && result.body instanceof ReadableStream) {
2187
- body = result.body;
2188
- }
2189
- else {
2190
- body = JSON.stringify(result.body);
2191
- }
2192
- return new Response(body, { status: result.status, headers });
2466
+ return finish();
2467
+ }
2468
+ function setContentLength(headers, byteLength) {
2469
+ if (!headers.has("content-length"))
2470
+ headers.set("content-length", String(byteLength));
2193
2471
  }
2194
2472
  function mockResponseFor(def) {
2195
2473
  const statuses = Object.keys(def.responses).map(Number).sort();
@@ -2203,10 +2481,11 @@ function mockResponseFor(def) {
2203
2481
  return { status, body: example };
2204
2482
  }
2205
2483
  function runHandler(def, ctx, requestTimeoutMs) {
2206
- const handlerPromise = Promise.resolve(def.handler(ctx));
2207
- return requestTimeoutMs > 0
2208
- ? withTimeout(handlerPromise, requestTimeoutMs)
2209
- : handlerPromise;
2484
+ const result = def.handler(ctx);
2485
+ if (requestTimeoutMs === 0 || !isPromiseLike(result)) {
2486
+ return result;
2487
+ }
2488
+ return withTimeout(result, requestTimeoutMs);
2210
2489
  }
2211
2490
  function withTimeout(p, ms) {
2212
2491
  return new Promise((resolve, reject) => {
@@ -2347,4 +2626,3 @@ function readHostPackageJsonInfo() {
2347
2626
  export function _resetPackageJsonCacheForTests() {
2348
2627
  PACKAGE_JSON_CACHE.value = undefined;
2349
2628
  }
2350
- //# sourceMappingURL=app.js.map