@jsenv/server 17.4.0 → 17.5.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.
@@ -2564,6 +2564,9 @@ const RESPONSE_KEYS_COMPOSITION = {
2564
2564
  headers: composeTwoHeaders,
2565
2565
  body: (prevBody, body) => body,
2566
2566
  bodyEncoding: (prevEncoding, encoding) => encoding,
2567
+ // measures a response hands back, merged into the server-timing header when
2568
+ // the server has serverTiming enabled (see finalizeResponseProperties)
2569
+ timing: (prevTiming, timing) => ({ ...prevTiming, ...timing }),
2567
2570
  };
2568
2571
 
2569
2572
  /**
@@ -3373,8 +3376,9 @@ const createEventHistory = (limit) => {
3373
3376
 
3374
3377
  const add = (data) => {
3375
3378
  events.push(data);
3376
-
3377
- if (events.length >= limit) {
3379
+ // Strictly above, not "as many as": shifting at `>= limit` keeps one less
3380
+ // than asked for.
3381
+ if (events.length > limit) {
3378
3382
  events.shift();
3379
3383
  }
3380
3384
  };
@@ -6147,6 +6151,9 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
6147
6151
  let statusMessage;
6148
6152
  let headers;
6149
6153
  let body;
6154
+ // measures the route hands back, merged into the server-timing header
6155
+ // when serverTiming is enabled (see finalizeResponseProperties)
6156
+ let timing;
6150
6157
 
6151
6158
  if (fetchReturnValue instanceof Response) {
6152
6159
  status = fetchReturnValue.status;
@@ -6162,6 +6169,7 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
6162
6169
  statusMessage = fetchReturnValue.statusMessage;
6163
6170
  headers = fetchReturnValue.headers || {};
6164
6171
  body = fetchReturnValue.body;
6172
+ timing = fetchReturnValue.timing;
6165
6173
  } else {
6166
6174
  throw new TypeError(
6167
6175
  `response must be a Response, or an Object, received ${fetchReturnValue}`,
@@ -6194,7 +6202,7 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
6194
6202
 
6195
6203
  onRouteMatch(route);
6196
6204
  onResponseHeaders(request, route, headers);
6197
- return { status, statusText, statusMessage, headers, body };
6205
+ return { status, statusText, statusMessage, headers, body, timing };
6198
6206
  };
6199
6207
 
6200
6208
  const matchRoutes = async (request) => {
@@ -6944,11 +6952,19 @@ The list of existing endpoints is available at ${routeInspectorUrl}.`,
6944
6952
  const timingToServerTimingResponseHeaders = (timing) => {
6945
6953
  const serverTimingHeader = {};
6946
6954
  Object.keys(timing).forEach((key, index) => {
6947
- const name = letters[index] || "zz";
6948
- serverTimingHeader[name] = {
6949
- desc: key,
6950
- dur: timing[key],
6951
- };
6955
+ // "a".."z" then "aa", "ab", …: names must be unique — they are the keys of
6956
+ // the header object, so a shared fallback would overwrite every entry past
6957
+ // it with the last one.
6958
+ const name =
6959
+ index < letters.length
6960
+ ? letters[index]
6961
+ : letters[Math.floor(index / letters.length) - 1] +
6962
+ letters[index % letters.length];
6963
+ const dur = timing[key];
6964
+ // a non-number duration is a marker ("served from memory cache"): it has
6965
+ // something to say (desc) but nothing to measure
6966
+ serverTimingHeader[name] =
6967
+ typeof dur === "number" ? { desc: key, dur } : { desc: key };
6952
6968
  });
6953
6969
  const serverTimingHeaderString =
6954
6970
  stringifyServerTimingHeader(serverTimingHeader);
@@ -7235,6 +7251,10 @@ const startServer = async ({
7235
7251
  // This param should be enabled ONLY during development on your machine
7236
7252
  canExposeSensitiveData = false,
7237
7253
  nagle = true,
7254
+ // false | true | { minDuration }: server-timing response headers.
7255
+ // minDuration (ms) drops entries that took less — 0 keeps everything, which
7256
+ // is what a test wants; a human reading devtools usually wants the noise
7257
+ // gone (the jsenv dev server passes 0.5 for that).
7238
7258
  serverTiming = false,
7239
7259
  requestWaitingMs = 0,
7240
7260
  requestWaitingCallback = ({ request, requestWaitingMs }) => {
@@ -7588,6 +7608,10 @@ const startServer = async ({
7588
7608
  sendResponseOperation.addAbortSignal(receiveRequestOperation.signal);
7589
7609
  return [receiveRequestOperation, sendResponseOperation];
7590
7610
  };
7611
+ const serverTimingMinDuration =
7612
+ serverTiming && typeof serverTiming === "object"
7613
+ ? serverTiming.minDuration || 0
7614
+ : 0;
7591
7615
  const getResponseProperties = async (request, { pushResponse }) => {
7592
7616
  const timings = {};
7593
7617
  const timing = serverTiming
@@ -7629,9 +7653,32 @@ const startServer = async ({
7629
7653
  const finalizeResponseProperties = (responseProperties) => {
7630
7654
  if (serverTiming) {
7631
7655
  startRespondingTiming.end();
7656
+ // A response can hand back measures of its own (a `timing` property —
7657
+ // durations keyed by description, null for a plain marker): they join
7658
+ // the server's own in the same server-timing header.
7659
+ if (responseProperties.timing) {
7660
+ Object.assign(timings, responseProperties.timing);
7661
+ delete responseProperties.timing;
7662
+ }
7663
+ // serverTiming.minDuration drops what took less: a request crosses
7664
+ // every registered route, and a wall of sub-millisecond ".routing"
7665
+ // entries buries the measures worth reading. Markers (null) and the
7666
+ // overall "time to start responding" — the summary the rest details —
7667
+ // always stay.
7668
+ const timingsWorthReading = {};
7669
+ for (const name of Object.keys(timings)) {
7670
+ const duration = timings[name];
7671
+ if (
7672
+ name === startRespondingTiming.name ||
7673
+ typeof duration !== "number" ||
7674
+ duration >= serverTimingMinDuration
7675
+ ) {
7676
+ timingsWorthReading[name] = duration;
7677
+ }
7678
+ }
7632
7679
  responseProperties.headers = composeTwoHeaders(
7633
7680
  responseProperties.headers,
7634
- timingToServerTimingResponseHeaders(timings),
7681
+ timingToServerTimingResponseHeaders(timingsWorthReading),
7635
7682
  );
7636
7683
  }
7637
7684
  if (requestWaitingMs) {
package/dist/ws/ws.js CHANGED
@@ -8,8 +8,7 @@ import require$$0$2 from "node:stream";
8
8
  import require$$7 from "node:url";
9
9
  import require$$0 from "node:zlib";
10
10
  import require$$0$1 from "node:buffer";
11
-
12
- var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
11
+ import require$$2 from "node:util";
13
12
 
14
13
  function getDefaultExportFromCjs (x) {
15
14
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
@@ -1693,664 +1692,6 @@ function requireReceiver () {
1693
1692
  return receiver$1;
1694
1693
  }
1695
1694
 
1696
- var util = {};
1697
-
1698
- var isBuffer;
1699
- var hasRequiredIsBuffer;
1700
-
1701
- function requireIsBuffer () {
1702
- if (hasRequiredIsBuffer) return isBuffer;
1703
- hasRequiredIsBuffer = 1;
1704
- isBuffer = function isBuffer(arg) {
1705
- return arg instanceof Buffer;
1706
- };
1707
- return isBuffer;
1708
- }
1709
-
1710
- var inherits = {exports: {}};
1711
-
1712
- var inherits_browser = {exports: {}};
1713
-
1714
- var hasRequiredInherits_browser;
1715
-
1716
- function requireInherits_browser () {
1717
- if (hasRequiredInherits_browser) return inherits_browser.exports;
1718
- hasRequiredInherits_browser = 1;
1719
- if (typeof Object.create === 'function') {
1720
- // implementation from standard node.js 'util' module
1721
- inherits_browser.exports = function inherits(ctor, superCtor) {
1722
- ctor.super_ = superCtor;
1723
- ctor.prototype = Object.create(superCtor.prototype, {
1724
- constructor: {
1725
- value: ctor,
1726
- enumerable: false,
1727
- writable: true,
1728
- configurable: true
1729
- }
1730
- });
1731
- };
1732
- } else {
1733
- // old school shim for old browsers
1734
- inherits_browser.exports = function inherits(ctor, superCtor) {
1735
- ctor.super_ = superCtor;
1736
- var TempCtor = function () {};
1737
- TempCtor.prototype = superCtor.prototype;
1738
- ctor.prototype = new TempCtor();
1739
- ctor.prototype.constructor = ctor;
1740
- };
1741
- }
1742
- return inherits_browser.exports;
1743
- }
1744
-
1745
- var hasRequiredInherits;
1746
-
1747
- function requireInherits () {
1748
- if (hasRequiredInherits) return inherits.exports;
1749
- hasRequiredInherits = 1;
1750
- try {
1751
- var util = requireUtil();
1752
- if (typeof util.inherits !== 'function') throw '';
1753
- inherits.exports = util.inherits;
1754
- } catch (e) {
1755
- inherits.exports = requireInherits_browser();
1756
- }
1757
- return inherits.exports;
1758
- }
1759
-
1760
- var hasRequiredUtil;
1761
-
1762
- function requireUtil () {
1763
- if (hasRequiredUtil) return util;
1764
- hasRequiredUtil = 1;
1765
- (function (exports) {
1766
- // Copyright Joyent, Inc. and other Node contributors.
1767
- //
1768
- // Permission is hereby granted, free of charge, to any person obtaining a
1769
- // copy of this software and associated documentation files (the
1770
- // "Software"), to deal in the Software without restriction, including
1771
- // without limitation the rights to use, copy, modify, merge, publish,
1772
- // distribute, sublicense, and/or sell copies of the Software, and to permit
1773
- // persons to whom the Software is furnished to do so, subject to the
1774
- // following conditions:
1775
- //
1776
- // The above copyright notice and this permission notice shall be included
1777
- // in all copies or substantial portions of the Software.
1778
- //
1779
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1780
- // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1781
- // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
1782
- // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
1783
- // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
1784
- // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
1785
- // USE OR OTHER DEALINGS IN THE SOFTWARE.
1786
-
1787
- var formatRegExp = /%[sdj%]/g;
1788
- exports.format = function(f) {
1789
- if (!isString(f)) {
1790
- var objects = [];
1791
- for (var i = 0; i < arguments.length; i++) {
1792
- objects.push(inspect(arguments[i]));
1793
- }
1794
- return objects.join(' ');
1795
- }
1796
-
1797
- var i = 1;
1798
- var args = arguments;
1799
- var len = args.length;
1800
- var str = String(f).replace(formatRegExp, function(x) {
1801
- if (x === '%%') return '%';
1802
- if (i >= len) return x;
1803
- switch (x) {
1804
- case '%s': return String(args[i++]);
1805
- case '%d': return Number(args[i++]);
1806
- case '%j':
1807
- try {
1808
- return JSON.stringify(args[i++]);
1809
- } catch (_) {
1810
- return '[Circular]';
1811
- }
1812
- default:
1813
- return x;
1814
- }
1815
- });
1816
- for (var x = args[i]; i < len; x = args[++i]) {
1817
- if (isNull(x) || !isObject(x)) {
1818
- str += ' ' + x;
1819
- } else {
1820
- str += ' ' + inspect(x);
1821
- }
1822
- }
1823
- return str;
1824
- };
1825
-
1826
-
1827
- // Mark that a method should not be used.
1828
- // Returns a modified function which warns once by default.
1829
- // If --no-deprecation is set, then it is a no-op.
1830
- exports.deprecate = function(fn, msg) {
1831
- // Allow for deprecating things in the process of starting up.
1832
- if (isUndefined(commonjsGlobal.process)) {
1833
- return function() {
1834
- return exports.deprecate(fn, msg).apply(this, arguments);
1835
- };
1836
- }
1837
-
1838
- if (process.noDeprecation === true) {
1839
- return fn;
1840
- }
1841
-
1842
- var warned = false;
1843
- function deprecated() {
1844
- if (!warned) {
1845
- if (process.throwDeprecation) {
1846
- throw new Error(msg);
1847
- } else if (process.traceDeprecation) {
1848
- console.trace(msg);
1849
- } else {
1850
- console.error(msg);
1851
- }
1852
- warned = true;
1853
- }
1854
- return fn.apply(this, arguments);
1855
- }
1856
-
1857
- return deprecated;
1858
- };
1859
-
1860
-
1861
- var debugs = {};
1862
- var debugEnviron;
1863
- exports.debuglog = function(set) {
1864
- if (isUndefined(debugEnviron))
1865
- debugEnviron = process.env.NODE_DEBUG || '';
1866
- set = set.toUpperCase();
1867
- if (!debugs[set]) {
1868
- if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) {
1869
- var pid = process.pid;
1870
- debugs[set] = function() {
1871
- var msg = exports.format.apply(exports, arguments);
1872
- console.error('%s %d: %s', set, pid, msg);
1873
- };
1874
- } else {
1875
- debugs[set] = function() {};
1876
- }
1877
- }
1878
- return debugs[set];
1879
- };
1880
-
1881
-
1882
- /**
1883
- * Echos the value of a value. Trys to print the value out
1884
- * in the best way possible given the different types.
1885
- *
1886
- * @param {Object} obj The object to print out.
1887
- * @param {Object} opts Optional options object that alters the output.
1888
- */
1889
- /* legacy: obj, showHidden, depth, colors*/
1890
- function inspect(obj, opts) {
1891
- // default options
1892
- var ctx = {
1893
- seen: [],
1894
- stylize: stylizeNoColor
1895
- };
1896
- // legacy...
1897
- if (arguments.length >= 3) ctx.depth = arguments[2];
1898
- if (arguments.length >= 4) ctx.colors = arguments[3];
1899
- if (isBoolean(opts)) {
1900
- // legacy...
1901
- ctx.showHidden = opts;
1902
- } else if (opts) {
1903
- // got an "options" object
1904
- exports._extend(ctx, opts);
1905
- }
1906
- // set default options
1907
- if (isUndefined(ctx.showHidden)) ctx.showHidden = false;
1908
- if (isUndefined(ctx.depth)) ctx.depth = 2;
1909
- if (isUndefined(ctx.colors)) ctx.colors = false;
1910
- if (isUndefined(ctx.customInspect)) ctx.customInspect = true;
1911
- if (ctx.colors) ctx.stylize = stylizeWithColor;
1912
- return formatValue(ctx, obj, ctx.depth);
1913
- }
1914
- exports.inspect = inspect;
1915
-
1916
-
1917
- // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
1918
- inspect.colors = {
1919
- 'bold' : [1, 22],
1920
- 'italic' : [3, 23],
1921
- 'underline' : [4, 24],
1922
- 'inverse' : [7, 27],
1923
- 'white' : [37, 39],
1924
- 'grey' : [90, 39],
1925
- 'black' : [30, 39],
1926
- 'blue' : [34, 39],
1927
- 'cyan' : [36, 39],
1928
- 'green' : [32, 39],
1929
- 'magenta' : [35, 39],
1930
- 'red' : [31, 39],
1931
- 'yellow' : [33, 39]
1932
- };
1933
-
1934
- // Don't use 'blue' not visible on cmd.exe
1935
- inspect.styles = {
1936
- 'special': 'cyan',
1937
- 'number': 'yellow',
1938
- 'boolean': 'yellow',
1939
- 'undefined': 'grey',
1940
- 'null': 'bold',
1941
- 'string': 'green',
1942
- 'date': 'magenta',
1943
- // "name": intentionally not styling
1944
- 'regexp': 'red'
1945
- };
1946
-
1947
-
1948
- function stylizeWithColor(str, styleType) {
1949
- var style = inspect.styles[styleType];
1950
-
1951
- if (style) {
1952
- return '\u001b[' + inspect.colors[style][0] + 'm' + str +
1953
- '\u001b[' + inspect.colors[style][1] + 'm';
1954
- } else {
1955
- return str;
1956
- }
1957
- }
1958
-
1959
-
1960
- function stylizeNoColor(str, styleType) {
1961
- return str;
1962
- }
1963
-
1964
-
1965
- function arrayToHash(array) {
1966
- var hash = {};
1967
-
1968
- array.forEach(function(val, idx) {
1969
- hash[val] = true;
1970
- });
1971
-
1972
- return hash;
1973
- }
1974
-
1975
-
1976
- function formatValue(ctx, value, recurseTimes) {
1977
- // Provide a hook for user-specified inspect functions.
1978
- // Check that value is an object with an inspect function on it
1979
- if (ctx.customInspect &&
1980
- value &&
1981
- isFunction(value.inspect) &&
1982
- // Filter out the util module, it's inspect function is special
1983
- value.inspect !== exports.inspect &&
1984
- // Also filter out any prototype objects using the circular check.
1985
- !(value.constructor && value.constructor.prototype === value)) {
1986
- var ret = value.inspect(recurseTimes, ctx);
1987
- if (!isString(ret)) {
1988
- ret = formatValue(ctx, ret, recurseTimes);
1989
- }
1990
- return ret;
1991
- }
1992
-
1993
- // Primitive types cannot have properties
1994
- var primitive = formatPrimitive(ctx, value);
1995
- if (primitive) {
1996
- return primitive;
1997
- }
1998
-
1999
- // Look up the keys of the object.
2000
- var keys = Object.keys(value);
2001
- var visibleKeys = arrayToHash(keys);
2002
-
2003
- if (ctx.showHidden) {
2004
- keys = Object.getOwnPropertyNames(value);
2005
- }
2006
-
2007
- // IE doesn't make error fields non-enumerable
2008
- // http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx
2009
- if (isError(value)
2010
- && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) {
2011
- return formatError(value);
2012
- }
2013
-
2014
- // Some type of object without properties can be shortcutted.
2015
- if (keys.length === 0) {
2016
- if (isFunction(value)) {
2017
- var name = value.name ? ': ' + value.name : '';
2018
- return ctx.stylize('[Function' + name + ']', 'special');
2019
- }
2020
- if (isRegExp(value)) {
2021
- return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
2022
- }
2023
- if (isDate(value)) {
2024
- return ctx.stylize(Date.prototype.toString.call(value), 'date');
2025
- }
2026
- if (isError(value)) {
2027
- return formatError(value);
2028
- }
2029
- }
2030
-
2031
- var base = '', array = false, braces = ['{', '}'];
2032
-
2033
- // Make Array say that they are Array
2034
- if (isArray(value)) {
2035
- array = true;
2036
- braces = ['[', ']'];
2037
- }
2038
-
2039
- // Make functions say that they are functions
2040
- if (isFunction(value)) {
2041
- var n = value.name ? ': ' + value.name : '';
2042
- base = ' [Function' + n + ']';
2043
- }
2044
-
2045
- // Make RegExps say that they are RegExps
2046
- if (isRegExp(value)) {
2047
- base = ' ' + RegExp.prototype.toString.call(value);
2048
- }
2049
-
2050
- // Make dates with properties first say the date
2051
- if (isDate(value)) {
2052
- base = ' ' + Date.prototype.toUTCString.call(value);
2053
- }
2054
-
2055
- // Make error with message first say the error
2056
- if (isError(value)) {
2057
- base = ' ' + formatError(value);
2058
- }
2059
-
2060
- if (keys.length === 0 && (!array || value.length == 0)) {
2061
- return braces[0] + base + braces[1];
2062
- }
2063
-
2064
- if (recurseTimes < 0) {
2065
- if (isRegExp(value)) {
2066
- return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
2067
- } else {
2068
- return ctx.stylize('[Object]', 'special');
2069
- }
2070
- }
2071
-
2072
- ctx.seen.push(value);
2073
-
2074
- var output;
2075
- if (array) {
2076
- output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);
2077
- } else {
2078
- output = keys.map(function(key) {
2079
- return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array);
2080
- });
2081
- }
2082
-
2083
- ctx.seen.pop();
2084
-
2085
- return reduceToSingleString(output, base, braces);
2086
- }
2087
-
2088
-
2089
- function formatPrimitive(ctx, value) {
2090
- if (isUndefined(value))
2091
- return ctx.stylize('undefined', 'undefined');
2092
- if (isString(value)) {
2093
- var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
2094
- .replace(/'/g, "\\'")
2095
- .replace(/\\"/g, '"') + '\'';
2096
- return ctx.stylize(simple, 'string');
2097
- }
2098
- if (isNumber(value))
2099
- return ctx.stylize('' + value, 'number');
2100
- if (isBoolean(value))
2101
- return ctx.stylize('' + value, 'boolean');
2102
- // For some reason typeof null is "object", so special case here.
2103
- if (isNull(value))
2104
- return ctx.stylize('null', 'null');
2105
- }
2106
-
2107
-
2108
- function formatError(value) {
2109
- return '[' + Error.prototype.toString.call(value) + ']';
2110
- }
2111
-
2112
-
2113
- function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
2114
- var output = [];
2115
- for (var i = 0, l = value.length; i < l; ++i) {
2116
- if (hasOwnProperty(value, String(i))) {
2117
- output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
2118
- String(i), true));
2119
- } else {
2120
- output.push('');
2121
- }
2122
- }
2123
- keys.forEach(function(key) {
2124
- if (!key.match(/^\d+$/)) {
2125
- output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
2126
- key, true));
2127
- }
2128
- });
2129
- return output;
2130
- }
2131
-
2132
-
2133
- function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {
2134
- var name, str, desc;
2135
- desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] };
2136
- if (desc.get) {
2137
- if (desc.set) {
2138
- str = ctx.stylize('[Getter/Setter]', 'special');
2139
- } else {
2140
- str = ctx.stylize('[Getter]', 'special');
2141
- }
2142
- } else {
2143
- if (desc.set) {
2144
- str = ctx.stylize('[Setter]', 'special');
2145
- }
2146
- }
2147
- if (!hasOwnProperty(visibleKeys, key)) {
2148
- name = '[' + key + ']';
2149
- }
2150
- if (!str) {
2151
- if (ctx.seen.indexOf(desc.value) < 0) {
2152
- if (isNull(recurseTimes)) {
2153
- str = formatValue(ctx, desc.value, null);
2154
- } else {
2155
- str = formatValue(ctx, desc.value, recurseTimes - 1);
2156
- }
2157
- if (str.indexOf('\n') > -1) {
2158
- if (array) {
2159
- str = str.split('\n').map(function(line) {
2160
- return ' ' + line;
2161
- }).join('\n').substr(2);
2162
- } else {
2163
- str = '\n' + str.split('\n').map(function(line) {
2164
- return ' ' + line;
2165
- }).join('\n');
2166
- }
2167
- }
2168
- } else {
2169
- str = ctx.stylize('[Circular]', 'special');
2170
- }
2171
- }
2172
- if (isUndefined(name)) {
2173
- if (array && key.match(/^\d+$/)) {
2174
- return str;
2175
- }
2176
- name = JSON.stringify('' + key);
2177
- if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
2178
- name = name.substr(1, name.length - 2);
2179
- name = ctx.stylize(name, 'name');
2180
- } else {
2181
- name = name.replace(/'/g, "\\'")
2182
- .replace(/\\"/g, '"')
2183
- .replace(/(^"|"$)/g, "'");
2184
- name = ctx.stylize(name, 'string');
2185
- }
2186
- }
2187
-
2188
- return name + ': ' + str;
2189
- }
2190
-
2191
-
2192
- function reduceToSingleString(output, base, braces) {
2193
- var length = output.reduce(function(prev, cur) {
2194
- if (cur.indexOf('\n') >= 0) ;
2195
- return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1;
2196
- }, 0);
2197
-
2198
- if (length > 60) {
2199
- return braces[0] +
2200
- (base === '' ? '' : base + '\n ') +
2201
- ' ' +
2202
- output.join(',\n ') +
2203
- ' ' +
2204
- braces[1];
2205
- }
2206
-
2207
- return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
2208
- }
2209
-
2210
-
2211
- // NOTE: These type checking functions intentionally don't use `instanceof`
2212
- // because it is fragile and can be easily faked with `Object.create()`.
2213
- function isArray(ar) {
2214
- return Array.isArray(ar);
2215
- }
2216
- exports.isArray = isArray;
2217
-
2218
- function isBoolean(arg) {
2219
- return typeof arg === 'boolean';
2220
- }
2221
- exports.isBoolean = isBoolean;
2222
-
2223
- function isNull(arg) {
2224
- return arg === null;
2225
- }
2226
- exports.isNull = isNull;
2227
-
2228
- function isNullOrUndefined(arg) {
2229
- return arg == null;
2230
- }
2231
- exports.isNullOrUndefined = isNullOrUndefined;
2232
-
2233
- function isNumber(arg) {
2234
- return typeof arg === 'number';
2235
- }
2236
- exports.isNumber = isNumber;
2237
-
2238
- function isString(arg) {
2239
- return typeof arg === 'string';
2240
- }
2241
- exports.isString = isString;
2242
-
2243
- function isSymbol(arg) {
2244
- return typeof arg === 'symbol';
2245
- }
2246
- exports.isSymbol = isSymbol;
2247
-
2248
- function isUndefined(arg) {
2249
- return arg === void 0;
2250
- }
2251
- exports.isUndefined = isUndefined;
2252
-
2253
- function isRegExp(re) {
2254
- return isObject(re) && objectToString(re) === '[object RegExp]';
2255
- }
2256
- exports.isRegExp = isRegExp;
2257
-
2258
- function isObject(arg) {
2259
- return typeof arg === 'object' && arg !== null;
2260
- }
2261
- exports.isObject = isObject;
2262
-
2263
- function isDate(d) {
2264
- return isObject(d) && objectToString(d) === '[object Date]';
2265
- }
2266
- exports.isDate = isDate;
2267
-
2268
- function isError(e) {
2269
- return isObject(e) &&
2270
- (objectToString(e) === '[object Error]' || e instanceof Error);
2271
- }
2272
- exports.isError = isError;
2273
-
2274
- function isFunction(arg) {
2275
- return typeof arg === 'function';
2276
- }
2277
- exports.isFunction = isFunction;
2278
-
2279
- function isPrimitive(arg) {
2280
- return arg === null ||
2281
- typeof arg === 'boolean' ||
2282
- typeof arg === 'number' ||
2283
- typeof arg === 'string' ||
2284
- typeof arg === 'symbol' || // ES6 symbol
2285
- typeof arg === 'undefined';
2286
- }
2287
- exports.isPrimitive = isPrimitive;
2288
-
2289
- exports.isBuffer = requireIsBuffer();
2290
-
2291
- function objectToString(o) {
2292
- return Object.prototype.toString.call(o);
2293
- }
2294
-
2295
-
2296
- function pad(n) {
2297
- return n < 10 ? '0' + n.toString(10) : n.toString(10);
2298
- }
2299
-
2300
-
2301
- var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
2302
- 'Oct', 'Nov', 'Dec'];
2303
-
2304
- // 26 Feb 16:19:34
2305
- function timestamp() {
2306
- var d = new Date();
2307
- var time = [pad(d.getHours()),
2308
- pad(d.getMinutes()),
2309
- pad(d.getSeconds())].join(':');
2310
- return [d.getDate(), months[d.getMonth()], time].join(' ');
2311
- }
2312
-
2313
-
2314
- // log is just a thin wrapper to console.log that prepends a timestamp
2315
- exports.log = function() {
2316
- console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments));
2317
- };
2318
-
2319
-
2320
- /**
2321
- * Inherit the prototype methods from one constructor into another.
2322
- *
2323
- * The Function.prototype.inherits from lang.js rewritten as a standalone
2324
- * function (not on Function.prototype). NOTE: If this file is to be loaded
2325
- * during bootstrapping this function needs to be rewritten using some native
2326
- * functions as prototype setup using normal JavaScript does not work as
2327
- * expected during bootstrapping (see mirror.js in r114903).
2328
- *
2329
- * @param {function} ctor Constructor function which needs to inherit the
2330
- * prototype.
2331
- * @param {function} superCtor Constructor function to inherit prototype from.
2332
- */
2333
- exports.inherits = requireInherits();
2334
-
2335
- exports._extend = function(origin, add) {
2336
- // Don't do anything if add isn't an object
2337
- if (!add || !isObject(add)) return origin;
2338
-
2339
- var keys = Object.keys(add);
2340
- var i = keys.length;
2341
- while (i--) {
2342
- origin[keys[i]] = add[keys[i]];
2343
- }
2344
- return origin;
2345
- };
2346
-
2347
- function hasOwnProperty(obj, prop) {
2348
- return Object.prototype.hasOwnProperty.call(obj, prop);
2349
- }
2350
- } (util));
2351
- return util;
2352
- }
2353
-
2354
1695
  /* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex" }] */
