@naturalcycles/backend-lib 9.77.2 → 9.78.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.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Returns true if the given Content-Type (parameters, e.g `; charset=utf-8`, are ignored) is compressible.
3
+ *
4
+ * Vendored (and modernized) from the `compressible` package.
5
+ * Instead of loading the whole `mime-db` (~2500 types), it's reduced to the equivalent:
6
+ * a regex + the list of compressible types that the regex doesn't match (as of mime-db@1.54.0).
7
+ * No type matched by the regex is marked as non-compressible in mime-db.
8
+ */
9
+ export declare function compressible(type: string): boolean;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Returns true if the given Content-Type (parameters, e.g `; charset=utf-8`, are ignored) is compressible.
3
+ *
4
+ * Vendored (and modernized) from the `compressible` package.
5
+ * Instead of loading the whole `mime-db` (~2500 types), it's reduced to the equivalent:
6
+ * a regex + the list of compressible types that the regex doesn't match (as of mime-db@1.54.0).
7
+ * No type matched by the regex is marked as non-compressible in mime-db.
8
+ */
9
+ export function compressible(type) {
10
+ const mime = EXTRACT_TYPE_REGEX.exec(type)[1].toLowerCase();
11
+ return COMPRESSIBLE_TYPE_REGEX.test(mime) || COMPRESSIBLE_TYPES.has(mime);
12
+ }
13
+ const EXTRACT_TYPE_REGEX = /^\s*([^;\s]*)/;
14
+ const COMPRESSIBLE_TYPE_REGEX = /^text\/|\+(?:json|text|xml)$/;
15
+ const COMPRESSIBLE_TYPES = new Set([
16
+ 'application/dart',
17
+ 'application/ecmascript',
18
+ 'application/javascript',
19
+ 'application/json',
20
+ 'application/octet-stream',
21
+ 'application/postscript',
22
+ 'application/raml+yaml',
23
+ 'application/rtf',
24
+ 'application/tar',
25
+ 'application/toml',
26
+ 'application/vnd.dart',
27
+ 'application/vnd.ms-fontobject',
28
+ 'application/vnd.ms-opentype',
29
+ 'application/wasm',
30
+ 'application/x-httpd-php',
31
+ 'application/x-javascript',
32
+ 'application/x-ns-proxy-autoconfig',
33
+ 'application/x-sh',
34
+ 'application/x-tar',
35
+ 'application/x-virtualbox-hdd',
36
+ 'application/x-virtualbox-ova',
37
+ 'application/x-virtualbox-ovf',
38
+ 'application/x-virtualbox-vbox',
39
+ 'application/x-virtualbox-vdi',
40
+ 'application/x-virtualbox-vhd',
41
+ 'application/x-virtualbox-vmdk',
42
+ 'application/x-www-form-urlencoded',
43
+ 'application/xml',
44
+ 'application/xml-dtd',
45
+ 'font/otf',
46
+ 'font/ttf',
47
+ 'image/bmp',
48
+ 'image/vnd.adobe.photoshop',
49
+ 'image/vnd.microsoft.icon',
50
+ 'image/vnd.ms-dds',
51
+ 'image/x-icon',
52
+ 'image/x-ms-bmp',
53
+ 'message/rfc822',
54
+ 'model/gltf-binary',
55
+ 'x-shader/x-fragment',
56
+ 'x-shader/x-vertex',
57
+ ]);
@@ -1,2 +1,14 @@
1
- import onFinished from 'on-finished';
2
- export { onFinished };
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ /**
3
+ * Invokes the listener once the request/response has finished (completed, closed or errored).
4
+ * If it has already finished - the listener is invoked asynchronously (on the next tick).
5
+ *
6
+ * Listener runs in the async context (e.g AsyncLocalStorage) of the onFinished caller.
7
+ *
8
+ * Vendored (and modernized) from the `on-finished` package, built on top of `stream.finished`.
9
+ * Stream listeners are attached once per msg (subsequent calls are queued), as `stream.finished`
10
+ * is too expensive to call per listener (and would exceed the default MaxListeners).
11
+ */
12
+ export declare function onFinished<T extends IncomingMessage | ServerResponse>(msg: T, listener: Listener<T>): T;
13
+ type Listener<T> = (err: Error | null, msg: T) => void;
14
+ export {};
@@ -1,2 +1,35 @@
1
- import onFinished from 'on-finished';
2
- export { onFinished };
1
+ import { AsyncResource } from 'node:async_hooks';
2
+ import { finished } from 'node:stream';
3
+ /**
4
+ * Invokes the listener once the request/response has finished (completed, closed or errored).
5
+ * If it has already finished - the listener is invoked asynchronously (on the next tick).
6
+ *
7
+ * Listener runs in the async context (e.g AsyncLocalStorage) of the onFinished caller.
8
+ *
9
+ * Vendored (and modernized) from the `on-finished` package, built on top of `stream.finished`.
10
+ * Stream listeners are attached once per msg (subsequent calls are queued), as `stream.finished`
11
+ * is too expensive to call per listener (and would exceed the default MaxListeners).
12
+ */
13
+ export function onFinished(msg, listener) {
14
+ const m = msg;
15
+ const queue = m[kQueue];
16
+ if (queue) {
17
+ queue.push(bindAsyncContext(listener));
18
+ return msg;
19
+ }
20
+ const q = (m[kQueue] = [bindAsyncContext(listener)]);
21
+ finished(msg, err => {
22
+ m[kQueue] = undefined;
23
+ for (const fn of q)
24
+ fn(err ?? null, msg);
25
+ });
26
+ return msg;
27
+ }
28
+ const kQueue = Symbol('onFinished');
29
+ /**
30
+ * Faster alternative to `AsyncResource.bind` (which is ~0.8µs per call).
31
+ */
32
+ function bindAsyncContext(fn) {
33
+ const resource = new AsyncResource('onFinished');
34
+ return (err, msg) => resource.runInAsyncScope(fn, null, err, msg);
35
+ }
@@ -0,0 +1,11 @@
1
+ import type { ServerResponse } from 'node:http';
2
+ /**
3
+ * Invokes the listener once, right before the response writes its headers
4
+ * (via `res.writeHead`, which is also called implicitly by the first `write`/`end`).
5
+ *
6
+ * Headers passed to `writeHead` are applied to `res` before the listener runs,
7
+ * so the listener sees (and can modify) the final headers and statusCode.
8
+ *
9
+ * Vendored (and modernized) from the `on-headers` package.
10
+ */
11
+ export declare function onHeaders(res: ServerResponse, listener: (this: ServerResponse) => void): void;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Invokes the listener once, right before the response writes its headers
3
+ * (via `res.writeHead`, which is also called implicitly by the first `write`/`end`).
4
+ *
5
+ * Headers passed to `writeHead` are applied to `res` before the listener runs,
6
+ * so the listener sees (and can modify) the final headers and statusCode.
7
+ *
8
+ * Vendored (and modernized) from the `on-headers` package.
9
+ */
10
+ export function onHeaders(res, listener) {
11
+ // Loosely typed, to call it with the (statusCode, reason?) signature, bypassing overloads
12
+ const prevWriteHead = res.writeHead;
13
+ let fired = false;
14
+ res.writeHead = function writeHead(statusCode, reasonOrHeaders, headers) {
15
+ if (fired)
16
+ return prevWriteHead.apply(this, arguments);
17
+ fired = true;
18
+ const reason = typeof reasonOrHeaders === 'string' ? reasonOrHeaders : undefined;
19
+ headers = reason === undefined ? reasonOrHeaders : headers;
20
+ this.statusCode = statusCode;
21
+ if (headers)
22
+ setHeaders(this, headers);
23
+ listener.call(this);
24
+ // Reason phrase is dropped if the listener changed the statusCode
25
+ return reason !== undefined && this.statusCode === statusCode
26
+ ? prevWriteHead.call(this, statusCode, reason)
27
+ : prevWriteHead.call(this, this.statusCode);
28
+ };
29
+ }
30
+ function setHeaders(res, headers) {
31
+ if (!Array.isArray(headers)) {
32
+ for (const k of Object.keys(headers)) {
33
+ if (k)
34
+ res.setHeader(k, headers[k]);
35
+ }
36
+ return;
37
+ }
38
+ if (Array.isArray(headers[0])) {
39
+ // 2D: [[k, v], [k, v]]
40
+ for (const [k, v] of headers) {
41
+ if (k)
42
+ res.setHeader(k, v);
43
+ }
44
+ return;
45
+ }
46
+ // 1D (raw): [k, v, k, v], keys may repeat
47
+ if (headers.length % 2)
48
+ throw new TypeError('headers array is malformed');
49
+ for (let i = 0; i < headers.length; i += 2) {
50
+ res.removeHeader(headers[i]);
51
+ }
52
+ for (let i = 0; i < headers.length; i += 2) {
53
+ if (headers[i])
54
+ res.appendHeader(headers[i], headers[i + 1]);
55
+ }
56
+ }
@@ -10,11 +10,11 @@
10
10
  * MIT Licensed
