@dbx-tools/shared-core 0.3.29 → 0.3.30

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 CHANGED
@@ -15,14 +15,14 @@
15
15
  "consola": "^3.4.2"
16
16
  },
17
17
  "dependencies": {
18
- "zod": "^4.3.6"
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.29",
25
+ "version": "0.3.30",
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
- * Built on `globalThis.crypto.randomUUID()` so the same function works in
26
- * Node (>= 19) and modern browsers without a polyfill.
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 = globalThis.crypto.randomUUID();
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
+ });