routup 5.0.0-beta.4 → 5.0.0-beta.6

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.
@@ -1,6 +1,5 @@
1
1
  import { FastURL } from "srvx";
2
2
  import { HTTPError, isHTTPError } from "@ebec/http";
3
- import { Buffer } from "node:buffer";
4
3
  import { subtle } from "uncrypto";
5
4
  import { merge } from "smob";
6
5
  import { compile } from "proxy-addr";
@@ -138,22 +137,7 @@ function sanitizeHeaderValue(value) {
138
137
  return value.replace(/[\r\n]/g, "");
139
138
  }
140
139
  //#endregion
141
- //#region src/utils/object.ts
142
- function isObject(item) {
143
- return !!item && typeof item === "object" && !Array.isArray(item);
144
- }
145
- //#endregion
146
140
  //#region src/utils/etag/module.ts
147
- /**
148
- * Determine if object is a Stats object.
149
- *
150
- * @param {object} obj
151
- * @return {boolean}
152
- * @api private
153
- */
154
- function isStatsObject(obj) {
155
- return isObject(obj) && "ctime" in obj && Object.prototype.toString.call(obj.ctime) === "[object Date]" && "mtime" in obj && Object.prototype.toString.call(obj.mtime) === "[object Date]" && "ino" in obj && typeof obj.ino === "number" && "size" in obj && typeof obj.size === "number";
156
- }
157
141
  async function sha1(str) {
158
142
  const enc = new TextEncoder();
159
143
  const hash = await subtle.digest("SHA-1", enc.encode(str));
@@ -163,39 +147,36 @@ async function sha1(str) {
163
147
  * Generate an ETag.
164
148
  */
165
149
  async function generateETag(input) {
166
- if (isStatsObject(input)) {
167
- const mtime = input.mtime.getTime().toString(16);
168
- return `"${input.size.toString(16)}-${mtime}"`;
169
- }
170
150
  if (input.length === 0) return "\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"";
171
- const entity = Buffer.isBuffer(input) ? input.toString("utf-8") : input;
172
- const hash = await sha1(entity);
173
- return `"${entity.length.toString(16)}-${hash.substring(0, 27)}"`;
151
+ const hash = await sha1(input);
152
+ return `"${input.length.toString(16)}-${hash.substring(0, 27)}"`;
174
153
  }
175
154
  /**
176
155
  * Create a simple ETag.
177
156
  */
178
- async function createEtag(input, options) {
179
- options = options || {};
180
- const weak = typeof options.weak === "boolean" ? options.weak : isStatsObject(input);
157
+ async function createEtag(input, options = {}) {
181
158
  const tag = await generateETag(input);
182
- return weak ? `W/${tag}` : tag;
159
+ return options.weak ? `W/${tag}` : tag;
160
+ }
161
+ //#endregion
162
+ //#region src/utils/object.ts
163
+ function isObject(item) {
164
+ return !!item && typeof item === "object" && !Array.isArray(item);
183
165
  }
184
166
  //#endregion
185
167
  //#region src/utils/etag/utils.ts
168
+ const textEncoder = /* @__PURE__ */ new TextEncoder();
186
169
  function buildEtagFn(input) {
187
170
  if (typeof input === "function") return input;
188
171
  input = input ?? true;
189
172
  if (input === false) return () => Promise.resolve(void 0);
190
173
  let options = { weak: true };
191
174
  if (isObject(input)) options = merge(input, options);
192
- return async (body, encoding, size) => {
193
- const buff = Buffer.isBuffer(body) ? body : Buffer.from(body, encoding);
175
+ return async (body, size) => {
194
176
  if (typeof options.threshold !== "undefined") {
195
- size = size ?? Buffer.byteLength(buff);
196
- if (size <= options.threshold) return;
177
+ if ((size ?? textEncoder.encode(body).byteLength) <= options.threshold) return;
197
178
  }
198
- return createEtag(buff, options);
179
+ return createEtag(body, options);
199
180
  };
200
181
  }
201
182
  //#endregion
@@ -616,6 +597,9 @@ var RoutupEvent = class {
616
597
  signal;
617
598
  _context;
618
599
  _routerOptions;
600
+ _nextCalled = false;
601
+ _nextResult;
602
+ _nextCalledDeferred;
619
603
  constructor(context) {
620
604
  this._context = context;
621
605
  this.request = context.request;
@@ -633,8 +617,32 @@ var RoutupEvent = class {
633
617
  if (!this._routerOptions) this._routerOptions = this._context.routerOptions();
634
618
  return this._routerOptions;
635
619
  }
620
+ get nextCalled() {
621
+ return this._nextCalled;
622
+ }
623
+ get nextResult() {
624
+ return this._nextResult;
625
+ }
626
+ whenNextCalled() {
627
+ if (!this._nextCalledDeferred) {
628
+ let resolve;
629
+ const promise = new Promise((r) => {
630
+ resolve = r;
631
+ });
632
+ this._nextCalledDeferred = {
633
+ promise,
634
+ resolve
635
+ };
636
+ if (this._nextCalled) resolve();
637
+ }
638
+ return this._nextCalledDeferred.promise;
639
+ }
636
640
  async next(error) {
637
- return this._context.next(this, error);
641
+ if (this._nextCalled) return this._nextResult;
642
+ this._nextCalled = true;
643
+ this._nextResult = this._context.next(this, error);
644
+ if (this._nextCalledDeferred) this._nextCalledDeferred.resolve();
645
+ return this._nextResult;
638
646
  }
639
647
  };
640
648
  //#endregion
@@ -974,11 +982,11 @@ var Handler = class {
974
982
  if (event.error) {
975
983
  const { fn } = this.config;
976
984
  const { error } = event;
977
- result = await this.executeWithTimeout(() => fn(error, handlerEvent), handlerEvent.routerOptions, childController);
985
+ result = await this.executeWithTimeout(() => this.resolveHandlerResult(fn(error, handlerEvent), handlerEvent), handlerEvent.routerOptions, childController);
978
986
  }
979
987
  } else {
980
988
  const { fn } = this.config;
981
- result = await this.executeWithTimeout(() => fn(handlerEvent), handlerEvent.routerOptions, childController);
989
+ result = await this.executeWithTimeout(() => this.resolveHandlerResult(fn(handlerEvent), handlerEvent), handlerEvent.routerOptions, childController);
982
990
  }
983
991
  } finally {
984
992
  if (cleanupParentListener) cleanupParentListener();
@@ -1015,6 +1023,41 @@ var Handler = class {
1015
1023
  this.config.method = method;
1016
1024
  this._method = method;
1017
1025
  }
1026
+ /**
1027
+ * Resolve a handler's return value into the final value handed to `toResponse`.
1028
+ *
1029
+ * Contract:
1030
+ * - non-undefined value → return as-is (becomes the response)
1031
+ * - `undefined` + `event.next()` was called → forward downstream result
1032
+ * - `undefined` + `event.next()` not yet called → wait until either `next()` is
1033
+ * invoked (e.g. from an async callback) or `signal` aborts. A global or
1034
+ * per-handler timeout aborts `signal` and surfaces as 408. With no timeout
1035
+ * configured and no eventual `next()` call, the request hangs by design.
1036
+ */
1037
+ async resolveHandlerResult(invocation, handlerEvent) {
1038
+ const value = await invocation;
1039
+ if (typeof value !== "undefined") return value;
1040
+ if (handlerEvent.nextCalled) return handlerEvent.nextResult;
1041
+ const { signal } = handlerEvent;
1042
+ if (signal.aborted) throw createError({
1043
+ status: 408,
1044
+ message: "Request Timeout"
1045
+ });
1046
+ return new Promise((resolve, reject) => {
1047
+ const onAbort = () => {
1048
+ signal.removeEventListener("abort", onAbort);
1049
+ reject(createError({
1050
+ status: 408,
1051
+ message: "Request Timeout"
1052
+ }));
1053
+ };
1054
+ signal.addEventListener("abort", onAbort, { once: true });
1055
+ handlerEvent.whenNextCalled().then(() => {
1056
+ signal.removeEventListener("abort", onAbort);
1057
+ resolve(handlerEvent.nextResult);
1058
+ });
1059
+ });
1060
+ }
1018
1061
  async executeWithTimeout(fn, routerOptions, controller) {
1019
1062
  const effectiveTimeout = this.resolveTimeout(routerOptions);
1020
1063
  if (!effectiveTimeout) return fn();
@@ -1325,7 +1368,9 @@ function getRequestProtocol(event, options = {}) {
1325
1368
  const PluginErrorCode = {
1326
1369
  PLUGIN: "PLUGIN",
1327
1370
  NOT_INSTALLED: "PLUGIN_NOT_INSTALLED",
1328
- INSTALL: "PLUGIN_INSTALL"
1371
+ ALREADY_INSTALLED: "PLUGIN_ALREADY_INSTALLED",
1372
+ INSTALL: "PLUGIN_INSTALL",
1373
+ DEPENDENCY: "PLUGIN_DEPENDENCY"
1329
1374
  };
1330
1375
  //#endregion
1331
1376
  //#region src/plugin/error/is.ts
@@ -1345,6 +1390,37 @@ var PluginError = class extends RoutupError {
1345
1390
  }
1346
1391
  };
1347
1392
  //#endregion
1393
+ //#region src/plugin/error/sub/already-installed.ts
1394
+ var PluginAlreadyInstalledError = class extends PluginError {
1395
+ pluginName;
1396
+ constructor(pluginName) {
1397
+ super({
1398
+ message: `Plugin "${pluginName}" is already installed on this router.`,
1399
+ code: PluginErrorCode.ALREADY_INSTALLED
1400
+ });
1401
+ this.name = "PluginAlreadyInstalledError";
1402
+ this.pluginName = pluginName;
1403
+ }
1404
+ };
1405
+ //#endregion
1406
+ //#region src/plugin/error/sub/dependency.ts
1407
+ var PluginDependencyError = class extends PluginError {
1408
+ pluginName;
1409
+ dependencyName;
1410
+ constructor(pluginName, dependencyName, detail) {
1411
+ let message;
1412
+ if (detail) message = `Plugin "${pluginName}" requires "${dependencyName}": ${detail}`;
1413
+ else message = `Plugin "${pluginName}" requires plugin "${dependencyName}" to be installed first. Register the dependency plugin before this one.`;
1414
+ super({
1415
+ message,
1416
+ code: PluginErrorCode.DEPENDENCY
1417
+ });
1418
+ this.name = "PluginDependencyError";
1419
+ this.pluginName = pluginName;
1420
+ this.dependencyName = dependencyName;
1421
+ }
1422
+ };
1423
+ //#endregion
1348
1424
  //#region src/plugin/error/sub/install.ts
1349
1425
  var PluginInstallError = class extends PluginError {
1350
1426
  pluginName;
@@ -1381,6 +1457,113 @@ function isPlugin(input) {
1381
1457
  return typeof input.install === "function" && input.install.length === 1;
1382
1458
  }
1383
1459
  //#endregion
1460
+ //#region src/plugin/semver.ts
1461
+ function parseVersion(version) {
1462
+ const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?(?:\+.+)?$/);
1463
+ if (!match) return;
1464
+ return {
1465
+ major: Number(match[1]),
1466
+ minor: Number(match[2]),
1467
+ patch: Number(match[3]),
1468
+ prerelease: match[4] ? match[4].split(".") : void 0
1469
+ };
1470
+ }
1471
+ function comparePrereleaseIdentifiers(a, b) {
1472
+ const len = Math.max(a.length, b.length);
1473
+ for (let i = 0; i < len; i++) {
1474
+ if (i >= a.length) return -1;
1475
+ if (i >= b.length) return 1;
1476
+ const aId = a[i];
1477
+ const bId = b[i];
1478
+ const aNum = /^\d+$/.test(aId) ? Number(aId) : void 0;
1479
+ const bNum = /^\d+$/.test(bId) ? Number(bId) : void 0;
1480
+ if (aNum !== void 0 && bNum !== void 0) {
1481
+ if (aNum !== bNum) return aNum - bNum;
1482
+ continue;
1483
+ }
1484
+ if (aNum !== void 0) return -1;
1485
+ if (bNum !== void 0) return 1;
1486
+ if (aId < bId) return -1;
1487
+ if (aId > bId) return 1;
1488
+ }
1489
+ return 0;
1490
+ }
1491
+ function compareVersions(a, b) {
1492
+ if (a.major !== b.major) return a.major - b.major;
1493
+ if (a.minor !== b.minor) return a.minor - b.minor;
1494
+ if (a.patch !== b.patch) return a.patch - b.patch;
1495
+ if (!a.prerelease && b.prerelease) return 1;
1496
+ if (a.prerelease && !b.prerelease) return -1;
1497
+ if (a.prerelease && b.prerelease) return comparePrereleaseIdentifiers(a.prerelease, b.prerelease);
1498
+ return 0;
1499
+ }
1500
+ /**
1501
+ * Check if a version satisfies a semver constraint.
1502
+ *
1503
+ * Supported constraint formats:
1504
+ * - Exact: '1.2.3'
1505
+ * - Comparison: '>=1.0.0', '>1.0.0', '<=2.0.0', '<2.0.0', '=1.0.0'
1506
+ * - Caret: '^1.2.3' (>=1.2.3 <2.0.0)
1507
+ * - Tilde: '~1.2.3' (>=1.2.3 <1.3.0)
1508
+ */
1509
+ function satisfiesVersion(version, constraint) {
1510
+ const parsed = parseVersion(version);
1511
+ if (!parsed) return false;
1512
+ return constraint.trim().split(/\s+/).every((part) => satisfiesSingle(parsed, part));
1513
+ }
1514
+ function satisfiesSingle(version, constraint) {
1515
+ if (constraint.startsWith("^")) return satisfiesCaret(version, constraint.substring(1));
1516
+ if (constraint.startsWith("~")) return satisfiesTilde(version, constraint.substring(1));
1517
+ if (constraint.startsWith(">=")) {
1518
+ const target = parseVersion(constraint.substring(2));
1519
+ return !!target && compareVersions(version, target) >= 0;
1520
+ }
1521
+ if (constraint.startsWith(">")) {
1522
+ const target = parseVersion(constraint.substring(1));
1523
+ return !!target && compareVersions(version, target) > 0;
1524
+ }
1525
+ if (constraint.startsWith("<=")) {
1526
+ const target = parseVersion(constraint.substring(2));
1527
+ return !!target && compareVersions(version, target) <= 0;
1528
+ }
1529
+ if (constraint.startsWith("<")) {
1530
+ const target = parseVersion(constraint.substring(1));
1531
+ return !!target && compareVersions(version, target) < 0;
1532
+ }
1533
+ if (constraint.startsWith("=")) {
1534
+ const target = parseVersion(constraint.substring(1));
1535
+ return !!target && compareVersions(version, target) === 0;
1536
+ }
1537
+ const target = parseVersion(constraint);
1538
+ return !!target && compareVersions(version, target) === 0;
1539
+ }
1540
+ /**
1541
+ * When the version under test has a prerelease tag, it only matches if it
1542
+ * shares the same [major, minor, patch] tuple as the range's lower bound.
1543
+ * This follows npm semver semantics where prereleases are scoped to their
1544
+ * exact version tuple.
1545
+ */
1546
+ function prereleaseAllowed(version, min) {
1547
+ if (!version.prerelease) return true;
1548
+ return version.major === min.major && version.minor === min.minor && version.patch === min.patch;
1549
+ }
1550
+ function satisfiesCaret(version, range) {
1551
+ const min = parseVersion(range);
1552
+ if (!min) return false;
1553
+ if (compareVersions(version, min) < 0) return false;
1554
+ if (version.prerelease && !prereleaseAllowed(version, min)) return false;
1555
+ if (min.major > 0) return version.major === min.major;
1556
+ if (min.minor > 0) return version.major === 0 && version.minor === min.minor;
1557
+ return version.major === 0 && version.minor === 0 && version.patch === min.patch;
1558
+ }
1559
+ function satisfiesTilde(version, range) {
1560
+ const min = parseVersion(range);
1561
+ if (!min) return false;
1562
+ if (compareVersions(version, min) < 0) return false;
1563
+ if (version.prerelease && !prereleaseAllowed(version, min)) return false;
1564
+ return version.major === min.major && version.minor === min.minor;
1565
+ }
1566
+ //#endregion
1384
1567
  //#region src/router/options.ts
1385
1568
  function normalizeRouterOptions(input) {
1386
1569
  if (typeof input.etag !== "undefined") input.etag = buildEtagFn(input.etag);
@@ -1449,6 +1632,18 @@ var Router = class Router {
1449
1632
  * Normalized options for this router instance.
1450
1633
  */
1451
1634
  _options;
1635
+ /**
1636
+ * Registry of installed plugins (name → version).
1637
+ *
1638
+ * @protected
1639
+ */
1640
+ plugins = /* @__PURE__ */ new Map();
1641
+ /**
1642
+ * Parent router reference for dependency lookups.
1643
+ *
1644
+ * @protected
1645
+ */
1646
+ parent;
1452
1647
  constructor(input = {}) {
1453
1648
  this.name = input.name;
1454
1649
  this.hookManager = new HookManager();
@@ -1718,6 +1913,7 @@ var Router = class Router {
1718
1913
  }
1719
1914
  if (isRouterInstance(item)) {
1720
1915
  if (path) item.setPath(path);
1916
+ item.parent = this;
1721
1917
  this.stack.push(item);
1722
1918
  continue;
1723
1919
  }
@@ -1736,11 +1932,52 @@ var Router = class Router {
1736
1932
  }
1737
1933
  return this;
1738
1934
  }
1935
+ /**
1936
+ * Check if a plugin with the given name is installed on this router
1937
+ * or any parent router.
1938
+ */
1939
+ hasPlugin(name) {
1940
+ if (this.plugins.has(name)) return true;
1941
+ if (this.parent) return this.parent.hasPlugin(name);
1942
+ return false;
1943
+ }
1944
+ /**
1945
+ * Get the version of an installed plugin by name,
1946
+ * searching this router and parent routers.
1947
+ */
1948
+ getPluginVersion(name) {
1949
+ if (this.plugins.has(name)) return this.plugins.get(name);
1950
+ if (this.parent) return this.parent.getPluginVersion(name);
1951
+ }
1952
+ validatePluginDependencies(plugin) {
1953
+ if (!plugin.dependencies || plugin.dependencies.length === 0) return;
1954
+ for (const dep of plugin.dependencies) {
1955
+ const dependency = typeof dep === "string" ? { name: dep } : dep;
1956
+ if (!this.hasPlugin(dependency.name)) {
1957
+ if (dependency.optional) continue;
1958
+ throw new PluginDependencyError(plugin.name, dependency.name);
1959
+ }
1960
+ if (dependency.version) {
1961
+ const installedVersion = this.getPluginVersion(dependency.name);
1962
+ if (!installedVersion) {
1963
+ if (dependency.optional) continue;
1964
+ throw new PluginDependencyError(plugin.name, dependency.name, `version "${dependency.version}" required but "${dependency.name}" has no version`);
1965
+ }
1966
+ if (!satisfiesVersion(installedVersion, dependency.version)) {
1967
+ if (dependency.optional) continue;
1968
+ throw new PluginDependencyError(plugin.name, dependency.name, `version "${dependency.version}" required but "${installedVersion}" is installed`);
1969
+ }
1970
+ }
1971
+ }
1972
+ }
1739
1973
  install(plugin, context = {}) {
1740
- const router = new Router({ name: context.name || plugin.name });
1974
+ if (this.plugins.has(plugin.name)) throw new PluginAlreadyInstalledError(plugin.name);
1975
+ this.validatePluginDependencies(plugin);
1976
+ const router = new Router({ name: plugin.name });
1741
1977
  plugin.install(router);
1742
1978
  if (context.path) this.use(context.path, router);
1743
1979
  else this.use(router);
1980
+ this.plugins.set(plugin.name, plugin.version);
1744
1981
  return this;
1745
1982
  }
1746
1983
  on(name, fn, priority) {
@@ -1756,6 +1993,6 @@ var Router = class Router {
1756
1993
  }
1757
1994
  };
1758
1995
  //#endregion
1759
- export { appendResponseHeaderDirective as $, isPath as A, getRequestAcceptableContentTypes as B, isWebHandler as C, defineErrorHandler as D, fromNodeMiddleware as E, RoutupEvent as F, sendAccepted as G, getRequestHeader as H, sendStream as I, isError as J, toResponse as K, sendRedirect as L, HandlerSymbol as M, HandlerType as N, defineCoreHandler as O, DispatcherEvent as P, appendResponseHeader as Q, sendFormat as R, fromWebHandler as S, fromNodeHandler as T, sendFile as U, useRequestNegotiator as V, sendCreated as W, setResponseHeaderAttachment as X, setResponseHeaderContentType as Y, setResponseContentTypeByFileName as Z, getRequestAcceptableCharset as _, PluginInstallError as a, HeaderName as at, isHandler as b, PluginErrorCode as c, getRequestHostName as d, createEventStream as et, matchRequestContentType as f, getRequestAcceptableEncodings as g, getRequestAcceptableEncoding as h, PluginNotInstalledError as i, setResponseCacheHeaders as it, PathMatcher as j, Handler as k, getRequestProtocol as l, getRequestAcceptableLanguages as m, normalizeRouterOptions as n, ErrorSymbol as nt, PluginError as o, MethodName as ot, getRequestAcceptableLanguage as p, createError as q, isPlugin as r, RoutupError as rt, isPluginError as s, Router as t, serializeEventStreamMessage as tt, getRequestIP as u, getRequestAcceptableCharsets as v, isWebHandlerProvider as w, isHandlerOptions as x, isRequestCacheable as y, getRequestAcceptableContentType as z };
1996
+ export { setResponseHeaderAttachment as $, defineErrorHandler as A, sendRedirect as B, isHandler as C, isWebHandlerProvider as D, isWebHandler as E, HandlerSymbol as F, getRequestHeader as G, getRequestAcceptableContentType as H, HandlerType as I, sendAccepted as J, sendFile as K, DispatcherEvent as L, Handler as M, isPath as N, fromNodeHandler as O, PathMatcher as P, setResponseHeaderContentType as Q, RoutupEvent as R, isRequestCacheable as S, fromWebHandler as T, getRequestAcceptableContentTypes as U, sendFormat as V, useRequestNegotiator as W, createError as X, toResponse as Y, isError as Z, getRequestAcceptableLanguages as _, PluginNotInstalledError as a, ErrorSymbol as at, getRequestAcceptableCharset as b, PluginAlreadyInstalledError as c, HeaderName as ct, PluginErrorCode as d, setResponseContentTypeByFileName as et, getRequestProtocol as f, getRequestAcceptableLanguage as g, matchRequestContentType as h, isPlugin as i, serializeEventStreamMessage as it, defineCoreHandler as j, fromNodeMiddleware as k, PluginError as l, MethodName as lt, getRequestHostName as m, normalizeRouterOptions as n, appendResponseHeaderDirective as nt, PluginInstallError as o, RoutupError as ot, getRequestIP as p, sendCreated as q, satisfiesVersion as r, createEventStream as rt, PluginDependencyError as s, setResponseCacheHeaders as st, Router as t, appendResponseHeader as tt, isPluginError as u, getRequestAcceptableEncoding as v, isHandlerOptions as w, getRequestAcceptableCharsets as x, getRequestAcceptableEncodings as y, sendStream as z };
1760
1997
 
1761
- //# sourceMappingURL=src-Ck8GklBr.mjs.map
1998
+ //# sourceMappingURL=src-C32sKqPQ.mjs.map