hanzio 2.2.0 → 3.1.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.
Files changed (116) hide show
  1. package/README.md +133 -7
  2. package/dist/api-wrapper/connection-auth.d.ts +61 -0
  3. package/dist/cli/fields.d.ts +30 -0
  4. package/dist/cli/index.d.ts +122 -0
  5. package/dist/format/index.d.ts +91 -0
  6. package/dist/format/phone.d.ts +19 -0
  7. package/dist/http/index.d.ts +14 -0
  8. package/dist/i18n/catalog.d.ts +47 -0
  9. package/dist/i18n/check.d.ts +45 -0
  10. package/dist/i18n/cli.d.ts +2 -0
  11. package/dist/i18n/content-rules.d.ts +55 -0
  12. package/dist/i18n/i18next.d.ts +39 -0
  13. package/dist/i18n/index.d.ts +16 -1
  14. package/dist/i18n/locales.d.ts +62 -0
  15. package/dist/i18n/vite.d.ts +16 -0
  16. package/dist/i18n/vue.d.ts +93 -0
  17. package/dist/{index-kj1phgrx.js → index-29mmh343.js} +24 -1
  18. package/dist/{index-zbj9s3s3.js → index-2kz392vw.js} +1 -1
  19. package/dist/index-5n2e326q.js +409 -0
  20. package/dist/{index-hy7kbb99.js → index-5tzqfmnj.js} +1 -1
  21. package/dist/{index-0zvtcbzy.js → index-6f2w90pp.js} +69 -1
  22. package/dist/index-6ragggbs.js +473 -0
  23. package/dist/index-mq2qgc20.js +116 -0
  24. package/dist/index-najbyzpr.js +99 -0
  25. package/dist/index-ww09bm49.js +250 -0
  26. package/dist/iso/countries.d.ts +262 -0
  27. package/dist/iso/currencies.d.ts +165 -0
  28. package/dist/iso/index.d.ts +4 -0
  29. package/dist/iso/languages.d.ts +195 -0
  30. package/dist/iso/phone-language.d.ts +5 -0
  31. package/dist/promise/index.d.ts +31 -0
  32. package/dist/redis/client.d.ts +49 -0
  33. package/dist/redis/concurrency-limit.d.ts +63 -0
  34. package/dist/redis/index.d.ts +3 -0
  35. package/dist/redis/lua-script.d.ts +13 -0
  36. package/dist/secrets/infisical-identity-file.d.ts +12 -0
  37. package/dist/src/api-wrapper/connection-auth.js +82 -0
  38. package/dist/src/api-wrapper/index.js +1 -1
  39. package/dist/src/cli/index.js +10 -0
  40. package/dist/src/format/index.js +79 -2
  41. package/dist/src/format/phone.js +21 -0
  42. package/dist/src/http/index.js +69 -0
  43. package/dist/src/i18n/check.js +15 -0
  44. package/dist/src/i18n/cli.js +71 -0
  45. package/dist/src/i18n/i18next.js +48 -0
  46. package/dist/src/i18n/index.js +16 -34
  47. package/dist/src/i18n/vite.js +25 -0
  48. package/dist/src/i18n/vue.js +51 -0
  49. package/dist/src/index.js +22 -6
  50. package/dist/src/iso/index.js +655 -0
  51. package/dist/src/redis/index.js +577 -0
  52. package/dist/src/secrets/index.js +78 -35
  53. package/dist/src/sitemap/index.js +1 -1
  54. package/dist/src/storage/index.js +8 -117
  55. package/dist/src/testing/index.js +348 -0
  56. package/dist/src/zod/index.js +20 -253
  57. package/dist/string/index.d.ts +21 -0
  58. package/dist/testing/connection-auth-contract.d.ts +43 -0
  59. package/dist/testing/helpers.d.ts +26 -0
  60. package/dist/testing/index.d.ts +5 -0
  61. package/dist/testing/module-mocker.d.ts +30 -0
  62. package/dist/testing/prisma.d.ts +30 -0
  63. package/dist/testing/window.d.ts +18 -0
  64. package/dist/typedswitch/index.d.ts +55 -34
  65. package/dist/zod/index.d.ts +10 -0
  66. package/dist/zod/introspect.d.ts +2 -1
  67. package/package.json +97 -2
  68. package/src/api-wrapper/README.md +57 -2
  69. package/src/api-wrapper/SKILL.md +256 -0
  70. package/src/api-wrapper/connection-auth.ts +142 -0
  71. package/src/cli/README.md +220 -0
  72. package/src/cli/fields.ts +213 -0
  73. package/src/cli/index.ts +532 -0
  74. package/src/format/README.md +82 -2
  75. package/src/format/index.ts +148 -0
  76. package/src/format/phone.ts +43 -0
  77. package/src/http/README.md +25 -0
  78. package/src/http/index.ts +94 -0
  79. package/src/i18n/README.md +206 -3
  80. package/src/i18n/catalog.ts +139 -0
  81. package/src/i18n/check.ts +376 -0
  82. package/src/i18n/cli.ts +90 -0
  83. package/src/i18n/content-rules.ts +179 -0
  84. package/src/i18n/i18next.ts +89 -0
  85. package/src/i18n/index.ts +34 -0
  86. package/src/i18n/locales.ts +135 -0
  87. package/src/i18n/vite.ts +40 -0
  88. package/src/i18n/vue.ts +82 -0
  89. package/src/index.ts +1 -1
  90. package/src/iso/README.md +44 -0
  91. package/src/iso/countries.ts +272 -0
  92. package/src/iso/currencies.ts +172 -0
  93. package/src/iso/index.ts +8 -0
  94. package/src/iso/languages.ts +208 -0
  95. package/src/iso/phone-language.ts +46 -0
  96. package/src/promise/index.ts +100 -0
  97. package/src/redis/README.md +112 -0
  98. package/src/redis/client.ts +239 -0
  99. package/src/redis/concurrency-limit.ts +591 -0
  100. package/src/redis/index.ts +15 -0
  101. package/src/redis/lua-script.ts +43 -0
  102. package/src/secrets/README.md +5 -1
  103. package/src/secrets/infisical-identity-file.ts +69 -0
  104. package/src/secrets/infisical.ts +18 -3
  105. package/src/string/index.ts +59 -0
  106. package/src/testing/README.md +119 -0
  107. package/src/testing/connection-auth-contract.ts +303 -0
  108. package/src/testing/helpers.ts +41 -0
  109. package/src/testing/index.ts +12 -0
  110. package/src/testing/module-mocker.ts +50 -0
  111. package/src/testing/prisma.ts +78 -0
  112. package/src/testing/window.ts +119 -0
  113. package/src/typedswitch/README.md +55 -39
  114. package/src/typedswitch/index.ts +97 -83
  115. package/src/zod/index.ts +20 -0
  116. package/src/zod/introspect.ts +1 -1
