hanzio 3.1.1 → 3.3.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.
- package/README.md +8 -1
- package/dist/cli/fields.d.ts +16 -6
- package/dist/cli/index.d.ts +57 -4
- package/dist/crypto/node.d.ts +17 -0
- package/dist/crypto/sealer.d.ts +34 -2
- package/dist/format/index.d.ts +6 -3
- package/dist/{index-nfvq3zax.js → index-818f7jz4.js} +228 -88
- package/dist/index-zz773050.js +135 -0
- package/dist/redis/concurrency-limit.d.ts +17 -7
- package/dist/src/cli/index.js +3 -1
- package/dist/src/crypto/index.js +15 -106
- package/dist/src/crypto/node.js +67 -0
- package/dist/src/format/index.js +4 -3
- package/dist/src/i18n/cli.js +33 -1
- package/dist/src/redis/index.js +5 -1
- package/dist/src/testing/index.js +23 -4
- package/dist/string/index.d.ts +1 -1
- package/dist/testing/module-mocker.d.ts +9 -9
- package/dist/testing/prisma.d.ts +5 -1
- package/dist/typedswitch/index.d.ts +26 -1
- package/package.json +8 -2
- package/src/cli/README.md +63 -6
- package/src/cli/fields.ts +95 -38
- package/src/cli/index.ts +302 -91
- package/src/crypto/README.md +21 -7
- package/src/crypto/node.ts +90 -0
- package/src/crypto/sealer.ts +120 -62
- package/src/format/README.md +7 -0
- package/src/format/index.ts +11 -5
- package/src/i18n/README.md +3 -1
- package/src/i18n/cli.ts +40 -0
- package/src/redis/README.md +9 -7
- package/src/redis/concurrency-limit.ts +26 -8
- package/src/string/index.ts +1 -1
- package/src/testing/README.md +12 -3
- package/src/testing/module-mocker.ts +40 -10
- package/src/testing/prisma.ts +6 -1
- package/src/typedswitch/README.md +20 -9
- package/src/typedswitch/index.ts +84 -8
|
@@ -8,10 +8,19 @@ export type RedisConcurrencyLimitOptions = {
|
|
|
8
8
|
maxStartsPerSecond?: number;
|
|
9
9
|
failOpenAfterMs?: number;
|
|
10
10
|
commandTimeoutMs?: number;
|
|
11
|
-
/** Receives throttled warnings
|
|
12
|
-
logger?:
|
|
11
|
+
/** Receives throttled warnings with `{ key, error }`. Defaults to `console`. */
|
|
12
|
+
logger?: {
|
|
13
|
+
warn(message: string, attributes: Record<string, unknown>): void;
|
|
14
|
+
};
|
|
15
|
+
/** The label `onWait` and `onQueueDepth` report under. Default: `key`. */
|
|
16
|
+
metricLabel?: string;
|
|
13
17
|
/** Called when a caller starts queueing; the returned function runs once it stops. */
|
|
14
|
-
onWait?: (stage: ConcurrencyLimitStage) => (() => void) | void;
|
|
18
|
+
onWait?: (stage: ConcurrencyLimitStage, metricLabel: string) => (() => void) | void;
|
|
19
|
+
/**
|
|
20
|
+
* Called once with `queueDepth`, to register a gauge; the returned function
|
|
21
|
+
* runs on `dispose()`.
|
|
22
|
+
*/
|
|
23
|
+
onQueueDepth?: (queueDepth: () => number, metricLabel: string) => (() => void) | void;
|
|
15
24
|
};
|
|
16
25
|
export type RedisConcurrencyLimit = {
|
|
17
26
|
<T>(run: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
@@ -46,10 +55,11 @@ export type RedisConcurrencyLimit = {
|
|
|
46
55
|
* That verdict is shared per limiter: once tripped, later calls degrade
|
|
47
56
|
* immediately while a background probe watches for Redis to come back.
|
|
48
57
|
*
|
|
49
|
-
* `onWait` and `
|
|
50
|
-
* customer host should
|
|
51
|
-
* or every key ever touched leaves a permanent
|
|
52
|
-
* such a limiter is discarded
|
|
58
|
+
* `onWait` and `onQueueDepth` feed metrics under `metricLabel`. A limiter
|
|
59
|
+
* minted per tenant or per customer host should share one pool label rather
|
|
60
|
+
* than default to its key, or every key ever touched leaves a permanent
|
|
61
|
+
* series. Call `dispose()` when such a limiter is discarded; it also runs the
|
|
62
|
+
* unregister function `onQueueDepth` returned.
|
|
53
63
|
*
|
|
54
64
|
* @example
|
|
55
65
|
* const withLimit = createRedisConcurrencyLimit({
|
package/dist/src/cli/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CliError2,
|
|
3
|
+
CliUsageError2,
|
|
3
4
|
defineCommand2,
|
|
4
5
|
defineCli2
|
|
5
|
-
} from "../../index-
|
|
6
|
+
} from "../../index-818f7jz4.js";
|
|
6
7
|
export {
|
|
7
8
|
CliError2 as CliError,
|
|
9
|
+
CliUsageError2 as CliUsageError,
|
|
8
10
|
defineCli2 as defineCli,
|
|
9
11
|
defineCommand2 as defineCommand
|
|
10
12
|
};
|
package/dist/src/crypto/index.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SealerError2,
|
|
3
|
+
sealerKeyInputs2,
|
|
4
|
+
formatEnvelope2,
|
|
5
|
+
parseEnvelope2,
|
|
6
|
+
jsonToSeal2,
|
|
7
|
+
createSealer2
|
|
8
|
+
} from "../../index-zz773050.js";
|
|
1
9
|
import {
|
|
2
10
|
utf8Encode2,
|
|
3
11
|
utf8Decode2,
|
|
@@ -7,117 +15,14 @@ import {
|
|
|
7
15
|
decodeBase64Url2,
|
|
8
16
|
bytesToHex2,
|
|
9
17
|
hexToBytes2,
|
|
10
|
-
toBytes,
|
|
11
18
|
safeEqual2,
|
|
12
19
|
randomBytes2,
|
|
13
20
|
randomToken2,
|
|
14
21
|
sha2562,
|
|
15
22
|
sha256Hex2,
|
|
16
23
|
hmacSign2,
|
|
17
|
-
verifyHmacSignature2
|
|
18
|
-
getSubtleCrypto
|
|
24
|
+
verifyHmacSignature2
|
|
19
25
|
} from "../../index-rp5pqghz.js";
|
|
20
|
-
// src/crypto/sealer.ts
|
|
21
|
-
var ERROR_MESSAGES = {
|
|
22
|
-
malformed: "Malformed sealed value",
|
|
23
|
-
unsupported_version: "Unsupported sealed value version",
|
|
24
|
-
decrypt_failed: "Could not open sealed value: wrong key or tampered data"
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
class SealerError extends Error {
|
|
28
|
-
code;
|
|
29
|
-
constructor(code) {
|
|
30
|
-
super(ERROR_MESSAGES[code]);
|
|
31
|
-
this.name = "SealerError";
|
|
32
|
-
this.code = code;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
var PREFIX = "enc:v1:";
|
|
36
|
-
var IV_BYTES = 12;
|
|
37
|
-
var TAG_BYTES = 16;
|
|
38
|
-
var DEFAULT_INFO = "hanzio.sealer.aes-256-gcm.v1";
|
|
39
|
-
function createSealer(options) {
|
|
40
|
-
const { secret, salt = "", info = DEFAULT_INFO } = options;
|
|
41
|
-
const secretBytes = toBytes(secret);
|
|
42
|
-
if (secretBytes.length === 0) {
|
|
43
|
-
throw new TypeError("Sealer secret must not be empty");
|
|
44
|
-
}
|
|
45
|
-
let keyPromise;
|
|
46
|
-
const getKey = () => {
|
|
47
|
-
if (!keyPromise) {
|
|
48
|
-
keyPromise = deriveKey(secretBytes, salt, info);
|
|
49
|
-
keyPromise.catch(() => {
|
|
50
|
-
keyPromise = undefined;
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
return keyPromise;
|
|
54
|
-
};
|
|
55
|
-
const seal = async (plaintext) => {
|
|
56
|
-
const iv = randomBytes2(IV_BYTES);
|
|
57
|
-
const encrypted = new Uint8Array(await getSubtleCrypto().encrypt({ name: "AES-GCM", iv }, await getKey(), utf8Encode2(plaintext)));
|
|
58
|
-
const tagStart = encrypted.length - TAG_BYTES;
|
|
59
|
-
return `${PREFIX}${encodeBase64Url2(iv)}.${encodeBase64Url2(encrypted.subarray(tagStart))}.${encodeBase64Url2(encrypted.subarray(0, tagStart))}`;
|
|
60
|
-
};
|
|
61
|
-
const open = async (sealed) => {
|
|
62
|
-
if (typeof sealed !== "string" || !sealed.startsWith(PREFIX)) {
|
|
63
|
-
const isOtherVersion = typeof sealed === "string" && /^enc:v\d+:/.test(sealed);
|
|
64
|
-
throw new SealerError(isOtherVersion ? "unsupported_version" : "malformed");
|
|
65
|
-
}
|
|
66
|
-
const parts = sealed.slice(PREFIX.length).split(".");
|
|
67
|
-
let iv;
|
|
68
|
-
let tag;
|
|
69
|
-
let ciphertext;
|
|
70
|
-
try {
|
|
71
|
-
if (parts.length !== 3)
|
|
72
|
-
throw new Error;
|
|
73
|
-
iv = decodeBase64Url2(parts[0]);
|
|
74
|
-
tag = decodeBase64Url2(parts[1]);
|
|
75
|
-
ciphertext = decodeBase64Url2(parts[2]);
|
|
76
|
-
} catch (_error) {
|
|
77
|
-
throw new SealerError("malformed");
|
|
78
|
-
}
|
|
79
|
-
if (iv.length !== IV_BYTES || tag.length !== TAG_BYTES) {
|
|
80
|
-
throw new SealerError("malformed");
|
|
81
|
-
}
|
|
82
|
-
const encrypted = new Uint8Array(ciphertext.length + TAG_BYTES);
|
|
83
|
-
encrypted.set(ciphertext);
|
|
84
|
-
encrypted.set(tag, ciphertext.length);
|
|
85
|
-
const key = await getKey();
|
|
86
|
-
let plaintext;
|
|
87
|
-
try {
|
|
88
|
-
plaintext = await getSubtleCrypto().decrypt({ name: "AES-GCM", iv }, key, encrypted);
|
|
89
|
-
} catch (_error) {
|
|
90
|
-
throw new SealerError("decrypt_failed");
|
|
91
|
-
}
|
|
92
|
-
return utf8Decode2(plaintext);
|
|
93
|
-
};
|
|
94
|
-
return {
|
|
95
|
-
seal,
|
|
96
|
-
open,
|
|
97
|
-
async sealJson(value) {
|
|
98
|
-
const json = JSON.stringify(value);
|
|
99
|
-
if (json === undefined) {
|
|
100
|
-
throw new TypeError("sealJson value is not JSON-serializable");
|
|
101
|
-
}
|
|
102
|
-
return seal(json);
|
|
103
|
-
},
|
|
104
|
-
async openJson(sealed) {
|
|
105
|
-
return JSON.parse(await open(sealed));
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
async function deriveKey(secret, salt, info) {
|
|
110
|
-
const subtle = getSubtleCrypto();
|
|
111
|
-
const baseKey = await subtle.importKey("raw", secret, "HKDF", false, [
|
|
112
|
-
"deriveKey"
|
|
113
|
-
]);
|
|
114
|
-
return subtle.deriveKey({
|
|
115
|
-
name: "HKDF",
|
|
116
|
-
hash: "SHA-256",
|
|
117
|
-
salt: utf8Encode2(salt),
|
|
118
|
-
info: utf8Encode2(info)
|
|
119
|
-
}, baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
120
|
-
}
|
|
121
26
|
// src/crypto/webhook.ts
|
|
122
27
|
var TIMESTAMP = /^\d+$/;
|
|
123
28
|
async function verifySvixSignature(options) {
|
|
@@ -174,18 +79,22 @@ function readHeader(headers, name) {
|
|
|
174
79
|
return;
|
|
175
80
|
}
|
|
176
81
|
export {
|
|
177
|
-
SealerError,
|
|
82
|
+
SealerError2 as SealerError,
|
|
178
83
|
bytesToHex2 as bytesToHex,
|
|
179
|
-
createSealer,
|
|
84
|
+
createSealer2 as createSealer,
|
|
180
85
|
decodeBase642 as decodeBase64,
|
|
181
86
|
decodeBase64Url2 as decodeBase64Url,
|
|
182
87
|
encodeBase642 as encodeBase64,
|
|
183
88
|
encodeBase64Url2 as encodeBase64Url,
|
|
89
|
+
formatEnvelope2 as formatEnvelope,
|
|
184
90
|
hexToBytes2 as hexToBytes,
|
|
185
91
|
hmacSign2 as hmacSign,
|
|
92
|
+
jsonToSeal2 as jsonToSeal,
|
|
93
|
+
parseEnvelope2 as parseEnvelope,
|
|
186
94
|
randomBytes2 as randomBytes,
|
|
187
95
|
randomToken2 as randomToken,
|
|
188
96
|
safeEqual2 as safeEqual,
|
|
97
|
+
sealerKeyInputs2 as sealerKeyInputs,
|
|
189
98
|
sha2562 as sha256,
|
|
190
99
|
sha256Hex2 as sha256Hex,
|
|
191
100
|
utf8Decode2 as utf8Decode,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SealerError2,
|
|
3
|
+
sealerKeyInputs2,
|
|
4
|
+
formatEnvelope2,
|
|
5
|
+
parseEnvelope2,
|
|
6
|
+
jsonToSeal2
|
|
7
|
+
} from "../../index-zz773050.js";
|
|
8
|
+
|
|
9
|
+
// src/crypto/node.ts
|
|
10
|
+
import {
|
|
11
|
+
createCipheriv,
|
|
12
|
+
createDecipheriv,
|
|
13
|
+
hkdfSync,
|
|
14
|
+
randomBytes
|
|
15
|
+
} from "node:crypto";
|
|
16
|
+
var ALGORITHM = "aes-256-gcm";
|
|
17
|
+
function createSealerSync(options) {
|
|
18
|
+
const { secret, contexts } = sealerKeyInputs2(options);
|
|
19
|
+
const keys = [];
|
|
20
|
+
const getKey = (index) => {
|
|
21
|
+
let key = keys[index];
|
|
22
|
+
if (!key) {
|
|
23
|
+
const { salt, info } = contexts[index];
|
|
24
|
+
key = Buffer.from(hkdfSync("sha256", secret, salt, info, 32));
|
|
25
|
+
keys[index] = key;
|
|
26
|
+
}
|
|
27
|
+
return key;
|
|
28
|
+
};
|
|
29
|
+
const seal = (plaintext) => {
|
|
30
|
+
const iv = randomBytes(12);
|
|
31
|
+
const cipher = createCipheriv(ALGORITHM, getKey(0), iv);
|
|
32
|
+
const ciphertext = Buffer.concat([
|
|
33
|
+
cipher.update(plaintext, "utf8"),
|
|
34
|
+
cipher.final()
|
|
35
|
+
]);
|
|
36
|
+
return formatEnvelope2(iv, cipher.getAuthTag(), ciphertext);
|
|
37
|
+
};
|
|
38
|
+
const openEnvelope = ({ iv, tag, ciphertext }) => {
|
|
39
|
+
for (let index = 0;index < contexts.length; index++) {
|
|
40
|
+
try {
|
|
41
|
+
const decipher = createDecipheriv(ALGORITHM, getKey(index), iv);
|
|
42
|
+
decipher.setAuthTag(tag);
|
|
43
|
+
return Buffer.concat([
|
|
44
|
+
decipher.update(ciphertext),
|
|
45
|
+
decipher.final()
|
|
46
|
+
]).toString("utf8");
|
|
47
|
+
} catch (_error) {}
|
|
48
|
+
}
|
|
49
|
+
throw new SealerError2("decrypt_failed");
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
seal,
|
|
53
|
+
open(sealed) {
|
|
54
|
+
const envelope = parseEnvelope2(sealed, options.passthroughUnsealed);
|
|
55
|
+
return envelope ? openEnvelope(envelope) : sealed;
|
|
56
|
+
},
|
|
57
|
+
sealJson: (value) => seal(jsonToSeal2(value)),
|
|
58
|
+
openJson(sealed) {
|
|
59
|
+
const envelope = parseEnvelope2(sealed, options.passthroughUnsealed);
|
|
60
|
+
return envelope ? JSON.parse(openEnvelope(envelope)) : sealed;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
SealerError2 as SealerError,
|
|
66
|
+
createSealerSync
|
|
67
|
+
};
|
package/dist/src/format/index.js
CHANGED
|
@@ -193,8 +193,9 @@ var DATE_PRESETS = {
|
|
|
193
193
|
"dddd, MMMM D": { weekday: "long", day: "numeric", month: "long" },
|
|
194
194
|
"MMMM YYYY": { month: "long", year: "numeric" }
|
|
195
195
|
};
|
|
196
|
-
function formatDate(date,
|
|
197
|
-
|
|
196
|
+
function formatDate(date, format, locale) {
|
|
197
|
+
const options = typeof format === "string" ? DATE_PRESETS[format] : format;
|
|
198
|
+
return getDateTimeFormat(locale, options).format(toTime(date));
|
|
198
199
|
}
|
|
199
200
|
function formatDateTime(date, locale) {
|
|
200
201
|
return getDateTimeFormat(locale, {
|
|
@@ -249,7 +250,7 @@ function pluralize(count, forms, locale) {
|
|
|
249
250
|
}
|
|
250
251
|
function withLocale(getLocale) {
|
|
251
252
|
return {
|
|
252
|
-
formatDate: (date,
|
|
253
|
+
formatDate: (date, format) => formatDate(date, format, getLocale()),
|
|
253
254
|
formatDateTime: (date) => formatDateTime(date, getLocale()),
|
|
254
255
|
formatCompactTime: (date) => formatCompactTime(date, getLocale()),
|
|
255
256
|
formatDayDivider: (date) => formatDayDivider(date, getLocale()),
|
package/dist/src/i18n/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
CliError2,
|
|
4
4
|
defineCommand2,
|
|
5
5
|
defineCli2
|
|
6
|
-
} from "../../index-
|
|
6
|
+
} from "../../index-818f7jz4.js";
|
|
7
7
|
import"../../index-najbyzpr.js";
|
|
8
8
|
import {
|
|
9
9
|
validateCatalogs2,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
// src/i18n/cli.ts
|
|
14
14
|
import { existsSync } from "node:fs";
|
|
15
|
+
import * as nodeModule from "node:module";
|
|
15
16
|
import { dirname, resolve } from "node:path";
|
|
16
17
|
import { pathToFileURL } from "node:url";
|
|
17
18
|
import { z } from "zod";
|
|
@@ -21,10 +22,41 @@ var CONFIG_NAMES = [
|
|
|
21
22
|
"i18n.config.js",
|
|
22
23
|
"i18n.config.mjs"
|
|
23
24
|
];
|
|
25
|
+
var BUNDLER_SUFFIXES = [
|
|
26
|
+
".ts",
|
|
27
|
+
".mts",
|
|
28
|
+
".js",
|
|
29
|
+
".mjs",
|
|
30
|
+
"/index.ts",
|
|
31
|
+
"/index.mts",
|
|
32
|
+
"/index.js",
|
|
33
|
+
"/index.mjs"
|
|
34
|
+
];
|
|
35
|
+
function resolveLikeBundlers() {
|
|
36
|
+
nodeModule.registerHooks?.({
|
|
37
|
+
resolve(specifier, context, nextResolve) {
|
|
38
|
+
try {
|
|
39
|
+
return nextResolve(specifier, context);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const code = error.code;
|
|
42
|
+
const retry = specifier.startsWith(".") && (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_UNSUPPORTED_DIR_IMPORT");
|
|
43
|
+
if (!retry)
|
|
44
|
+
throw error;
|
|
45
|
+
for (const suffix of BUNDLER_SUFFIXES) {
|
|
46
|
+
try {
|
|
47
|
+
return nextResolve(specifier + suffix, context);
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
24
55
|
async function loadConfig(file) {
|
|
25
56
|
const path = file ? resolve(file) : CONFIG_NAMES.map((name) => resolve(name)).find((path) => existsSync(path));
|
|
26
57
|
if (!path || !existsSync(path))
|
|
27
58
|
throw new CliError2(`No i18n config found: pass --config or add ${CONFIG_NAMES[0]}.`);
|
|
59
|
+
resolveLikeBundlers();
|
|
28
60
|
const loaded = await import(pathToFileURL(path).href);
|
|
29
61
|
if (!loaded.default?.catalogs)
|
|
30
62
|
throw new CliError2(`${path} must default-export an i18n config`);
|
package/dist/src/redis/index.js
CHANGED
|
@@ -315,6 +315,7 @@ function createRedisConcurrencyLimit(input) {
|
|
|
315
315
|
if (!Number.isInteger(input.max) || input.max < 1)
|
|
316
316
|
throw new Error(`max must be a positive integer, got ${input.max}`);
|
|
317
317
|
const logger = input.logger ?? console;
|
|
318
|
+
const metricLabel = input.metricLabel ?? input.key;
|
|
318
319
|
const commandTimeoutMs = input.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
319
320
|
const send = withCommandTimeout(input.send, commandTimeoutMs);
|
|
320
321
|
const ttlMs = String(input.slotTtlMs);
|
|
@@ -435,7 +436,7 @@ function createRedisConcurrencyLimit(input) {
|
|
|
435
436
|
return {
|
|
436
437
|
depth: () => queue.length,
|
|
437
438
|
acquire(token) {
|
|
438
|
-
const stopWait = input.onWait?.(stage);
|
|
439
|
+
const stopWait = input.onWait?.(stage, metricLabel);
|
|
439
440
|
const granted = new Promise((resolve) => {
|
|
440
441
|
queue.push({ token, settle: resolve });
|
|
441
442
|
});
|
|
@@ -451,6 +452,7 @@ function createRedisConcurrencyLimit(input) {
|
|
|
451
452
|
const slotPump = createPump("slot", input.max, async (tokens) => parseGrantReply(await acquireScript([inflightKey, cooldownKey], [String(input.max), ttlMs, ...tokens])));
|
|
452
453
|
const startPump = maxStartsPerSecond === undefined ? null : createPump("start", maxStartsPerSecond, async (tokens) => parseGrantReply(await startScript([startsKey, cooldownKey], [String(maxStartsPerSecond), String(START_WINDOW_MS), ...tokens])));
|
|
453
454
|
const queueDepth = () => slotPump.depth() + (startPump?.depth() ?? 0);
|
|
455
|
+
let unregisterQueueDepth = input.onQueueDepth?.(queueDepth, metricLabel);
|
|
454
456
|
const heldTokens = new Set;
|
|
455
457
|
let heartbeatTimer = null;
|
|
456
458
|
function scheduleHeartbeat() {
|
|
@@ -555,6 +557,8 @@ function createRedisConcurrencyLimit(input) {
|
|
|
555
557
|
}
|
|
556
558
|
}
|
|
557
559
|
function dispose() {
|
|
560
|
+
unregisterQueueDepth?.();
|
|
561
|
+
unregisterQueueDepth = undefined;
|
|
558
562
|
if (probeTimer) {
|
|
559
563
|
clearTimeout(probeTimer);
|
|
560
564
|
probeTimer = null;
|
|
@@ -205,11 +205,30 @@ function mockCallArg(fn, callIndex = 0, argIndex = 0) {
|
|
|
205
205
|
return fn.mock.calls[callIndex]?.[argIndex];
|
|
206
206
|
}
|
|
207
207
|
// src/testing/module-mocker.ts
|
|
208
|
+
import { isAbsolute } from "node:path";
|
|
209
|
+
function resolveFrom(baseDir) {
|
|
210
|
+
return (specifier) => {
|
|
211
|
+
if (isAbsolute(specifier))
|
|
212
|
+
return specifier;
|
|
213
|
+
if (specifier.startsWith("."))
|
|
214
|
+
return Bun.resolveSync(specifier, baseDir);
|
|
215
|
+
try {
|
|
216
|
+
return Bun.resolveSync(specifier, baseDir);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
try {
|
|
219
|
+
return Bun.resolveSync(`./${specifier}`, baseDir);
|
|
220
|
+
} catch {
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
208
227
|
class ModuleMocker {
|
|
209
|
-
resolve;
|
|
210
228
|
restorers = [];
|
|
211
|
-
|
|
212
|
-
|
|
229
|
+
resolve;
|
|
230
|
+
constructor(baseDirOrResolve = (specifier) => specifier) {
|
|
231
|
+
this.resolve = typeof baseDirOrResolve === "string" ? resolveFrom(baseDirOrResolve) : baseDirOrResolve;
|
|
213
232
|
}
|
|
214
233
|
async mock(specifier, renderMocks) {
|
|
215
234
|
const { mock } = await import("bun:test");
|
|
@@ -222,7 +241,7 @@ class ModuleMocker {
|
|
|
222
241
|
});
|
|
223
242
|
}
|
|
224
243
|
clear() {
|
|
225
|
-
for (const restore of this.restorers)
|
|
244
|
+
for (const restore of this.restorers.toReversed())
|
|
226
245
|
restore();
|
|
227
246
|
this.restorers = [];
|
|
228
247
|
}
|
package/dist/string/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type JsonValue = string | number | boolean | null | JsonObject | JsonArra
|
|
|
3
3
|
export type JsonObject = {
|
|
4
4
|
[key: string]: JsonValue | undefined;
|
|
5
5
|
};
|
|
6
|
-
export type JsonArray =
|
|
6
|
+
export type JsonArray = JsonValue[];
|
|
7
7
|
/**
|
|
8
8
|
* Parses the first number-like token in a string. Only a `-` directly before
|
|
9
9
|
* the digits is a sign, so `'2024-01-05'` gives `2024`. Whitespace and the
|
|
@@ -9,21 +9,21 @@ type ModuleExports = Record<string, unknown>;
|
|
|
9
9
|
* keep working, and `clear()` re-registers the originals. Call it in
|
|
10
10
|
* `afterEach` or `afterAll`.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Without an argument, pass absolute paths. Pass your source root to resolve
|
|
13
|
+
* against it: package names from there (so mocks hit the same copy your code
|
|
14
|
+
* imports), and `./lib/x` or `lib/x` relative to it. A function maps your own
|
|
15
|
+
* shorthand, such as an `@/` alias, to absolute paths instead.
|
|
15
16
|
*
|
|
16
17
|
* @example
|
|
17
|
-
* const moduleMocker = new ModuleMocker(
|
|
18
|
-
*
|
|
19
|
-
* )
|
|
20
|
-
* await moduleMocker.mock('@/lib/posthog', () => ({ captureException: mock() }))
|
|
18
|
+
* const moduleMocker = new ModuleMocker(srcDir)
|
|
19
|
+
* await moduleMocker.mock('lib/posthog.ts', () => ({ captureException: mock() }))
|
|
20
|
+
* await moduleMocker.mock('ai', () => ({ generateText: mock() }))
|
|
21
21
|
* afterAll(() => moduleMocker.clear())
|
|
22
22
|
*/
|
|
23
23
|
export declare class ModuleMocker {
|
|
24
|
-
private readonly resolve;
|
|
25
24
|
private restorers;
|
|
26
|
-
|
|
25
|
+
private readonly resolve;
|
|
26
|
+
constructor(baseDirOrResolve?: string | ((specifier: string) => string));
|
|
27
27
|
mock(specifier: string, renderMocks: () => ModuleExports): Promise<void>;
|
|
28
28
|
clear(): void;
|
|
29
29
|
}
|
package/dist/testing/prisma.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Mock, mock } from 'bun:test';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* `never[]` parameters let `mockImplementation` take a callback that types its
|
|
4
|
+
* arguments; the plain call signature keeps the mock callable with any.
|
|
5
|
+
*/
|
|
6
|
+
export type PrismaMethodMock = Mock<(...args: never[]) => Promise<unknown>> & ((...args: unknown[]) => Promise<unknown>);
|
|
3
7
|
/**
|
|
4
8
|
* A Prisma client where every model delegate and every method is a Bun mock,
|
|
5
9
|
* created on first access, like `jest-mock-extended`'s `mockDeep()`.
|
|
@@ -80,6 +80,23 @@ type RawHandlerReturnType<T> = T extends (...args: any[]) => infer R ? R : never
|
|
|
80
80
|
type RawCasesReturnType<Cases> = {
|
|
81
81
|
[K in keyof Cases]: RawHandlerReturnType<Cases[K]>;
|
|
82
82
|
}[keyof Cases];
|
|
83
|
+
/**
|
|
84
|
+
* `R` is only inferred from the call's contextual type. Without one it stays
|
|
85
|
+
* `unknown`, and the overload returns the handlers' own return types.
|
|
86
|
+
*/
|
|
87
|
+
type Annotated<R, Inferred> = unknown extends R ? Inferred : R;
|
|
88
|
+
/**
|
|
89
|
+
* TypeScript fixes a handler's return type the first time it checks it, and
|
|
90
|
+
* an object-mode call (`value, key, cases`) is first tried against the
|
|
91
|
+
* plain-value overload with a default (`value, cases, defaultCase`). This type
|
|
92
|
+
* types those cases' handlers by the annotation there too, so they keep their
|
|
93
|
+
* literals. No value satisfies it: `default` would have to be `never`.
|
|
94
|
+
*/
|
|
95
|
+
type ObjectCasesContext<R> = {
|
|
96
|
+
[key: string]: (value: never) => R;
|
|
97
|
+
} & {
|
|
98
|
+
default: never;
|
|
99
|
+
};
|
|
83
100
|
/** Only required, defined handlers guarantee that a case is handled. */
|
|
84
101
|
type HandledCaseKeys<Cases> = {
|
|
85
102
|
[K in keyof Cases]-?: {} extends Pick<Cases, K> ? never : undefined extends Cases[K] ? never : K;
|
|
@@ -113,13 +130,21 @@ export type PartialObjectSwitchCases<T, K extends keyof T, R> = T[K] extends Dis
|
|
|
113
130
|
type NoUnknownCases<Cases, Allowed extends string> = {
|
|
114
131
|
[P in keyof Cases as `${P & (string | number)}` extends Allowed ? never : P]: never;
|
|
115
132
|
};
|
|
133
|
+
export declare function typedSwitch<T extends Discriminant, R, Cases extends {
|
|
134
|
+
[V in T as `${V}`]: (value: V) => R;
|
|
135
|
+
} & NoUnknownCases<Cases, `${T}`>>(value: T, cases: Cases): Annotated<R, RawCasesReturnType<Cases>>;
|
|
136
|
+
export declare function typedSwitch<T extends Discriminant, R, Cases extends {
|
|
137
|
+
[V in T as `${V}`]?: (value: V) => R;
|
|
138
|
+
} & NoUnknownCases<Cases, `${T}`>, DefaultCase extends ((value: Exclude<T, ValuesForKeys<T, HandledCaseKeys<Cases>>>) => R) | ObjectCasesContext<R>>(value: T, cases: Cases, defaultCase: DefaultCase): Annotated<R, RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>>;
|
|
139
|
+
export declare function typedSwitch<T extends object, K extends keyof T, R, Cases extends ObjectCases<T, K, R> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>>(value: T, key: K, cases: Cases): Annotated<R, RawCasesReturnType<Cases>>;
|
|
140
|
+
export declare function typedSwitch<T extends object, K extends keyof T, R, Cases extends PartialObjectCases<T, K, R> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>, DefaultCase extends (value: Exclude<T, Record<K, ValuesForKeys<T[K], HandledCaseKeys<Cases>>>>) => R>(value: T, key: K, cases: Cases, defaultCase: DefaultCase): Annotated<R, RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>>;
|
|
116
141
|
export declare function typedSwitch<T extends Discriminant, Cases extends {
|
|
117
142
|
[V in T as `${V}`]: (value: V) => unknown;
|
|
118
143
|
} & NoUnknownCases<Cases, `${T}`>>(value: T, cases: Cases): RawCasesReturnType<Cases>;
|
|
144
|
+
export declare function typedSwitch<T extends object, K extends keyof T, Cases extends ObjectCases<T, K, unknown> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>>(value: T, key: K, cases: Cases): RawCasesReturnType<Cases>;
|
|
119
145
|
export declare function typedSwitch<T extends Discriminant, Cases extends {
|
|
120
146
|
[V in T as `${V}`]?: (value: V) => unknown;
|
|
121
147
|
} & NoUnknownCases<Cases, `${T}`>, DefaultCase extends (value: Exclude<T, ValuesForKeys<T, HandledCaseKeys<Cases>>>) => unknown>(value: T, cases: Cases, defaultCase: DefaultCase): RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>;
|
|
122
|
-
export declare function typedSwitch<T extends object, K extends keyof T, Cases extends ObjectCases<T, K, unknown> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>>(value: T, key: K, cases: Cases): RawCasesReturnType<Cases>;
|
|
123
148
|
export declare function typedSwitch<T extends object, K extends keyof T, Cases extends PartialObjectCases<T, K, unknown> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>, DefaultCase extends (value: Exclude<T, Record<K, ValuesForKeys<T[K], HandledCaseKeys<Cases>>>>) => unknown>(value: T, key: K, cases: Cases, defaultCase: DefaultCase): RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>;
|
|
124
149
|
/**
|
|
125
150
|
* Builds cases that share one handler, for spreading into `typedSwitch` cases.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hanzio",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "A lightweight set of utility functions for TypeScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/src/index.js",
|
|
@@ -107,6 +107,12 @@
|
|
|
107
107
|
"import": "./dist/src/crypto/index.js",
|
|
108
108
|
"default": "./dist/src/crypto/index.js"
|
|
109
109
|
},
|
|
110
|
+
"./crypto/node": {
|
|
111
|
+
"types": "./dist/crypto/node.d.ts",
|
|
112
|
+
"bun": "./src/crypto/node.ts",
|
|
113
|
+
"import": "./dist/src/crypto/node.js",
|
|
114
|
+
"default": "./dist/src/crypto/node.js"
|
|
115
|
+
},
|
|
110
116
|
"./storage": {
|
|
111
117
|
"types": "./dist/storage/index.d.ts",
|
|
112
118
|
"bun": "./src/storage/index.ts",
|
|
@@ -194,7 +200,7 @@
|
|
|
194
200
|
},
|
|
195
201
|
"scripts": {
|
|
196
202
|
"build": "rm -rf dist && bun run build:js && bun run build:types",
|
|
197
|
-
"build:js": "bun build ./src/index.ts ./src/zod/index.ts ./src/api-wrapper/index.ts ./src/api-wrapper/testing.ts ./src/api-wrapper/connection-auth.ts ./src/jwt/index.ts ./src/sitemap/index.ts ./src/p-queue/index.ts ./src/cool-console-log/index.ts ./src/format/index.ts ./src/format/phone.ts ./src/crypto/index.ts ./src/storage/index.ts ./src/i18n/index.ts ./src/i18n/vue.ts ./src/i18n/i18next.ts ./src/i18n/vite.ts ./src/i18n/check.ts ./src/i18n/cli.ts ./src/iso/index.ts ./src/secrets/index.ts ./src/secrets/vite.ts ./src/secrets/worker.ts ./src/redis/index.ts ./src/http/index.ts ./src/cli/index.ts ./src/testing/index.ts --outdir ./dist --target node --packages external --splitting",
|
|
203
|
+
"build:js": "bun build ./src/index.ts ./src/zod/index.ts ./src/api-wrapper/index.ts ./src/api-wrapper/testing.ts ./src/api-wrapper/connection-auth.ts ./src/jwt/index.ts ./src/sitemap/index.ts ./src/p-queue/index.ts ./src/cool-console-log/index.ts ./src/format/index.ts ./src/format/phone.ts ./src/crypto/index.ts ./src/crypto/node.ts ./src/storage/index.ts ./src/i18n/index.ts ./src/i18n/vue.ts ./src/i18n/i18next.ts ./src/i18n/vite.ts ./src/i18n/check.ts ./src/i18n/cli.ts ./src/iso/index.ts ./src/secrets/index.ts ./src/secrets/vite.ts ./src/secrets/worker.ts ./src/redis/index.ts ./src/http/index.ts ./src/cli/index.ts ./src/testing/index.ts --outdir ./dist --target node --packages external --splitting",
|
|
198
204
|
"build:types": "tsgo -p tsconfig.build.json",
|
|
199
205
|
"prepublishOnly": "bun run build",
|
|
200
206
|
"test": "bun test",
|