@dbx-tools/shared-core 0.3.29 → 0.3.31
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.
- package/package.json +2 -2
- package/src/async.ts +28 -0
- package/src/hash.ts +27 -3
- package/test/async.test.ts +44 -0
- package/test/hash.test.ts +55 -0
package/package.json
CHANGED
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
"consola": "^3.4.2"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"zod": "
|
|
18
|
+
"zod": "4.3.6"
|
|
19
19
|
},
|
|
20
20
|
"main": "index.ts",
|
|
21
21
|
"license": "UNLICENSED",
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.31",
|
|
26
26
|
"types": "index.ts",
|
|
27
27
|
"type": "module",
|
|
28
28
|
"exports": {
|
package/src/async.ts
CHANGED
|
@@ -183,6 +183,34 @@ export function tieAbortSignal(child: AbortController, parent?: AbortSignal): vo
|
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Combine several optional cancellation sources into one signal that aborts
|
|
188
|
+
* as soon as any of them does.
|
|
189
|
+
*
|
|
190
|
+
* The usual caller is an operation that has to honor more than one source at
|
|
191
|
+
* once - a caller's own signal (a closed connection, an agent run being
|
|
192
|
+
* cancelled) plus one derived from a timeout - where the awaited I/O accepts
|
|
193
|
+
* only a single signal. Absent sources are ignored, and a lone signal is
|
|
194
|
+
* returned as-is so the common path allocates nothing. Returns `undefined`
|
|
195
|
+
* only when every input is absent, which callers can pass straight through
|
|
196
|
+
* to an optional `signal` parameter.
|
|
197
|
+
*
|
|
198
|
+
* Aborting an input aborts the result (carrying that input's `reason`);
|
|
199
|
+
* nothing propagates back the other way.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* await fetch(url, { signal: combineAbortSignals(req.signal, timeout.signal) });
|
|
203
|
+
*/
|
|
204
|
+
export function combineAbortSignals(
|
|
205
|
+
...signals: (AbortSignal | undefined)[]
|
|
206
|
+
): AbortSignal | undefined {
|
|
207
|
+
const present = signals.filter((signal): signal is AbortSignal => signal !== undefined);
|
|
208
|
+
if (present.length <= 1) return present[0];
|
|
209
|
+
const combined = new AbortController();
|
|
210
|
+
for (const signal of present) tieAbortSignal(combined, signal);
|
|
211
|
+
return combined.signal;
|
|
212
|
+
}
|
|
213
|
+
|
|
186
214
|
/**
|
|
187
215
|
* Promisified `setTimeout` that wakes up early (and rejects with
|
|
188
216
|
* `signal.reason`) when `signal` aborts mid-wait. Short-circuits to a
|
package/src/hash.ts
CHANGED
|
@@ -22,8 +22,13 @@
|
|
|
22
22
|
* id has to be short / typeable and the scope is bounded - cache keys
|
|
23
23
|
* local to a request, slug suffixes. `length <= 0` throws.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* Prefers `crypto.randomUUID()`, which covers Node (>= 19) and a browser on
|
|
26
|
+
* a secure origin. Browsers gate `randomUUID` behind a secure context, so a
|
|
27
|
+
* page served over plain http (a LAN dev host) has `crypto` but not that
|
|
28
|
+
* method; this package is browser-safe and its callers mint ids on the
|
|
29
|
+
* render path, so it degrades instead of throwing: `getRandomValues` when
|
|
30
|
+
* present, else `Math.random`. Every branch returns a well-formed v4 UUID -
|
|
31
|
+
* only the entropy source differs.
|
|
27
32
|
*
|
|
28
33
|
* @example
|
|
29
34
|
* id(); // "123e4567-e89b-12d3-a456-426614174000"
|
|
@@ -33,13 +38,32 @@ export function id(length?: number): string {
|
|
|
33
38
|
if (length !== undefined && length <= 0) {
|
|
34
39
|
throw new Error("Length must be greater than 0");
|
|
35
40
|
}
|
|
36
|
-
const id =
|
|
41
|
+
const id = uuidV4();
|
|
37
42
|
if (length !== undefined) {
|
|
38
43
|
return id.replace(/-/g, "").slice(0, length);
|
|
39
44
|
}
|
|
40
45
|
return id;
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
/** A v4 UUID from the strongest randomness source this runtime offers. */
|
|
49
|
+
function uuidV4(): string {
|
|
50
|
+
const webCrypto = globalThis.crypto as Crypto | undefined;
|
|
51
|
+
if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
|
|
52
|
+
|
|
53
|
+
const bytes = new Uint8Array(16);
|
|
54
|
+
if (typeof webCrypto?.getRandomValues === "function") {
|
|
55
|
+
webCrypto.getRandomValues(bytes);
|
|
56
|
+
} else {
|
|
57
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
58
|
+
}
|
|
59
|
+
// Stamp the version (4) and variant (10xx) fields RFC 4122 requires.
|
|
60
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
|
|
61
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
|
62
|
+
|
|
63
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
64
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
/**
|
|
44
68
|
* Short, deterministic FNV-1a hash over one or more values. Wraps
|
|
45
69
|
* {@link fnvHashWithOptions} with all defaults: 6-char Crockford-style
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { async } from "../index";
|
|
4
|
+
|
|
5
|
+
describe("async.combineAbortSignals", () => {
|
|
6
|
+
it("returns undefined when every source is absent", () => {
|
|
7
|
+
assert.equal(async.combineAbortSignals(), undefined);
|
|
8
|
+
assert.equal(async.combineAbortSignals(undefined, undefined), undefined);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("passes a lone signal through without wrapping it", () => {
|
|
12
|
+
const { signal } = new AbortController();
|
|
13
|
+
assert.equal(async.combineAbortSignals(signal), signal);
|
|
14
|
+
assert.equal(async.combineAbortSignals(undefined, signal, undefined), signal);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("aborts when any source aborts, carrying that source's reason", () => {
|
|
18
|
+
for (const index of [0, 1, 2]) {
|
|
19
|
+
const controllers = [new AbortController(), new AbortController(), new AbortController()];
|
|
20
|
+
const combined = async.combineAbortSignals(...controllers.map((c) => c.signal));
|
|
21
|
+
assert.equal(combined?.aborted, false);
|
|
22
|
+
controllers[index]!.abort(new Error(`source ${index}`));
|
|
23
|
+
assert.equal(combined?.aborted, true);
|
|
24
|
+
assert.equal((combined?.reason as Error).message, `source ${index}`);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("is already aborted when a source aborted before combining", () => {
|
|
29
|
+
const early = new AbortController();
|
|
30
|
+
early.abort(new Error("gone"));
|
|
31
|
+
const combined = async.combineAbortSignals(early.signal, new AbortController().signal);
|
|
32
|
+
assert.equal(combined?.aborted, true);
|
|
33
|
+
assert.equal((combined?.reason as Error).message, "gone");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("does not propagate back to the sources", () => {
|
|
37
|
+
const first = new AbortController();
|
|
38
|
+
const second = new AbortController();
|
|
39
|
+
const combined = async.combineAbortSignals(first.signal, second.signal);
|
|
40
|
+
first.abort();
|
|
41
|
+
assert.equal(combined?.aborted, true);
|
|
42
|
+
assert.equal(second.signal.aborted, false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { afterEach, describe, it } from "node:test";
|
|
3
|
+
import { hash } from "../index";
|
|
4
|
+
|
|
5
|
+
const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
6
|
+
|
|
7
|
+
const original = globalThis.crypto;
|
|
8
|
+
|
|
9
|
+
const withCrypto = (value: unknown, body: () => void) => {
|
|
10
|
+
Object.defineProperty(globalThis, "crypto", { value, configurable: true, writable: true });
|
|
11
|
+
try {
|
|
12
|
+
body();
|
|
13
|
+
} finally {
|
|
14
|
+
Object.defineProperty(globalThis, "crypto", {
|
|
15
|
+
value: original,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("hash.id", () => {
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
assert.equal(globalThis.crypto, original);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("mints a v4 UUID from crypto.randomUUID when available", () => {
|
|
28
|
+
assert.match(hash.id(), UUID_V4);
|
|
29
|
+
assert.notEqual(hash.id(), hash.id());
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns a short hex slice when a length is given", () => {
|
|
33
|
+
assert.match(hash.id(8), /^[0-9a-f]{8}$/);
|
|
34
|
+
assert.equal(hash.id(1).length, 1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("rejects a non-positive length", () => {
|
|
38
|
+
assert.throws(() => hash.id(0), /greater than 0/);
|
|
39
|
+
assert.throws(() => hash.id(-1), /greater than 0/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("falls back to getRandomValues where randomUUID is absent (plain-http browser)", () => {
|
|
43
|
+
withCrypto({ getRandomValues: original.getRandomValues.bind(original) }, () => {
|
|
44
|
+
assert.match(hash.id(), UUID_V4);
|
|
45
|
+
assert.notEqual(hash.id(), hash.id());
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("falls back to Math.random where crypto is absent entirely", () => {
|
|
50
|
+
withCrypto(undefined, () => {
|
|
51
|
+
assert.match(hash.id(), UUID_V4);
|
|
52
|
+
assert.match(hash.id(12), /^[0-9a-f]{12}$/);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|