2355
1696
 
2356
1697
  var sender$1;
@@ -2364,7 +1705,7 @@ function requireSender () {
2364
1705
  const { randomFillSync } = require$$1;
2365
1706
  const {
2366
1707
  types: { isUint8Array }
2367
- } = requireUtil();
1708
+ } = require$$2;
2368
1709
 
2369
1710
  const PerMessageDeflate = requirePermessageDeflate();
2370
1711
  const { EMPTY_BUFFER, kWebSocket, NOOP } = requireConstants();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "17.4.0",
3
+ "version": "17.5.0",
4
4
  "type": "module",
5
5
  "description": "A modern Node.js HTTP server with declarative routing, content negotiation, and WebSocket support",
6
6
  "repository": {
@@ -31,4 +31,7 @@ const RESPONSE_KEYS_COMPOSITION = {
31
31
  headers: composeTwoHeaders,
32
32
  body: (prevBody, body) => body,
33
33
  bodyEncoding: (prevEncoding, encoding) => encoding,
34
+ // measures a response hands back, merged into the server-timing header when
35
+ // the server has serverTiming enabled (see finalizeResponseProperties)
36
+ timing: (prevTiming, timing) => ({ ...prevTiming, ...timing }),
34
37
  };
@@ -4,11 +4,7 @@
4
4
  * It is meant to be used inside "requestToResponse"
5
5
  */
6
6
 
7
- import {
8
- urlIsOrIsInsideOf,
9
- urlToExtension,
10
- urlToPathname,
11
- } from "@jsenv/urls";
7
+ import { urlIsOrIsInsideOf, urlToExtension, urlToPathname } from "@jsenv/urls";
12
8
  import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
13
9
  import { createReadStream, readFile, statSync } from "node:fs";
14
10
 
@@ -279,6 +279,9 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
279
279
  let statusMessage;
280
280
  let headers;
281
281
  let body;
282
+ // measures the route hands back, merged into the server-timing header
283
+ // when serverTiming is enabled (see finalizeResponseProperties)
284
+ let timing;
282
285
 
283
286
  if (fetchReturnValue instanceof Response) {
284
287
  status = fetchReturnValue.status;
@@ -294,6 +297,7 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
294
297
  statusMessage = fetchReturnValue.statusMessage;
295
298
  headers = fetchReturnValue.headers || {};
296
299
  body = fetchReturnValue.body;
300
+ timing = fetchReturnValue.timing;
297
301
  } else {
298
302
  throw new TypeError(
299
303
  `response must be a Response, or an Object, received ${fetchReturnValue}`,
@@ -326,7 +330,7 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
326
330
 
327
331
  onRouteMatch(route);
328
332
  onResponseHeaders(request, route, headers);
329
- return { status, statusText, statusMessage, headers, body };
333
+ return { status, statusText, statusMessage, headers, body, timing };
330
334
  };
331
335
 
332
336
  const matchRoutes = async (request) => {
@@ -412,8 +412,9 @@ const createEventHistory = (limit) => {
412
412
 
413
413
  const add = (data) => {
414
414
  events.push(data);
415
-
416
- if (events.length >= limit) {
415
+ // Strictly above, not "as many as": shifting at `>= limit` keeps one less
416
+ // than asked for.
417
+ if (events.length > limit) {
417
418
  events.shift();
418
419
  }
419
420
  };
@@ -12,11 +12,19 @@ import {
12
12
  export const timingToServerTimingResponseHeaders = (timing) => {
13
13
  const serverTimingHeader = {};
14
14
  Object.keys(timing).forEach((key, index) => {
15
- const name = letters[index] || "zz";
16
- serverTimingHeader[name] = {
17
- desc: key,
18
- dur: timing[key],
19
- };
15
+ // "a".."z" then "aa", "ab", …: names must be unique — they are the keys of
16
+ // the header object, so a shared fallback would overwrite every entry past
17
+ // it with the last one.
18
+ const name =
19
+ index < letters.length
20
+ ? letters[index]
21
+ : letters[Math.floor(index / letters.length) - 1] +
22
+ letters[index % letters.length];
23
+ const dur = timing[key];
24
+ // a non-number duration is a marker ("served from memory cache"): it has
25
+ // something to say (desc) but nothing to measure
26
+ serverTimingHeader[name] =
27
+ typeof dur === "number" ? { desc: key, dur } : { desc: key };
20
28
  });
21
29
  const serverTimingHeaderString =
22
30
  stringifyServerTimingHeader(serverTimingHeader);
@@ -149,6 +149,10 @@ export const startServer = async ({
149
149
  // This param should be enabled ONLY during development on your machine
150
150
  canExposeSensitiveData = false,
151
151
  nagle = true,
152
+ // false | true | { minDuration }: server-timing response headers.
153
+ // minDuration (ms) drops entries that took less — 0 keeps everything, which
154
+ // is what a test wants; a human reading devtools usually wants the noise
155
+ // gone (the jsenv dev server passes 0.5 for that).
152
156
  serverTiming = false,
153
157
  requestWaitingMs = 0,
154
158
  requestWaitingCallback = ({ request, requestWaitingMs }) => {
@@ -502,6 +506,10 @@ export const startServer = async ({
502
506
  sendResponseOperation.addAbortSignal(receiveRequestOperation.signal);
503
507
  return [receiveRequestOperation, sendResponseOperation];
504
508
  };
509
+ const serverTimingMinDuration =
510
+ serverTiming && typeof serverTiming === "object"
511
+ ? serverTiming.minDuration || 0
512
+ : 0;
505
513
  const getResponseProperties = async (request, { pushResponse }) => {
506
514
  const timings = {};
507
515
  const timing = serverTiming
@@ -543,9 +551,32 @@ export const startServer = async ({
543
551
  const finalizeResponseProperties = (responseProperties) => {
544
552
  if (serverTiming) {
545
553
  startRespondingTiming.end();
554
+ // A response can hand back measures of its own (a `timing` property —
555
+ // durations keyed by description, null for a plain marker): they join
556
+ // the server's own in the same server-timing header.
557
+ if (responseProperties.timing) {
558
+ Object.assign(timings, responseProperties.timing);
559
+ delete responseProperties.timing;
560
+ }
561
+ // serverTiming.minDuration drops what took less: a request crosses
562
+ // every registered route, and a wall of sub-millisecond ".routing"
563
+ // entries buries the measures worth reading. Markers (null) and the
564
+ // overall "time to start responding" — the summary the rest details —
565
+ // always stay.
566
+ const timingsWorthReading = {};
567
+ for (const name of Object.keys(timings)) {
568
+ const duration = timings[name];
569
+ if (
570
+ name === startRespondingTiming.name ||
571
+ typeof duration !== "number" ||
572
+ duration >= serverTimingMinDuration
573
+ ) {
574
+ timingsWorthReading[name] = duration;
575
+ }
576
+ }
546
577
  responseProperties.headers = composeTwoHeaders(
547
578
  responseProperties.headers,
548
- timingToServerTimingResponseHeaders(timings),
579
+ timingToServerTimingResponseHeaders(timingsWorthReading),
549
580
  );
550
581
  }
551
582
  if (requestWaitingMs) {