@@ -0,0 +1,577 @@
1
+ // src/redis/client.ts
2
+ var CONNECTION_OPTIONS = {
3
+ autoReconnect: true,
4
+ maxRetries: 1e9,
5
+ enableOfflineQueue: true
6
+ };
7
+ function defaultCreateClient(url, options) {
8
+ return new Bun.RedisClient(url, options);
9
+ }
10
+ function defaultOnError(error, context) {
11
+ const channel = context.channel ? ` (${context.channel})` : "";
12
+ console.error(`Redis ${context.role} ${context.stage} failed${channel}`, error);
13
+ }
14
+ function createManagedRedis(options) {
15
+ const createClient = options.createClient ?? defaultCreateClient;
16
+ const report = options.onError ?? defaultOnError;
17
+ const url = () => typeof options.url === "function" ? options.url() : options.url;
18
+ let client = null;
19
+ let subscriber = null;
20
+ const handlers = new Map;
21
+ const listeners = new Map;
22
+ const subscribing = new Map;
23
+ function listenerFor(channel) {
24
+ let listener = listeners.get(channel);
25
+ if (!listener) {
26
+ listener = (message, messageChannel) => {
27
+ if (messageChannel !== channel)
28
+ return;
29
+ for (const entry of [...handlers.get(channel) ?? []]) {
30
+ try {
31
+ entry.handler(message, channel);
32
+ } catch (error) {
33
+ report(error, { role: "subscriber", stage: "handler", channel });
34
+ }
35
+ }
36
+ };
37
+ listeners.set(channel, listener);
38
+ }
39
+ return listener;
40
+ }
41
+ async function resubscribe(connection) {
42
+ for (const channel of handlers.keys()) {
43
+ try {
44
+ await connection.subscribe(channel, listenerFor(channel));
45
+ } catch (error) {
46
+ report(error, { role: "subscriber", stage: "resubscribe", channel });
47
+ }
48
+ }
49
+ }
50
+ function getSubscriber() {
51
+ if (subscriber)
52
+ return subscriber;
53
+ const connection = createClient(url(), CONNECTION_OPTIONS);
54
+ connection.onconnect = () => {
55
+ resubscribe(connection);
56
+ };
57
+ connection.onclose = (error) => {
58
+ if (subscriber !== connection)
59
+ return;
60
+ subscriber = null;
61
+ listeners.clear();
62
+ report(error, { role: "subscriber", stage: "close" });
63
+ if (handlers.size > 0)
64
+ getSubscriber();
65
+ };
66
+ subscriber = connection;
67
+ return connection;
68
+ }
69
+ function getClient() {
70
+ if (client)
71
+ return client;
72
+ const connection = createClient(url(), CONNECTION_OPTIONS);
73
+ connection.onclose = (error) => {
74
+ if (client !== connection)
75
+ return;
76
+ client = null;
77
+ if (error)
78
+ report(error, { role: "client", stage: "close" });
79
+ };
80
+ client = connection;
81
+ return connection;
82
+ }
83
+ async function subscribe(channel, handler) {
84
+ const entry = { handler };
85
+ let entries = handlers.get(channel);
86
+ if (!entries) {
87
+ entries = new Set;
88
+ handlers.set(channel, entries);
89
+ }
90
+ entries.add(entry);
91
+ let pending = subscribing.get(channel);
92
+ const first = !pending && entries.size === 1;
93
+ if (first) {
94
+ pending = getSubscriber().subscribe(channel, listenerFor(channel));
95
+ subscribing.set(channel, pending);
96
+ pending.catch(() => {}).finally(() => {
97
+ if (subscribing.get(channel) === pending)
98
+ subscribing.delete(channel);
99
+ });
100
+ }
101
+ try {
102
+ await pending;
103
+ } catch (error) {
104
+ entries.delete(entry);
105
+ if (entries.size === 0 && handlers.get(channel) === entries) {
106
+ handlers.delete(channel);
107
+ listeners.delete(channel);
108
+ }
109
+ if (first)
110
+ report(error, { role: "subscriber", stage: "subscribe", channel });
111
+ throw error;
112
+ }
113
+ let left = false;
114
+ return async () => {
115
+ if (left)
116
+ return;
117
+ left = true;
118
+ const current = handlers.get(channel);
119
+ if (!current?.delete(entry) || current.size > 0)
120
+ return;
121
+ handlers.delete(channel);
122
+ const listener = listeners.get(channel);
123
+ listeners.delete(channel);
124
+ const connection = subscriber;
125
+ try {
126
+ if (listener)
127
+ await connection?.unsubscribe(channel, listener);
128
+ } catch (error) {
129
+ if (subscriber !== connection)
130
+ return;
131
+ report(error, { role: "subscriber", stage: "unsubscribe", channel });
132
+ }
133
+ };
134
+ }
135
+ function close() {
136
+ const connections = [client, subscriber];
137
+ client = null;
138
+ subscriber = null;
139
+ handlers.clear();
140
+ listeners.clear();
141
+ subscribing.clear();
142
+ for (const connection of connections) {
143
+ try {
144
+ connection?.close();
145
+ } catch {}
146
+ }
147
+ }
148
+ return {
149
+ getClient,
150
+ subscribe,
151
+ isConnected: () => client?.connected === true,
152
+ getStats: () => ({
153
+ channelCount: handlers.size,
154
+ channels: [...handlers.keys()],
155
+ isConnected: client?.connected === true,
156
+ isSubscriberConnected: subscriber?.connected === true
157
+ }),
158
+ close
159
+ };
160
+ }
161
+ // src/redis/lua-script.ts
162
+ function isNoScriptError(error) {
163
+ const message = error instanceof Error ? error.message : String(error);
164
+ return message.includes("NOSCRIPT");
165
+ }
166
+ function createLuaScript(send, source) {
167
+ let sha = null;
168
+ let loading = null;
169
+ function load() {
170
+ loading ??= send("SCRIPT", ["LOAD", source]).then(String).finally(() => {
171
+ loading = null;
172
+ });
173
+ return loading;
174
+ }
175
+ return async function evalScript(keys, args) {
176
+ const tail = [String(keys.length), ...keys, ...args];
177
+ sha ??= await load();
178
+ try {
179
+ return await send("EVALSHA", [sha, ...tail]);
180
+ } catch (error) {
181
+ if (!isNoScriptError(error))
182
+ throw error;
183
+ sha = null;
184
+ return await send("EVAL", [source, ...tail]);
185
+ }
186
+ };
187
+ }
188
+
189
+ // src/redis/concurrency-limit.ts
190
+ var POLL_MS = 100;
191
+ var FAILURE_LOG_THROTTLE_MS = 1e4;
192
+ var DEFAULT_FAIL_OPEN_AFTER_MS = 15000;
193
+ var DEFAULT_COMMAND_TIMEOUT_MS = 3000;
194
+ var START_WINDOW_MS = 1000;
195
+ var MAX_COOLDOWN_MS = 5 * 60 * 1000;
196
+ var BREAKER_PROBE_INTERVAL_MS = 5000;
197
+ var MAX_RENEW_BATCH = 200;
198
+ var NOW_MS_LUA = `
199
+ local time = redis.call('TIME')
200
+ local now = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
201
+ `;
202
+ var ACQUIRE_SCRIPT = `
203
+ local inflightKey = KEYS[1]
204
+ local cooldownKey = KEYS[2]
205
+ local limit = tonumber(ARGV[1])
206
+ local ttlMs = tonumber(ARGV[2])
207
+ local cooldownMs = redis.call('PTTL', cooldownKey)
208
+ if cooldownMs > 0 then
209
+ return {0, cooldownMs}
210
+ end
211
+ ${NOW_MS_LUA}
212
+ redis.call('ZREMRANGEBYSCORE', inflightKey, '-inf', now)
213
+ local free = limit - redis.call('ZCARD', inflightKey)
214
+ local granted = 0
215
+ while granted < free and (granted + 3) <= #ARGV do
216
+ redis.call('ZADD', inflightKey, now + ttlMs, ARGV[granted + 3])
217
+ granted = granted + 1
218
+ end
219
+ if granted > 0 then
220
+ redis.call('PEXPIRE', inflightKey, ttlMs)
221
+ end
222
+ return {granted, 0}
223
+ `.trim();
224
+ var START_SCRIPT = `
225
+ local startsKey = KEYS[1]
226
+ local cooldownKey = KEYS[2]
227
+ local limit = tonumber(ARGV[1])
228
+ local windowMs = tonumber(ARGV[2])
229
+ local cooldownMs = redis.call('PTTL', cooldownKey)
230
+ if cooldownMs > 0 then
231
+ return {0, cooldownMs}
232
+ end
233
+ ${NOW_MS_LUA}
234
+ redis.call('ZREMRANGEBYSCORE', startsKey, '-inf', now - windowMs)
235
+ local free = limit - redis.call('ZCARD', startsKey)
236
+ local granted = 0
237
+ while granted < free and (granted + 3) <= #ARGV do
238
+ redis.call('ZADD', startsKey, now, ARGV[granted + 3])
239
+ granted = granted + 1
240
+ end
241
+ if granted > 0 then
242
+ redis.call('PEXPIRE', startsKey, windowMs)
243
+ end
244
+ local waitMs = 0
245
+ if granted < (#ARGV - 2) then
246
+ local oldest = redis.call('ZRANGE', startsKey, 0, 0, 'WITHSCORES')
247
+ if oldest[2] then
248
+ waitMs = math.ceil(tonumber(oldest[2]) + windowMs - now)
249
+ if waitMs < 0 then
250
+ waitMs = 0
251
+ end
252
+ end
253
+ end
254
+ return {granted, waitMs}
255
+ `.trim();
256
+ var RENEW_SCRIPT = `
257
+ local key = KEYS[1]
258
+ local ttlMs = tonumber(ARGV[1])
259
+ ${NOW_MS_LUA}
260
+ local renewed = 0
261
+ for i = 2, #ARGV do
262
+ if redis.call('ZSCORE', key, ARGV[i]) then
263
+ redis.call('ZADD', key, now + ttlMs, ARGV[i])
264
+ renewed = renewed + 1
265
+ end
266
+ end
267
+ if renewed > 0 then
268
+ redis.call('PEXPIRE', key, ttlMs)
269
+ end
270
+ return renewed
271
+ `.trim();
272
+ var TRIP_COOLDOWN_SCRIPT = `
273
+ local key = KEYS[1]
274
+ local waitMs = tonumber(ARGV[1])
275
+ local ttl = redis.call('PTTL', key)
276
+ if ttl < waitMs then
277
+ redis.call('SET', key, '1', 'PX', ARGV[1])
278
+ end
279
+ return 1
280
+ `.trim();
281
+ function withCommandTimeout(send, timeoutMs) {
282
+ return async (command, args) => {
283
+ let timer;
284
+ try {
285
+ return await Promise.race([
286
+ send(command, args),
287
+ new Promise((_, reject) => {
288
+ timer = setTimeout(() => reject(new Error(`Redis command timed out after ${timeoutMs}ms`)), timeoutMs);
289
+ })
290
+ ]);
291
+ } finally {
292
+ if (timer)
293
+ clearTimeout(timer);
294
+ }
295
+ };
296
+ }
297
+ function createToken() {
298
+ const pid = globalThis.process?.pid;
299
+ return pid === undefined ? crypto.randomUUID() : `${pid}:${crypto.randomUUID()}`;
300
+ }
301
+ function jitterMs() {
302
+ return Math.floor(Math.random() * POLL_MS);
303
+ }
304
+ function unref(timer) {
305
+ timer.unref?.();
306
+ }
307
+ function parseGrantReply(reply) {
308
+ const [granted, waitMs] = Array.isArray(reply) ? reply : [];
309
+ return {
310
+ granted: Math.max(0, Number(granted) || 0),
311
+ waitMs: Math.min(MAX_COOLDOWN_MS, Math.max(0, Number(waitMs) || 0))
312
+ };
313
+ }
314
+ function createRedisConcurrencyLimit(input) {
315
+ if (!Number.isInteger(input.max) || input.max < 1)
316
+ throw new Error(`max must be a positive integer, got ${input.max}`);
317
+ const logger = input.logger ?? console;
318
+ const commandTimeoutMs = input.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
319
+ const send = withCommandTimeout(input.send, commandTimeoutMs);
320
+ const ttlMs = String(input.slotTtlMs);
321
+ const renewEveryMs = Math.max(100, Math.floor(input.slotTtlMs / 3));
322
+ const failOpenAfterMs = input.failOpenAfterMs ?? DEFAULT_FAIL_OPEN_AFTER_MS;
323
+ const maxStartsPerSecond = input.maxStartsPerSecond;
324
+ const inflightKey = `{${input.key}}:inflight`;
325
+ const startsKey = `{${input.key}}:starts`;
326
+ const cooldownKey = `{${input.key}}:cooldown`;
327
+ const acquireScript = createLuaScript(send, ACQUIRE_SCRIPT);
328
+ const startScript = createLuaScript(send, START_SCRIPT);
329
+ const renewScript = createLuaScript(send, RENEW_SCRIPT);
330
+ const tripScript = createLuaScript(send, TRIP_COOLDOWN_SCRIPT);
331
+ const lastFailureLogAtMs = new Map;
332
+ function logFailure(message, error) {
333
+ const nowMs = Date.now();
334
+ if (nowMs - (lastFailureLogAtMs.get(message) ?? 0) < FAILURE_LOG_THROTTLE_MS)
335
+ return;
336
+ lastFailureLogAtMs.set(message, nowMs);
337
+ logger.warn(message, { key: input.key, error });
338
+ }
339
+ let firstFailureAtMs = null;
340
+ let degraded = false;
341
+ let probeTimer = null;
342
+ function scheduleProbe() {
343
+ if (probeTimer)
344
+ return;
345
+ probeTimer = setTimeout(() => {
346
+ probeTimer = null;
347
+ probeRedis();
348
+ }, BREAKER_PROBE_INTERVAL_MS);
349
+ unref(probeTimer);
350
+ }
351
+ async function probeRedis() {
352
+ try {
353
+ await acquireScript([inflightKey, cooldownKey], [String(input.max), ttlMs]);
354
+ noteRedisSuccess();
355
+ } catch (error) {
356
+ logFailure("Redis concurrency probe failed", error);
357
+ scheduleProbe();
358
+ }
359
+ }
360
+ function noteRedisSuccess() {
361
+ firstFailureAtMs = null;
362
+ if (!degraded)
363
+ return;
364
+ degraded = false;
365
+ if (probeTimer) {
366
+ clearTimeout(probeTimer);
367
+ probeTimer = null;
368
+ }
369
+ logger.warn("Redis reachable again, concurrency limiting re-enabled", {
370
+ key: input.key
371
+ });
372
+ }
373
+ function noteRedisFailure(error) {
374
+ logFailure("Redis concurrency command failed, retrying", error);
375
+ if (degraded)
376
+ return true;
377
+ firstFailureAtMs ??= Date.now();
378
+ if (Date.now() - firstFailureAtMs < failOpenAfterMs)
379
+ return false;
380
+ degraded = true;
381
+ scheduleProbe();
382
+ logger.warn("Redis unreachable, running without a concurrency slot", {
383
+ key: input.key
384
+ });
385
+ return true;
386
+ }
387
+ function createPump(stage, maxBatch, grant) {
388
+ const queue = [];
389
+ let running = false;
390
+ let wakeSleeper = null;
391
+ function sleep(ms) {
392
+ return new Promise((resolve) => {
393
+ const timer = setTimeout(() => {
394
+ wakeSleeper = null;
395
+ resolve();
396
+ }, ms);
397
+ wakeSleeper = () => {
398
+ wakeSleeper = null;
399
+ clearTimeout(timer);
400
+ resolve();
401
+ };
402
+ });
403
+ }
404
+ function settleAll(grantedSlot) {
405
+ for (const waiter of queue.splice(0))
406
+ waiter.settle(grantedSlot);
407
+ }
408
+ async function loop() {
409
+ running = true;
410
+ try {
411
+ await Promise.resolve();
412
+ while (queue.length > 0) {
413
+ let reply;
414
+ try {
415
+ reply = await grant(queue.slice(0, maxBatch).map((waiter) => waiter.token));
416
+ noteRedisSuccess();
417
+ } catch (error) {
418
+ if (noteRedisFailure(error)) {
419
+ settleAll(false);
420
+ continue;
421
+ }
422
+ await sleep(POLL_MS + jitterMs());
423
+ continue;
424
+ }
425
+ for (const waiter of queue.splice(0, reply.granted))
426
+ waiter.settle(true);
427
+ if (queue.length === 0)
428
+ break;
429
+ await sleep((reply.waitMs || POLL_MS) + jitterMs());
430
+ }
431
+ } finally {
432
+ running = false;
433
+ }
434
+ }
435
+ return {
436
+ depth: () => queue.length,
437
+ acquire(token) {
438
+ const stopWait = input.onWait?.(stage);
439
+ const granted = new Promise((resolve) => {
440
+ queue.push({ token, settle: resolve });
441
+ });
442
+ if (!running)
443
+ loop();
444
+ return stopWait ? granted.finally(stopWait) : granted;
445
+ },
446
+ wake() {
447
+ wakeSleeper?.();
448
+ }
449
+ };
450
+ }
451
+ const slotPump = createPump("slot", input.max, async (tokens) => parseGrantReply(await acquireScript([inflightKey, cooldownKey], [String(input.max), ttlMs, ...tokens])));
452
+ const startPump = maxStartsPerSecond === undefined ? null : createPump("start", maxStartsPerSecond, async (tokens) => parseGrantReply(await startScript([startsKey, cooldownKey], [String(maxStartsPerSecond), String(START_WINDOW_MS), ...tokens])));
453
+ const queueDepth = () => slotPump.depth() + (startPump?.depth() ?? 0);
454
+ const heldTokens = new Set;
455
+ let heartbeatTimer = null;
456
+ function scheduleHeartbeat() {
457
+ if (heartbeatTimer || heldTokens.size === 0)
458
+ return;
459
+ heartbeatTimer = setTimeout(() => {
460
+ heartbeatTimer = null;
461
+ renewHeldSlots();
462
+ }, renewEveryMs);
463
+ unref(heartbeatTimer);
464
+ }
465
+ async function renewHeldSlots() {
466
+ const tokens = [...heldTokens];
467
+ for (let index = 0;index < tokens.length; index += MAX_RENEW_BATCH) {
468
+ try {
469
+ await renewScript([inflightKey], [ttlMs, ...tokens.slice(index, index + MAX_RENEW_BATCH)]);
470
+ noteRedisSuccess();
471
+ } catch (error) {
472
+ logFailure("Failed to renew Redis concurrency slots", error);
473
+ }
474
+ }
475
+ scheduleHeartbeat();
476
+ }
477
+ let pendingReleases = [];
478
+ let releaseFlush = null;
479
+ function release(token) {
480
+ pendingReleases.push(token);
481
+ releaseFlush ??= (async () => {
482
+ await Promise.resolve();
483
+ const tokens = pendingReleases;
484
+ pendingReleases = [];
485
+ releaseFlush = null;
486
+ try {
487
+ await send("ZREM", [inflightKey, ...tokens]);
488
+ noteRedisSuccess();
489
+ } catch (error) {
490
+ logFailure("Failed to release Redis concurrency slot", error);
491
+ }
492
+ slotPump.wake();
493
+ })();
494
+ return releaseFlush;
495
+ }
496
+ async function tripCooldown(retryAfterMs) {
497
+ const waitMs = Math.min(MAX_COOLDOWN_MS, Math.max(0, Math.ceil(retryAfterMs)));
498
+ if (waitMs === 0)
499
+ return;
500
+ try {
501
+ await tripScript([cooldownKey], [String(waitMs)]);
502
+ } catch (error) {
503
+ logFailure("Failed to trip Redis rate-limit cooldown", error);
504
+ }
505
+ }
506
+ async function waitForGrant(pending, signal, lateGrant) {
507
+ if (!signal)
508
+ return pending;
509
+ let aborted = false;
510
+ let abort;
511
+ try {
512
+ return await Promise.race([
513
+ pending.then((granted) => {
514
+ if (aborted && granted)
515
+ lateGrant();
516
+ return granted;
517
+ }),
518
+ new Promise((_, reject) => {
519
+ abort = () => {
520
+ aborted = true;
521
+ reject(signal.reason);
522
+ };
523
+ signal.addEventListener("abort", abort, { once: true });
524
+ if (signal.aborted)
525
+ abort();
526
+ })
527
+ ]);
528
+ } finally {
529
+ if (abort)
530
+ signal.removeEventListener("abort", abort);
531
+ }
532
+ }
533
+ async function withRedisConcurrencyLimit(run, signal) {
534
+ signal?.throwIfAborted();
535
+ if (degraded)
536
+ return run();
537
+ const token = createToken();
538
+ const granted = await waitForGrant(slotPump.acquire(token), signal, () => {
539
+ release(token);
540
+ });
541
+ if (!granted) {
542
+ signal?.throwIfAborted();
543
+ return run();
544
+ }
545
+ heldTokens.add(token);
546
+ scheduleHeartbeat();
547
+ try {
548
+ if (startPump)
549
+ await waitForGrant(startPump.acquire(createToken()), signal, () => {});
550
+ signal?.throwIfAborted();
551
+ return await run();
552
+ } finally {
553
+ heldTokens.delete(token);
554
+ await release(token);
555
+ }
556
+ }
557
+ function dispose() {
558
+ if (probeTimer) {
559
+ clearTimeout(probeTimer);
560
+ probeTimer = null;
561
+ }
562
+ if (heartbeatTimer) {
563
+ clearTimeout(heartbeatTimer);
564
+ heartbeatTimer = null;
565
+ }
566
+ }
567
+ return Object.assign(withRedisConcurrencyLimit, {
568
+ tripCooldown,
569
+ queueDepth,
570
+ dispose
571
+ });
572
+ }
573
+ export {
574
+ createLuaScript,
575
+ createManagedRedis,
576
+ createRedisConcurrencyLimit
577
+ };