11
11
  */
12
12
  import zlib from 'node:zlib';
13
- import compressible from 'compressible';
14
13
  // @ts-expect-error no types
15
14
  import Negotiator from 'negotiator';
16
- import onHeaders from 'on-headers';
17
- import vary from 'vary';
15
+ import { compressible } from '../compressible.js';
16
+ import { onHeaders } from '../onHeaders.js';
17
+ import { vary } from '../vary.js';
18
18
  const SUPPORTED_ENCODING = ['zstd', 'br', 'gzip', 'deflate', 'identity'];
19
19
  const PREFERRED_ENCODING = ['zstd', 'br', 'gzip'];
20
20
  const encodingSupported = new Set(['gzip', 'deflate', 'identity', 'br', 'zstd']);
package/dist/vary.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { ServerResponse } from 'node:http';
2
+ /**
3
+ * Adds the field (header name) to the response's Vary header, unless it's already there
4
+ * (case-insensitive). Vary: * stays as is, and adding '*' replaces the whole header with '*'.
5
+ *
6
+ * Vendored (and modernized) from the `vary` package.
7
+ */
8
+ export declare function vary(res: ServerResponse, field: string): void;
package/dist/vary.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Adds the field (header name) to the response's Vary header, unless it's already there
3
+ * (case-insensitive). Vary: * stays as is, and adding '*' replaces the whole header with '*'.
4
+ *
5
+ * Vendored (and modernized) from the `vary` package.
6
+ */
7
+ export function vary(res, field) {
8
+ const header = res.getHeader('Vary');
9
+ const value = Array.isArray(header) ? header.join(', ') : String(header ?? '');
10
+ if (!value) {
11
+ res.setHeader('Vary', field);
12
+ return;
13
+ }
14
+ const fields = new Set(value
15
+ .toLowerCase()
16
+ .split(',')
17
+ .map(f => f.trim()));
18
+ if (field === '*' || fields.has('*')) {
19
+ if (value !== '*')
20
+ res.setHeader('Vary', '*');
21
+ }
22
+ else if (!fields.has(field.toLowerCase())) {
23
+ res.setHeader('Vary', `${value}, ${field}`);
24
+ }
25
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/backend-lib",
3
3
  "type": "module",
4
- "version": "9.77.2",
4
+ "version": "9.78.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/db-lib": "^10",
7
7
  "@naturalcycles/js-lib": "^15",
@@ -10,21 +10,13 @@
10
10
  "@types/cookie-parser": "^1",
11
11
  "@types/cors": "^2",
12
12
  "@types/express": "^5",
13
- "@types/compressible": "^2",
14
- "@types/on-finished": "^2",
15
- "@types/on-headers": "^1",
16
- "@types/vary": "^1",
17
13
  "busboy": "^1",
18
- "compressible": "^2",
19
14
  "cookie-parser": "^1",
20
15
  "cors": "^2",
21
16
  "express": "^5",
22
17
  "helmet": "^8",
23
18
  "negotiator": "^1",
24
- "on-headers": "^1",
25
- "on-finished": "^2",
26
- "tslib": "^2",
27
- "vary": "^1"
19
+ "tslib": "^2"
28
20
  },
29
21
  "peerDependencies": {
30
22
  "@sentry/node-core": "^10",
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Returns true if the given Content-Type (parameters, e.g `; charset=utf-8`, are ignored) is compressible.
3
+ *
4
+ * Vendored (and modernized) from the `compressible` package.
5
+ * Instead of loading the whole `mime-db` (~2500 types), it's reduced to the equivalent:
6
+ * a regex + the list of compressible types that the regex doesn't match (as of mime-db@1.54.0).
7
+ * No type matched by the regex is marked as non-compressible in mime-db.
8
+ */
9
+ export function compressible(type: string): boolean {
10
+ const mime = EXTRACT_TYPE_REGEX.exec(type)![1]!.toLowerCase()
11
+ return COMPRESSIBLE_TYPE_REGEX.test(mime) || COMPRESSIBLE_TYPES.has(mime)
12
+ }
13
+
14
+ const EXTRACT_TYPE_REGEX = /^\s*([^;\s]*)/
15
+ const COMPRESSIBLE_TYPE_REGEX = /^text\/|\+(?:json|text|xml)$/
16
+
17
+ const COMPRESSIBLE_TYPES = new Set([
18
+ 'application/dart',
19
+ 'application/ecmascript',
20
+ 'application/javascript',
21
+ 'application/json',
22
+ 'application/octet-stream',
23
+ 'application/postscript',
24
+ 'application/raml+yaml',
25
+ 'application/rtf',
26
+ 'application/tar',
27
+ 'application/toml',
28
+ 'application/vnd.dart',
29
+ 'application/vnd.ms-fontobject',
30
+ 'application/vnd.ms-opentype',
31
+ 'application/wasm',
32
+ 'application/x-httpd-php',
33
+ 'application/x-javascript',
34
+ 'application/x-ns-proxy-autoconfig',
35
+ 'application/x-sh',
36
+ 'application/x-tar',
37
+ 'application/x-virtualbox-hdd',
38
+ 'application/x-virtualbox-ova',
39
+ 'application/x-virtualbox-ovf',
40
+ 'application/x-virtualbox-vbox',
41
+ 'application/x-virtualbox-vdi',
42
+ 'application/x-virtualbox-vhd',
43
+ 'application/x-virtualbox-vmdk',
44
+ 'application/x-www-form-urlencoded',
45
+ 'application/xml',
46
+ 'application/xml-dtd',
47
+ 'font/otf',
48
+ 'font/ttf',
49
+ 'image/bmp',
50
+ 'image/vnd.adobe.photoshop',
51
+ 'image/vnd.microsoft.icon',
52
+ 'image/vnd.ms-dds',
53
+ 'image/x-icon',
54
+ 'image/x-ms-bmp',
55
+ 'message/rfc822',
56
+ 'model/gltf-binary',
57
+ 'x-shader/x-fragment',
58
+ 'x-shader/x-vertex',
59
+ ])
package/src/onFinished.ts CHANGED
@@ -1,3 +1,44 @@
1
- import onFinished from 'on-finished'
1
+ import { AsyncResource } from 'node:async_hooks'
2
+ import type { IncomingMessage, ServerResponse } from 'node:http'
3
+ import { finished } from 'node:stream'
2
4
 
3
- export { onFinished }
5
+ /**
6
+ * Invokes the listener once the request/response has finished (completed, closed or errored).
7
+ * If it has already finished - the listener is invoked asynchronously (on the next tick).
8
+ *
9
+ * Listener runs in the async context (e.g AsyncLocalStorage) of the onFinished caller.
10
+ *
11
+ * Vendored (and modernized) from the `on-finished` package, built on top of `stream.finished`.
12
+ * Stream listeners are attached once per msg (subsequent calls are queued), as `stream.finished`
13
+ * is too expensive to call per listener (and would exceed the default MaxListeners).
14
+ */
15
+ export function onFinished<T extends IncomingMessage | ServerResponse>(
16
+ msg: T,
17
+ listener: Listener<T>,
18
+ ): T {
19
+ const m = msg as T & { [kQueue]?: Listener<T>[] }
20
+ const queue = m[kQueue]
21
+ if (queue) {
22
+ queue.push(bindAsyncContext(listener))
23
+ return msg
24
+ }
25
+
26
+ const q = (m[kQueue] = [bindAsyncContext(listener)])
27
+ finished(msg, err => {
28
+ m[kQueue] = undefined
29
+ for (const fn of q) fn(err ?? null, msg)
30
+ })
31
+ return msg
32
+ }
33
+
34
+ type Listener<T> = (err: Error | null, msg: T) => void
35
+
36
+ const kQueue = Symbol('onFinished')
37
+
38
+ /**
39
+ * Faster alternative to `AsyncResource.bind` (which is ~0.8µs per call).
40
+ */
41
+ function bindAsyncContext<T>(fn: Listener<T>): Listener<T> {
42
+ const resource = new AsyncResource('onFinished')
43
+ return (err, msg) => resource.runInAsyncScope(fn, null, err, msg)
44
+ }
@@ -0,0 +1,66 @@
1
+ import type { OutgoingHttpHeader, OutgoingHttpHeaders, ServerResponse } from 'node:http'
2
+
3
+ /**
4
+ * Invokes the listener once, right before the response writes its headers
5
+ * (via `res.writeHead`, which is also called implicitly by the first `write`/`end`).
6
+ *
7
+ * Headers passed to `writeHead` are applied to `res` before the listener runs,
8
+ * so the listener sees (and can modify) the final headers and statusCode.
9
+ *
10
+ * Vendored (and modernized) from the `on-headers` package.
11
+ */
12
+ export function onHeaders(res: ServerResponse, listener: (this: ServerResponse) => void): void {
13
+ // Loosely typed, to call it with the (statusCode, reason?) signature, bypassing overloads
14
+ const prevWriteHead = res.writeHead as (statusCode: number, reason?: string) => ServerResponse
15
+ let fired = false
16
+
17
+ res.writeHead = function writeHead(
18
+ this: ServerResponse,
19
+ statusCode: number,
20
+ reasonOrHeaders?: string | Headers,
21
+ headers?: Headers,
22
+ ) {
23
+ if (fired) return prevWriteHead.apply(this, arguments as any)
24
+ fired = true
25
+
26
+ const reason = typeof reasonOrHeaders === 'string' ? reasonOrHeaders : undefined
27
+ headers = reason === undefined ? (reasonOrHeaders as Headers | undefined) : headers
28
+
29
+ this.statusCode = statusCode
30
+ if (headers) setHeaders(this, headers)
31
+ listener.call(this)
32
+
33
+ // Reason phrase is dropped if the listener changed the statusCode
34
+ return reason !== undefined && this.statusCode === statusCode
35
+ ? prevWriteHead.call(this, statusCode, reason)
36
+ : prevWriteHead.call(this, this.statusCode)
37
+ }
38
+ }
39
+
40
+ type Headers = OutgoingHttpHeaders | OutgoingHttpHeader[]
41
+
42
+ function setHeaders(res: ServerResponse, headers: Headers): void {
43
+ if (!Array.isArray(headers)) {
44
+ for (const k of Object.keys(headers)) {
45
+ if (k) res.setHeader(k, headers[k]!)
46
+ }
47
+ return
48
+ }
49
+
50
+ if (Array.isArray(headers[0])) {
51
+ // 2D: [[k, v], [k, v]]
52
+ for (const [k, v] of headers as unknown as [string, OutgoingHttpHeader][]) {
53
+ if (k) res.setHeader(k, v)
54
+ }
55
+ return
56
+ }
57
+
58
+ // 1D (raw): [k, v, k, v], keys may repeat
59
+ if (headers.length % 2) throw new TypeError('headers array is malformed')
60
+ for (let i = 0; i < headers.length; i += 2) {
61
+ res.removeHeader(headers[i] as string)
62
+ }
63
+ for (let i = 0; i < headers.length; i += 2) {
64
+ if (headers[i]) res.appendHeader(headers[i] as string, headers[i + 1] as string | string[])
65
+ }
66
+ }
@@ -11,11 +11,11 @@
11
11
  */
12
12
 
13
13
  import zlib from 'node:zlib'
14
- import compressible from 'compressible'
15
14
  // @ts-expect-error no types
16
15
  import Negotiator from 'negotiator'
17
- import onHeaders from 'on-headers'
18
- import vary from 'vary'
16
+ import { compressible } from '../compressible.js'
17
+ import { onHeaders } from '../onHeaders.js'
18
+ import { vary } from '../vary.js'
19
19
  import type { BackendRequest, BackendRequestHandler, BackendResponse } from './server.model.js'
20
20
 
21
21
  export interface CompressionOptions {
package/src/vary.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { ServerResponse } from 'node:http'
2
+
3
+ /**
4
+ * Adds the field (header name) to the response's Vary header, unless it's already there
5
+ * (case-insensitive). Vary: * stays as is, and adding '*' replaces the whole header with '*'.
6
+ *
7
+ * Vendored (and modernized) from the `vary` package.
8
+ */
9
+ export function vary(res: ServerResponse, field: string): void {
10
+ const header = res.getHeader('Vary')
11
+ const value = Array.isArray(header) ? header.join(', ') : String(header ?? '')
12
+ if (!value) {
13
+ res.setHeader('Vary', field)
14
+ return
15
+ }
16
+
17
+ const fields = new Set(
18
+ value
19
+ .toLowerCase()
20
+ .split(',')
21
+ .map(f => f.trim()),
22
+ )
23
+
24
+ if (field === '*' || fields.has('*')) {
25
+ if (value !== '*') res.setHeader('Vary', '*')
26
+ } else if (!fields.has(field.toLowerCase())) {
27
+ res.setHeader('Vary', `${value}, ${field}`)
28
+ }
29
+ }