flowx-control 1.0.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 +443 -0
- package/dist/batch.d.mts +37 -0
- package/dist/batch.d.ts +37 -0
- package/dist/batch.js +75 -0
- package/dist/batch.js.map +1 -0
- package/dist/batch.mjs +73 -0
- package/dist/batch.mjs.map +1 -0
- package/dist/bulkhead.d.mts +30 -0
- package/dist/bulkhead.d.ts +30 -0
- package/dist/bulkhead.js +85 -0
- package/dist/bulkhead.js.map +1 -0
- package/dist/bulkhead.mjs +83 -0
- package/dist/bulkhead.mjs.map +1 -0
- package/dist/circuit-breaker.d.mts +44 -0
- package/dist/circuit-breaker.d.ts +44 -0
- package/dist/circuit-breaker.js +132 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/circuit-breaker.mjs +130 -0
- package/dist/circuit-breaker.mjs.map +1 -0
- package/dist/debounce.d.mts +30 -0
- package/dist/debounce.d.ts +30 -0
- package/dist/debounce.js +96 -0
- package/dist/debounce.js.map +1 -0
- package/dist/debounce.mjs +94 -0
- package/dist/debounce.mjs.map +1 -0
- package/dist/deferred.d.mts +27 -0
- package/dist/deferred.d.ts +27 -0
- package/dist/deferred.js +42 -0
- package/dist/deferred.js.map +1 -0
- package/dist/deferred.mjs +40 -0
- package/dist/deferred.mjs.map +1 -0
- package/dist/fallback.d.mts +33 -0
- package/dist/fallback.d.ts +33 -0
- package/dist/fallback.js +43 -0
- package/dist/fallback.js.map +1 -0
- package/dist/fallback.mjs +40 -0
- package/dist/fallback.mjs.map +1 -0
- package/dist/hedge.d.mts +18 -0
- package/dist/hedge.d.ts +18 -0
- package/dist/hedge.js +47 -0
- package/dist/hedge.js.map +1 -0
- package/dist/hedge.mjs +45 -0
- package/dist/hedge.mjs.map +1 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1151 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1122 -0
- package/dist/index.mjs.map +1 -0
- package/dist/memo.d.mts +35 -0
- package/dist/memo.d.ts +35 -0
- package/dist/memo.js +74 -0
- package/dist/memo.js.map +1 -0
- package/dist/memo.mjs +72 -0
- package/dist/memo.mjs.map +1 -0
- package/dist/mutex.d.mts +24 -0
- package/dist/mutex.d.ts +24 -0
- package/dist/mutex.js +46 -0
- package/dist/mutex.js.map +1 -0
- package/dist/mutex.mjs +44 -0
- package/dist/mutex.mjs.map +1 -0
- package/dist/pipeline.d.mts +42 -0
- package/dist/pipeline.d.ts +42 -0
- package/dist/pipeline.js +30 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/pipeline.mjs +27 -0
- package/dist/pipeline.mjs.map +1 -0
- package/dist/poll.d.mts +35 -0
- package/dist/poll.d.ts +35 -0
- package/dist/poll.js +111 -0
- package/dist/poll.js.map +1 -0
- package/dist/poll.mjs +109 -0
- package/dist/poll.mjs.map +1 -0
- package/dist/queue.d.mts +47 -0
- package/dist/queue.d.ts +47 -0
- package/dist/queue.js +121 -0
- package/dist/queue.js.map +1 -0
- package/dist/queue.mjs +119 -0
- package/dist/queue.mjs.map +1 -0
- package/dist/rate-limit.d.mts +28 -0
- package/dist/rate-limit.d.ts +28 -0
- package/dist/rate-limit.js +94 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/rate-limit.mjs +92 -0
- package/dist/rate-limit.mjs.map +1 -0
- package/dist/retry.d.mts +45 -0
- package/dist/retry.d.ts +45 -0
- package/dist/retry.js +111 -0
- package/dist/retry.js.map +1 -0
- package/dist/retry.mjs +108 -0
- package/dist/retry.mjs.map +1 -0
- package/dist/semaphore.d.mts +27 -0
- package/dist/semaphore.d.ts +27 -0
- package/dist/semaphore.js +47 -0
- package/dist/semaphore.js.map +1 -0
- package/dist/semaphore.mjs +45 -0
- package/dist/semaphore.mjs.map +1 -0
- package/dist/throttle.d.mts +25 -0
- package/dist/throttle.d.ts +25 -0
- package/dist/throttle.js +97 -0
- package/dist/throttle.js.map +1 -0
- package/dist/throttle.mjs +95 -0
- package/dist/throttle.mjs.map +1 -0
- package/dist/timeout.d.mts +19 -0
- package/dist/timeout.d.ts +19 -0
- package/dist/timeout.js +79 -0
- package/dist/timeout.js.map +1 -0
- package/dist/timeout.mjs +77 -0
- package/dist/timeout.mjs.map +1 -0
- package/dist/types-BsCO2J40.d.mts +35 -0
- package/dist/types-BsCO2J40.d.ts +35 -0
- package/package.json +167 -0
package/dist/queue.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/queue.ts
|
|
4
|
+
function createQueue(options) {
|
|
5
|
+
const { concurrency = 1, autoStart = true, timeout = 0 } = options ?? {};
|
|
6
|
+
if (concurrency < 1) throw new RangeError("concurrency must be >= 1");
|
|
7
|
+
let paused = !autoStart;
|
|
8
|
+
let active = 0;
|
|
9
|
+
const entries = [];
|
|
10
|
+
const emptyCallbacks = [];
|
|
11
|
+
const idleCallbacks = [];
|
|
12
|
+
function notifyIdle() {
|
|
13
|
+
if (active === 0 && entries.length === 0) {
|
|
14
|
+
for (const cb of idleCallbacks.splice(0)) cb();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function notifyEmpty() {
|
|
18
|
+
if (entries.length === 0) {
|
|
19
|
+
for (const cb of emptyCallbacks.splice(0)) cb();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function tryRun() {
|
|
23
|
+
if (paused) return;
|
|
24
|
+
while (active < concurrency && entries.length > 0) {
|
|
25
|
+
const entry = entries.shift();
|
|
26
|
+
active++;
|
|
27
|
+
let timer;
|
|
28
|
+
const run = async () => {
|
|
29
|
+
try {
|
|
30
|
+
let result;
|
|
31
|
+
if (timeout > 0) {
|
|
32
|
+
result = await Promise.race([
|
|
33
|
+
entry.fn(),
|
|
34
|
+
new Promise((_, rej) => {
|
|
35
|
+
timer = setTimeout(() => rej(new Error("Queue task timeout")), timeout);
|
|
36
|
+
})
|
|
37
|
+
]);
|
|
38
|
+
} else {
|
|
39
|
+
result = await entry.fn();
|
|
40
|
+
}
|
|
41
|
+
if (timer) clearTimeout(timer);
|
|
42
|
+
entry.resolve(result);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (timer) clearTimeout(timer);
|
|
45
|
+
entry.reject(error instanceof Error ? error : new Error(String(error)));
|
|
46
|
+
} finally {
|
|
47
|
+
active--;
|
|
48
|
+
notifyEmpty();
|
|
49
|
+
tryRun();
|
|
50
|
+
notifyIdle();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
run();
|
|
54
|
+
}
|
|
55
|
+
notifyEmpty();
|
|
56
|
+
notifyIdle();
|
|
57
|
+
}
|
|
58
|
+
function add(fn, addOptions) {
|
|
59
|
+
const priority = addOptions?.priority ?? 0;
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const entry = { fn, priority, resolve, reject };
|
|
62
|
+
let inserted = false;
|
|
63
|
+
for (let i = 0; i < entries.length; i++) {
|
|
64
|
+
if (priority < entries[i].priority) {
|
|
65
|
+
entries.splice(i, 0, entry);
|
|
66
|
+
inserted = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!inserted) {
|
|
71
|
+
entries.push(entry);
|
|
72
|
+
}
|
|
73
|
+
tryRun();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function addAll(fns, addOptions) {
|
|
77
|
+
return Promise.all(fns.map((fn) => add(fn, addOptions)));
|
|
78
|
+
}
|
|
79
|
+
function pause() {
|
|
80
|
+
paused = true;
|
|
81
|
+
}
|
|
82
|
+
function resume() {
|
|
83
|
+
paused = false;
|
|
84
|
+
tryRun();
|
|
85
|
+
}
|
|
86
|
+
function clear() {
|
|
87
|
+
for (const entry of entries.splice(0)) {
|
|
88
|
+
entry.reject(new Error("Queue cleared"));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function onEmpty() {
|
|
92
|
+
if (entries.length === 0) return Promise.resolve();
|
|
93
|
+
return new Promise((resolve) => emptyCallbacks.push(resolve));
|
|
94
|
+
}
|
|
95
|
+
function onIdle() {
|
|
96
|
+
if (active === 0 && entries.length === 0) return Promise.resolve();
|
|
97
|
+
return new Promise((resolve) => idleCallbacks.push(resolve));
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
add,
|
|
101
|
+
addAll,
|
|
102
|
+
pause,
|
|
103
|
+
resume,
|
|
104
|
+
clear,
|
|
105
|
+
onEmpty,
|
|
106
|
+
onIdle,
|
|
107
|
+
get size() {
|
|
108
|
+
return entries.length;
|
|
109
|
+
},
|
|
110
|
+
get pending() {
|
|
111
|
+
return active;
|
|
112
|
+
},
|
|
113
|
+
get isPaused() {
|
|
114
|
+
return paused;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
exports.createQueue = createQueue;
|
|
120
|
+
//# sourceMappingURL=queue.js.map
|
|
121
|
+
//# sourceMappingURL=queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/queue.ts"],"names":[],"mappings":"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","file":"queue.js","sourcesContent":["// ============================================================================\n// FlowX — Priority Async Queue\n// ============================================================================\n\nexport interface QueueOptions {\n /** Maximum concurrent tasks (default: 1) */\n concurrency?: number;\n /** Start processing immediately (default: true) */\n autoStart?: boolean;\n /** Per-task timeout in ms (0 = no timeout) */\n timeout?: number;\n}\n\nexport interface QueueAddOptions {\n /** Task priority — lower number = higher priority (default: 0) */\n priority?: number;\n}\n\nexport interface AsyncQueue {\n /** Add a task to the queue */\n add: <T>(fn: () => Promise<T>, options?: QueueAddOptions) => Promise<T>;\n /** Add multiple tasks and return all results */\n addAll: <T>(fns: Array<() => Promise<T>>, options?: QueueAddOptions) => Promise<T[]>;\n /** Pause processing */\n pause: () => void;\n /** Resume processing */\n resume: () => void;\n /** Clear all pending tasks */\n clear: () => void;\n /** Wait until the queue is empty */\n onEmpty: () => Promise<void>;\n /** Wait until the queue is idle (empty + no active tasks) */\n onIdle: () => Promise<void>;\n /** Number of pending tasks */\n readonly size: number;\n /** Number of active tasks */\n readonly pending: number;\n /** Whether the queue is paused */\n readonly isPaused: boolean;\n}\n\ninterface QueueEntry {\n fn: () => Promise<any>;\n priority: number;\n resolve: (value: any) => void;\n reject: (error: Error) => void;\n}\n\n/**\n * Create a priority async queue with concurrency control.\n *\n * @example\n * ```ts\n * const queue = createQueue({ concurrency: 3 });\n * const result = await queue.add(() => fetch('/api'), { priority: 1 });\n * await queue.onIdle();\n * ```\n */\nexport function createQueue(options?: QueueOptions): AsyncQueue {\n const { concurrency = 1, autoStart = true, timeout = 0 } = options ?? {};\n\n if (concurrency < 1) throw new RangeError('concurrency must be >= 1');\n\n let paused = !autoStart;\n let active = 0;\n const entries: QueueEntry[] = [];\n const emptyCallbacks: Array<() => void> = [];\n const idleCallbacks: Array<() => void> = [];\n\n function notifyIdle(): void {\n if (active === 0 && entries.length === 0) {\n for (const cb of idleCallbacks.splice(0)) cb();\n }\n }\n\n function notifyEmpty(): void {\n if (entries.length === 0) {\n for (const cb of emptyCallbacks.splice(0)) cb();\n }\n }\n\n function tryRun(): void {\n if (paused) return;\n\n while (active < concurrency && entries.length > 0) {\n const entry = entries.shift()!;\n active++;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const run = async () => {\n try {\n let result: any;\n if (timeout > 0) {\n result = await Promise.race([\n entry.fn(),\n new Promise<never>((_, rej) => {\n timer = setTimeout(() => rej(new Error('Queue task timeout')), timeout);\n }),\n ]);\n } else {\n result = await entry.fn();\n }\n if (timer) clearTimeout(timer);\n entry.resolve(result);\n } catch (error) {\n if (timer) clearTimeout(timer);\n entry.reject(error instanceof Error ? error : new Error(String(error)));\n } finally {\n active--;\n notifyEmpty();\n tryRun();\n notifyIdle();\n }\n };\n\n run();\n }\n\n notifyEmpty();\n notifyIdle();\n }\n\n function add<T>(fn: () => Promise<T>, addOptions?: QueueAddOptions): Promise<T> {\n const priority = addOptions?.priority ?? 0;\n\n return new Promise<T>((resolve, reject) => {\n const entry: QueueEntry = { fn, priority, resolve, reject };\n\n // Insert sorted by priority (lower = higher priority)\n let inserted = false;\n for (let i = 0; i < entries.length; i++) {\n if (priority < entries[i].priority) {\n entries.splice(i, 0, entry);\n inserted = true;\n break;\n }\n }\n if (!inserted) {\n entries.push(entry);\n }\n\n tryRun();\n });\n }\n\n function addAll<T>(fns: Array<() => Promise<T>>, addOptions?: QueueAddOptions): Promise<T[]> {\n return Promise.all(fns.map((fn) => add(fn, addOptions)));\n }\n\n function pause(): void {\n paused = true;\n }\n\n function resume(): void {\n paused = false;\n tryRun();\n }\n\n function clear(): void {\n for (const entry of entries.splice(0)) {\n entry.reject(new Error('Queue cleared'));\n }\n }\n\n function onEmpty(): Promise<void> {\n if (entries.length === 0) return Promise.resolve();\n return new Promise<void>((resolve) => emptyCallbacks.push(resolve));\n }\n\n function onIdle(): Promise<void> {\n if (active === 0 && entries.length === 0) return Promise.resolve();\n return new Promise<void>((resolve) => idleCallbacks.push(resolve));\n }\n\n return {\n add,\n addAll,\n pause,\n resume,\n clear,\n onEmpty,\n onIdle,\n get size() {\n return entries.length;\n },\n get pending() {\n return active;\n },\n get isPaused() {\n return paused;\n },\n };\n}\n"]}
|
package/dist/queue.mjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// src/queue.ts
|
|
2
|
+
function createQueue(options) {
|
|
3
|
+
const { concurrency = 1, autoStart = true, timeout = 0 } = options ?? {};
|
|
4
|
+
if (concurrency < 1) throw new RangeError("concurrency must be >= 1");
|
|
5
|
+
let paused = !autoStart;
|
|
6
|
+
let active = 0;
|
|
7
|
+
const entries = [];
|
|
8
|
+
const emptyCallbacks = [];
|
|
9
|
+
const idleCallbacks = [];
|
|
10
|
+
function notifyIdle() {
|
|
11
|
+
if (active === 0 && entries.length === 0) {
|
|
12
|
+
for (const cb of idleCallbacks.splice(0)) cb();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function notifyEmpty() {
|
|
16
|
+
if (entries.length === 0) {
|
|
17
|
+
for (const cb of emptyCallbacks.splice(0)) cb();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function tryRun() {
|
|
21
|
+
if (paused) return;
|
|
22
|
+
while (active < concurrency && entries.length > 0) {
|
|
23
|
+
const entry = entries.shift();
|
|
24
|
+
active++;
|
|
25
|
+
let timer;
|
|
26
|
+
const run = async () => {
|
|
27
|
+
try {
|
|
28
|
+
let result;
|
|
29
|
+
if (timeout > 0) {
|
|
30
|
+
result = await Promise.race([
|
|
31
|
+
entry.fn(),
|
|
32
|
+
new Promise((_, rej) => {
|
|
33
|
+
timer = setTimeout(() => rej(new Error("Queue task timeout")), timeout);
|
|
34
|
+
})
|
|
35
|
+
]);
|
|
36
|
+
} else {
|
|
37
|
+
result = await entry.fn();
|
|
38
|
+
}
|
|
39
|
+
if (timer) clearTimeout(timer);
|
|
40
|
+
entry.resolve(result);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (timer) clearTimeout(timer);
|
|
43
|
+
entry.reject(error instanceof Error ? error : new Error(String(error)));
|
|
44
|
+
} finally {
|
|
45
|
+
active--;
|
|
46
|
+
notifyEmpty();
|
|
47
|
+
tryRun();
|
|
48
|
+
notifyIdle();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
run();
|
|
52
|
+
}
|
|
53
|
+
notifyEmpty();
|
|
54
|
+
notifyIdle();
|
|
55
|
+
}
|
|
56
|
+
function add(fn, addOptions) {
|
|
57
|
+
const priority = addOptions?.priority ?? 0;
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const entry = { fn, priority, resolve, reject };
|
|
60
|
+
let inserted = false;
|
|
61
|
+
for (let i = 0; i < entries.length; i++) {
|
|
62
|
+
if (priority < entries[i].priority) {
|
|
63
|
+
entries.splice(i, 0, entry);
|
|
64
|
+
inserted = true;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!inserted) {
|
|
69
|
+
entries.push(entry);
|
|
70
|
+
}
|
|
71
|
+
tryRun();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function addAll(fns, addOptions) {
|
|
75
|
+
return Promise.all(fns.map((fn) => add(fn, addOptions)));
|
|
76
|
+
}
|
|
77
|
+
function pause() {
|
|
78
|
+
paused = true;
|
|
79
|
+
}
|
|
80
|
+
function resume() {
|
|
81
|
+
paused = false;
|
|
82
|
+
tryRun();
|
|
83
|
+
}
|
|
84
|
+
function clear() {
|
|
85
|
+
for (const entry of entries.splice(0)) {
|
|
86
|
+
entry.reject(new Error("Queue cleared"));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function onEmpty() {
|
|
90
|
+
if (entries.length === 0) return Promise.resolve();
|
|
91
|
+
return new Promise((resolve) => emptyCallbacks.push(resolve));
|
|
92
|
+
}
|
|
93
|
+
function onIdle() {
|
|
94
|
+
if (active === 0 && entries.length === 0) return Promise.resolve();
|
|
95
|
+
return new Promise((resolve) => idleCallbacks.push(resolve));
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
add,
|
|
99
|
+
addAll,
|
|
100
|
+
pause,
|
|
101
|
+
resume,
|
|
102
|
+
clear,
|
|
103
|
+
onEmpty,
|
|
104
|
+
onIdle,
|
|
105
|
+
get size() {
|
|
106
|
+
return entries.length;
|
|
107
|
+
},
|
|
108
|
+
get pending() {
|
|
109
|
+
return active;
|
|
110
|
+
},
|
|
111
|
+
get isPaused() {
|
|
112
|
+
return paused;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { createQueue };
|
|
118
|
+
//# sourceMappingURL=queue.mjs.map
|
|
119
|
+
//# sourceMappingURL=queue.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/queue.ts"],"names":[],"mappings":"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","file":"queue.mjs","sourcesContent":["// ============================================================================\n// FlowX — Priority Async Queue\n// ============================================================================\n\nexport interface QueueOptions {\n /** Maximum concurrent tasks (default: 1) */\n concurrency?: number;\n /** Start processing immediately (default: true) */\n autoStart?: boolean;\n /** Per-task timeout in ms (0 = no timeout) */\n timeout?: number;\n}\n\nexport interface QueueAddOptions {\n /** Task priority — lower number = higher priority (default: 0) */\n priority?: number;\n}\n\nexport interface AsyncQueue {\n /** Add a task to the queue */\n add: <T>(fn: () => Promise<T>, options?: QueueAddOptions) => Promise<T>;\n /** Add multiple tasks and return all results */\n addAll: <T>(fns: Array<() => Promise<T>>, options?: QueueAddOptions) => Promise<T[]>;\n /** Pause processing */\n pause: () => void;\n /** Resume processing */\n resume: () => void;\n /** Clear all pending tasks */\n clear: () => void;\n /** Wait until the queue is empty */\n onEmpty: () => Promise<void>;\n /** Wait until the queue is idle (empty + no active tasks) */\n onIdle: () => Promise<void>;\n /** Number of pending tasks */\n readonly size: number;\n /** Number of active tasks */\n readonly pending: number;\n /** Whether the queue is paused */\n readonly isPaused: boolean;\n}\n\ninterface QueueEntry {\n fn: () => Promise<any>;\n priority: number;\n resolve: (value: any) => void;\n reject: (error: Error) => void;\n}\n\n/**\n * Create a priority async queue with concurrency control.\n *\n * @example\n * ```ts\n * const queue = createQueue({ concurrency: 3 });\n * const result = await queue.add(() => fetch('/api'), { priority: 1 });\n * await queue.onIdle();\n * ```\n */\nexport function createQueue(options?: QueueOptions): AsyncQueue {\n const { concurrency = 1, autoStart = true, timeout = 0 } = options ?? {};\n\n if (concurrency < 1) throw new RangeError('concurrency must be >= 1');\n\n let paused = !autoStart;\n let active = 0;\n const entries: QueueEntry[] = [];\n const emptyCallbacks: Array<() => void> = [];\n const idleCallbacks: Array<() => void> = [];\n\n function notifyIdle(): void {\n if (active === 0 && entries.length === 0) {\n for (const cb of idleCallbacks.splice(0)) cb();\n }\n }\n\n function notifyEmpty(): void {\n if (entries.length === 0) {\n for (const cb of emptyCallbacks.splice(0)) cb();\n }\n }\n\n function tryRun(): void {\n if (paused) return;\n\n while (active < concurrency && entries.length > 0) {\n const entry = entries.shift()!;\n active++;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const run = async () => {\n try {\n let result: any;\n if (timeout > 0) {\n result = await Promise.race([\n entry.fn(),\n new Promise<never>((_, rej) => {\n timer = setTimeout(() => rej(new Error('Queue task timeout')), timeout);\n }),\n ]);\n } else {\n result = await entry.fn();\n }\n if (timer) clearTimeout(timer);\n entry.resolve(result);\n } catch (error) {\n if (timer) clearTimeout(timer);\n entry.reject(error instanceof Error ? error : new Error(String(error)));\n } finally {\n active--;\n notifyEmpty();\n tryRun();\n notifyIdle();\n }\n };\n\n run();\n }\n\n notifyEmpty();\n notifyIdle();\n }\n\n function add<T>(fn: () => Promise<T>, addOptions?: QueueAddOptions): Promise<T> {\n const priority = addOptions?.priority ?? 0;\n\n return new Promise<T>((resolve, reject) => {\n const entry: QueueEntry = { fn, priority, resolve, reject };\n\n // Insert sorted by priority (lower = higher priority)\n let inserted = false;\n for (let i = 0; i < entries.length; i++) {\n if (priority < entries[i].priority) {\n entries.splice(i, 0, entry);\n inserted = true;\n break;\n }\n }\n if (!inserted) {\n entries.push(entry);\n }\n\n tryRun();\n });\n }\n\n function addAll<T>(fns: Array<() => Promise<T>>, addOptions?: QueueAddOptions): Promise<T[]> {\n return Promise.all(fns.map((fn) => add(fn, addOptions)));\n }\n\n function pause(): void {\n paused = true;\n }\n\n function resume(): void {\n paused = false;\n tryRun();\n }\n\n function clear(): void {\n for (const entry of entries.splice(0)) {\n entry.reject(new Error('Queue cleared'));\n }\n }\n\n function onEmpty(): Promise<void> {\n if (entries.length === 0) return Promise.resolve();\n return new Promise<void>((resolve) => emptyCallbacks.push(resolve));\n }\n\n function onIdle(): Promise<void> {\n if (active === 0 && entries.length === 0) return Promise.resolve();\n return new Promise<void>((resolve) => idleCallbacks.push(resolve));\n }\n\n return {\n add,\n addAll,\n pause,\n resume,\n clear,\n onEmpty,\n onIdle,\n get size() {\n return entries.length;\n },\n get pending() {\n return active;\n },\n get isPaused() {\n return paused;\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
interface RateLimitOptions {
|
|
2
|
+
/** Maximum number of executions per interval */
|
|
3
|
+
limit: number;
|
|
4
|
+
/** Time window in ms */
|
|
5
|
+
interval: number;
|
|
6
|
+
/** Whether to reject or queue when limit is exceeded (default: 'queue') */
|
|
7
|
+
strategy?: 'queue' | 'reject';
|
|
8
|
+
}
|
|
9
|
+
interface RateLimiter {
|
|
10
|
+
/** Execute a function within the rate limit */
|
|
11
|
+
execute: <T>(fn: () => T | Promise<T>) => Promise<T>;
|
|
12
|
+
/** Reset the rate limiter state */
|
|
13
|
+
reset: () => void;
|
|
14
|
+
/** Number of remaining tokens in current window */
|
|
15
|
+
readonly remaining: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a rate limiter using the token bucket algorithm.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const limiter = createRateLimiter({ limit: 10, interval: 1000 });
|
|
23
|
+
* await limiter.execute(() => fetch('/api'));
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
declare function createRateLimiter(options: RateLimitOptions): RateLimiter;
|
|
27
|
+
|
|
28
|
+
export { type RateLimitOptions, type RateLimiter, createRateLimiter };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
interface RateLimitOptions {
|
|
2
|
+
/** Maximum number of executions per interval */
|
|
3
|
+
limit: number;
|
|
4
|
+
/** Time window in ms */
|
|
5
|
+
interval: number;
|
|
6
|
+
/** Whether to reject or queue when limit is exceeded (default: 'queue') */
|
|
7
|
+
strategy?: 'queue' | 'reject';
|
|
8
|
+
}
|
|
9
|
+
interface RateLimiter {
|
|
10
|
+
/** Execute a function within the rate limit */
|
|
11
|
+
execute: <T>(fn: () => T | Promise<T>) => Promise<T>;
|
|
12
|
+
/** Reset the rate limiter state */
|
|
13
|
+
reset: () => void;
|
|
14
|
+
/** Number of remaining tokens in current window */
|
|
15
|
+
readonly remaining: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a rate limiter using the token bucket algorithm.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const limiter = createRateLimiter({ limit: 10, interval: 1000 });
|
|
23
|
+
* await limiter.execute(() => fetch('/api'));
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
declare function createRateLimiter(options: RateLimitOptions): RateLimiter;
|
|
27
|
+
|
|
28
|
+
export { type RateLimitOptions, type RateLimiter, createRateLimiter };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/types.ts
|
|
4
|
+
var FlowXError = class extends Error {
|
|
5
|
+
constructor(message, code) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "FlowXError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var RateLimitError = class extends FlowXError {
|
|
13
|
+
constructor(message = "Rate limit exceeded") {
|
|
14
|
+
super(message, "ERR_RATE_LIMIT");
|
|
15
|
+
this.name = "RateLimitError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/rate-limit.ts
|
|
20
|
+
function createRateLimiter(options) {
|
|
21
|
+
const { limit, interval, strategy = "queue" } = options;
|
|
22
|
+
if (limit < 1) throw new RangeError("limit must be >= 1");
|
|
23
|
+
if (interval < 1) throw new RangeError("interval must be >= 1");
|
|
24
|
+
let tokens = limit;
|
|
25
|
+
let lastRefill = Date.now();
|
|
26
|
+
const queue = [];
|
|
27
|
+
let drainTimer = null;
|
|
28
|
+
function refill() {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const elapsed = now - lastRefill;
|
|
31
|
+
const refillCount = Math.floor(elapsed / interval) * limit;
|
|
32
|
+
if (refillCount > 0) {
|
|
33
|
+
tokens = Math.min(limit, tokens + refillCount);
|
|
34
|
+
lastRefill = now;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function drainQueue() {
|
|
38
|
+
refill();
|
|
39
|
+
while (queue.length > 0 && tokens > 0) {
|
|
40
|
+
tokens--;
|
|
41
|
+
const item = queue.shift();
|
|
42
|
+
item.resolve();
|
|
43
|
+
}
|
|
44
|
+
if (queue.length > 0) {
|
|
45
|
+
scheduleDrain();
|
|
46
|
+
} else {
|
|
47
|
+
drainTimer = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function scheduleDrain() {
|
|
51
|
+
if (drainTimer) return;
|
|
52
|
+
const timeToNextToken = interval / limit;
|
|
53
|
+
drainTimer = setTimeout(() => {
|
|
54
|
+
drainTimer = null;
|
|
55
|
+
drainQueue();
|
|
56
|
+
}, timeToNextToken);
|
|
57
|
+
}
|
|
58
|
+
async function execute(fn) {
|
|
59
|
+
refill();
|
|
60
|
+
if (tokens > 0) {
|
|
61
|
+
tokens--;
|
|
62
|
+
return await fn();
|
|
63
|
+
}
|
|
64
|
+
if (strategy === "reject") {
|
|
65
|
+
throw new RateLimitError();
|
|
66
|
+
}
|
|
67
|
+
await new Promise((resolve) => {
|
|
68
|
+
queue.push({ resolve });
|
|
69
|
+
scheduleDrain();
|
|
70
|
+
});
|
|
71
|
+
return await fn();
|
|
72
|
+
}
|
|
73
|
+
function reset() {
|
|
74
|
+
tokens = limit;
|
|
75
|
+
lastRefill = Date.now();
|
|
76
|
+
if (drainTimer) {
|
|
77
|
+
clearTimeout(drainTimer);
|
|
78
|
+
drainTimer = null;
|
|
79
|
+
}
|
|
80
|
+
drainQueue();
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
execute,
|
|
84
|
+
reset,
|
|
85
|
+
get remaining() {
|
|
86
|
+
refill();
|
|
87
|
+
return tokens;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
exports.createRateLimiter = createRateLimiter;
|
|
93
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
94
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/rate-limit.ts"],"names":[],"mappings":"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","file":"rate-limit.js","sourcesContent":["// ============================================================================\n// FlowX — Types & Error Hierarchy\n// ============================================================================\n\n/** Generic async function signature */\nexport type AsyncFn<TArgs extends any[] = any[], TReturn = any> = (\n ...args: TArgs\n) => Promise<TReturn>;\n\n/** Backoff strategy for retry/poll operations */\nexport type BackoffStrategy =\n | 'fixed'\n | 'linear'\n | 'exponential'\n | ((attempt: number, delay: number) => number);\n\n// ── Error Classes ───────────────────────────────────────────────────────────\n\n/** Base error class for all FlowX errors */\nexport class FlowXError extends Error {\n public readonly code: string;\n constructor(message: string, code: string) {\n super(message);\n this.name = 'FlowXError';\n this.code = code;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a promise exceeds its timeout */\nexport class TimeoutError extends FlowXError {\n constructor(message = 'Operation timed out') {\n super(message, 'ERR_TIMEOUT');\n this.name = 'TimeoutError';\n }\n}\n\n/** Thrown when a circuit breaker is open */\nexport class CircuitBreakerError extends FlowXError {\n constructor(message = 'Circuit breaker is open') {\n super(message, 'ERR_CIRCUIT_OPEN');\n this.name = 'CircuitBreakerError';\n }\n}\n\n/** Thrown when a bulkhead rejects due to capacity */\nexport class BulkheadError extends FlowXError {\n constructor(message = 'Bulkhead capacity exceeded') {\n super(message, 'ERR_BULKHEAD_FULL');\n this.name = 'BulkheadError';\n }\n}\n\n/** Thrown when an operation is aborted */\nexport class AbortError extends FlowXError {\n constructor(message = 'Operation aborted') {\n super(message, 'ERR_ABORTED');\n this.name = 'AbortError';\n }\n}\n\n/** Thrown when rate limit is exceeded */\nexport class RateLimitError extends FlowXError {\n constructor(message = 'Rate limit exceeded') {\n super(message, 'ERR_RATE_LIMIT');\n this.name = 'RateLimitError';\n }\n}\n\n// ── Utility Helpers ─────────────────────────────────────────────────────────\n\n/** Sleep for the specified duration, respecting AbortSignal */\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new AbortError());\n return;\n }\n\n let onAbort: (() => void) | undefined;\n\n const timer = setTimeout(() => {\n if (signal && onAbort) {\n signal.removeEventListener('abort', onAbort);\n }\n resolve();\n }, ms);\n\n if (signal) {\n onAbort = () => {\n clearTimeout(timer);\n reject(new AbortError());\n };\n signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n}\n\n/** Calculate delay based on backoff strategy */\nexport function calculateDelay(\n attempt: number,\n baseDelay: number,\n strategy: BackoffStrategy,\n jitter: boolean | number = false,\n): number {\n let delay: number;\n\n if (typeof strategy === 'function') {\n delay = strategy(attempt, baseDelay);\n } else {\n switch (strategy) {\n case 'fixed':\n delay = baseDelay;\n break;\n case 'linear':\n delay = baseDelay * attempt;\n break;\n case 'exponential':\n delay = baseDelay * Math.pow(2, attempt - 1);\n break;\n }\n }\n\n if (jitter) {\n const factor = typeof jitter === 'number' ? jitter : 1;\n delay = delay * (1 - factor * 0.5 + Math.random() * factor);\n }\n\n return Math.max(0, Math.floor(delay));\n}\n","// ============================================================================\n// FlowX — Token Bucket Rate Limiter\n// ============================================================================\nimport { RateLimitError } from './types';\n\nexport interface RateLimitOptions {\n /** Maximum number of executions per interval */\n limit: number;\n /** Time window in ms */\n interval: number;\n /** Whether to reject or queue when limit is exceeded (default: 'queue') */\n strategy?: 'queue' | 'reject';\n}\n\nexport interface RateLimiter {\n /** Execute a function within the rate limit */\n execute: <T>(fn: () => T | Promise<T>) => Promise<T>;\n /** Reset the rate limiter state */\n reset: () => void;\n /** Number of remaining tokens in current window */\n readonly remaining: number;\n}\n\n/**\n * Create a rate limiter using the token bucket algorithm.\n *\n * @example\n * ```ts\n * const limiter = createRateLimiter({ limit: 10, interval: 1000 });\n * await limiter.execute(() => fetch('/api'));\n * ```\n */\nexport function createRateLimiter(options: RateLimitOptions): RateLimiter {\n const { limit, interval, strategy = 'queue' } = options;\n\n if (limit < 1) throw new RangeError('limit must be >= 1');\n if (interval < 1) throw new RangeError('interval must be >= 1');\n\n let tokens = limit;\n let lastRefill = Date.now();\n const queue: Array<{ resolve: () => void }> = [];\n let drainTimer: ReturnType<typeof setTimeout> | null = null;\n\n function refill(): void {\n const now = Date.now();\n const elapsed = now - lastRefill;\n const refillCount = Math.floor(elapsed / interval) * limit;\n\n if (refillCount > 0) {\n tokens = Math.min(limit, tokens + refillCount);\n lastRefill = now;\n }\n }\n\n function drainQueue(): void {\n refill();\n while (queue.length > 0 && tokens > 0) {\n tokens--;\n const item = queue.shift()!;\n item.resolve();\n }\n\n if (queue.length > 0) {\n scheduleDrain();\n } else {\n drainTimer = null;\n }\n }\n\n function scheduleDrain(): void {\n if (drainTimer) return;\n const timeToNextToken = interval / limit;\n drainTimer = setTimeout(() => {\n drainTimer = null;\n drainQueue();\n }, timeToNextToken);\n }\n\n async function execute<T>(fn: () => T | Promise<T>): Promise<T> {\n refill();\n\n if (tokens > 0) {\n tokens--;\n return await fn();\n }\n\n if (strategy === 'reject') {\n throw new RateLimitError();\n }\n\n // Queue strategy — wait for a token\n await new Promise<void>((resolve) => {\n queue.push({ resolve });\n scheduleDrain();\n });\n\n return await fn();\n }\n\n function reset(): void {\n tokens = limit;\n lastRefill = Date.now();\n if (drainTimer) {\n clearTimeout(drainTimer);\n drainTimer = null;\n }\n drainQueue();\n }\n\n return {\n execute,\n reset,\n get remaining() {\n refill();\n return tokens;\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var FlowXError = class extends Error {
|
|
3
|
+
constructor(message, code) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "FlowXError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var RateLimitError = class extends FlowXError {
|
|
11
|
+
constructor(message = "Rate limit exceeded") {
|
|
12
|
+
super(message, "ERR_RATE_LIMIT");
|
|
13
|
+
this.name = "RateLimitError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/rate-limit.ts
|
|
18
|
+
function createRateLimiter(options) {
|
|
19
|
+
const { limit, interval, strategy = "queue" } = options;
|
|
20
|
+
if (limit < 1) throw new RangeError("limit must be >= 1");
|
|
21
|
+
if (interval < 1) throw new RangeError("interval must be >= 1");
|
|
22
|
+
let tokens = limit;
|
|
23
|
+
let lastRefill = Date.now();
|
|
24
|
+
const queue = [];
|
|
25
|
+
let drainTimer = null;
|
|
26
|
+
function refill() {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const elapsed = now - lastRefill;
|
|
29
|
+
const refillCount = Math.floor(elapsed / interval) * limit;
|
|
30
|
+
if (refillCount > 0) {
|
|
31
|
+
tokens = Math.min(limit, tokens + refillCount);
|
|
32
|
+
lastRefill = now;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function drainQueue() {
|
|
36
|
+
refill();
|
|
37
|
+
while (queue.length > 0 && tokens > 0) {
|
|
38
|
+
tokens--;
|
|
39
|
+
const item = queue.shift();
|
|
40
|
+
item.resolve();
|
|
41
|
+
}
|
|
42
|
+
if (queue.length > 0) {
|
|
43
|
+
scheduleDrain();
|
|
44
|
+
} else {
|
|
45
|
+
drainTimer = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function scheduleDrain() {
|
|
49
|
+
if (drainTimer) return;
|
|
50
|
+
const timeToNextToken = interval / limit;
|
|
51
|
+
drainTimer = setTimeout(() => {
|
|
52
|
+
drainTimer = null;
|
|
53
|
+
drainQueue();
|
|
54
|
+
}, timeToNextToken);
|
|
55
|
+
}
|
|
56
|
+
async function execute(fn) {
|
|
57
|
+
refill();
|
|
58
|
+
if (tokens > 0) {
|
|
59
|
+
tokens--;
|
|
60
|
+
return await fn();
|
|
61
|
+
}
|
|
62
|
+
if (strategy === "reject") {
|
|
63
|
+
throw new RateLimitError();
|
|
64
|
+
}
|
|
65
|
+
await new Promise((resolve) => {
|
|
66
|
+
queue.push({ resolve });
|
|
67
|
+
scheduleDrain();
|
|
68
|
+
});
|
|
69
|
+
return await fn();
|
|
70
|
+
}
|
|
71
|
+
function reset() {
|
|
72
|
+
tokens = limit;
|
|
73
|
+
lastRefill = Date.now();
|
|
74
|
+
if (drainTimer) {
|
|
75
|
+
clearTimeout(drainTimer);
|
|
76
|
+
drainTimer = null;
|
|
77
|
+
}
|
|
78
|
+
drainQueue();
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
execute,
|
|
82
|
+
reset,
|
|
83
|
+
get remaining() {
|
|
84
|
+
refill();
|
|
85
|
+
return tokens;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { createRateLimiter };
|
|
91
|
+
//# sourceMappingURL=rate-limit.mjs.map
|
|
92
|
+
//# sourceMappingURL=rate-limit.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/rate-limit.ts"],"names":[],"mappings":"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","file":"rate-limit.mjs","sourcesContent":["// ============================================================================\n// FlowX — Types & Error Hierarchy\n// ============================================================================\n\n/** Generic async function signature */\nexport type AsyncFn<TArgs extends any[] = any[], TReturn = any> = (\n ...args: TArgs\n) => Promise<TReturn>;\n\n/** Backoff strategy for retry/poll operations */\nexport type BackoffStrategy =\n | 'fixed'\n | 'linear'\n | 'exponential'\n | ((attempt: number, delay: number) => number);\n\n// ── Error Classes ───────────────────────────────────────────────────────────\n\n/** Base error class for all FlowX errors */\nexport class FlowXError extends Error {\n public readonly code: string;\n constructor(message: string, code: string) {\n super(message);\n this.name = 'FlowXError';\n this.code = code;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a promise exceeds its timeout */\nexport class TimeoutError extends FlowXError {\n constructor(message = 'Operation timed out') {\n super(message, 'ERR_TIMEOUT');\n this.name = 'TimeoutError';\n }\n}\n\n/** Thrown when a circuit breaker is open */\nexport class CircuitBreakerError extends FlowXError {\n constructor(message = 'Circuit breaker is open') {\n super(message, 'ERR_CIRCUIT_OPEN');\n this.name = 'CircuitBreakerError';\n }\n}\n\n/** Thrown when a bulkhead rejects due to capacity */\nexport class BulkheadError extends FlowXError {\n constructor(message = 'Bulkhead capacity exceeded') {\n super(message, 'ERR_BULKHEAD_FULL');\n this.name = 'BulkheadError';\n }\n}\n\n/** Thrown when an operation is aborted */\nexport class AbortError extends FlowXError {\n constructor(message = 'Operation aborted') {\n super(message, 'ERR_ABORTED');\n this.name = 'AbortError';\n }\n}\n\n/** Thrown when rate limit is exceeded */\nexport class RateLimitError extends FlowXError {\n constructor(message = 'Rate limit exceeded') {\n super(message, 'ERR_RATE_LIMIT');\n this.name = 'RateLimitError';\n }\n}\n\n// ── Utility Helpers ─────────────────────────────────────────────────────────\n\n/** Sleep for the specified duration, respecting AbortSignal */\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new AbortError());\n return;\n }\n\n let onAbort: (() => void) | undefined;\n\n const timer = setTimeout(() => {\n if (signal && onAbort) {\n signal.removeEventListener('abort', onAbort);\n }\n resolve();\n }, ms);\n\n if (signal) {\n onAbort = () => {\n clearTimeout(timer);\n reject(new AbortError());\n };\n signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n}\n\n/** Calculate delay based on backoff strategy */\nexport function calculateDelay(\n attempt: number,\n baseDelay: number,\n strategy: BackoffStrategy,\n jitter: boolean | number = false,\n): number {\n let delay: number;\n\n if (typeof strategy === 'function') {\n delay = strategy(attempt, baseDelay);\n } else {\n switch (strategy) {\n case 'fixed':\n delay = baseDelay;\n break;\n case 'linear':\n delay = baseDelay * attempt;\n break;\n case 'exponential':\n delay = baseDelay * Math.pow(2, attempt - 1);\n break;\n }\n }\n\n if (jitter) {\n const factor = typeof jitter === 'number' ? jitter : 1;\n delay = delay * (1 - factor * 0.5 + Math.random() * factor);\n }\n\n return Math.max(0, Math.floor(delay));\n}\n","// ============================================================================\n// FlowX — Token Bucket Rate Limiter\n// ============================================================================\nimport { RateLimitError } from './types';\n\nexport interface RateLimitOptions {\n /** Maximum number of executions per interval */\n limit: number;\n /** Time window in ms */\n interval: number;\n /** Whether to reject or queue when limit is exceeded (default: 'queue') */\n strategy?: 'queue' | 'reject';\n}\n\nexport interface RateLimiter {\n /** Execute a function within the rate limit */\n execute: <T>(fn: () => T | Promise<T>) => Promise<T>;\n /** Reset the rate limiter state */\n reset: () => void;\n /** Number of remaining tokens in current window */\n readonly remaining: number;\n}\n\n/**\n * Create a rate limiter using the token bucket algorithm.\n *\n * @example\n * ```ts\n * const limiter = createRateLimiter({ limit: 10, interval: 1000 });\n * await limiter.execute(() => fetch('/api'));\n * ```\n */\nexport function createRateLimiter(options: RateLimitOptions): RateLimiter {\n const { limit, interval, strategy = 'queue' } = options;\n\n if (limit < 1) throw new RangeError('limit must be >= 1');\n if (interval < 1) throw new RangeError('interval must be >= 1');\n\n let tokens = limit;\n let lastRefill = Date.now();\n const queue: Array<{ resolve: () => void }> = [];\n let drainTimer: ReturnType<typeof setTimeout> | null = null;\n\n function refill(): void {\n const now = Date.now();\n const elapsed = now - lastRefill;\n const refillCount = Math.floor(elapsed / interval) * limit;\n\n if (refillCount > 0) {\n tokens = Math.min(limit, tokens + refillCount);\n lastRefill = now;\n }\n }\n\n function drainQueue(): void {\n refill();\n while (queue.length > 0 && tokens > 0) {\n tokens--;\n const item = queue.shift()!;\n item.resolve();\n }\n\n if (queue.length > 0) {\n scheduleDrain();\n } else {\n drainTimer = null;\n }\n }\n\n function scheduleDrain(): void {\n if (drainTimer) return;\n const timeToNextToken = interval / limit;\n drainTimer = setTimeout(() => {\n drainTimer = null;\n drainQueue();\n }, timeToNextToken);\n }\n\n async function execute<T>(fn: () => T | Promise<T>): Promise<T> {\n refill();\n\n if (tokens > 0) {\n tokens--;\n return await fn();\n }\n\n if (strategy === 'reject') {\n throw new RateLimitError();\n }\n\n // Queue strategy — wait for a token\n await new Promise<void>((resolve) => {\n queue.push({ resolve });\n scheduleDrain();\n });\n\n return await fn();\n }\n\n function reset(): void {\n tokens = limit;\n lastRefill = Date.now();\n if (drainTimer) {\n clearTimeout(drainTimer);\n drainTimer = null;\n }\n drainQueue();\n }\n\n return {\n execute,\n reset,\n get remaining() {\n refill();\n return tokens;\n },\n };\n}\n"]}
|
package/dist/retry.d.mts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { B as BackoffStrategy } from './types-BsCO2J40.mjs';
|
|
2
|
+
|
|
3
|
+
interface RetryOptions {
|
|
4
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
5
|
+
retries?: number;
|
|
6
|
+
/** Base delay in ms between retries (default: 1000) */
|
|
7
|
+
delay?: number;
|
|
8
|
+
/** Backoff strategy (default: 'exponential') */
|
|
9
|
+
backoff?: BackoffStrategy;
|
|
10
|
+
/** Add randomized jitter to delays (default: false) */
|
|
11
|
+
jitter?: boolean | number;
|
|
12
|
+
/** Maximum delay cap in ms (default: 30000) */
|
|
13
|
+
maxDelay?: number;
|
|
14
|
+
/** Custom predicate to decide if retry should happen */
|
|
15
|
+
shouldRetry?: (error: Error, attempt: number) => boolean | Promise<boolean>;
|
|
16
|
+
/** Callback fired before each retry */
|
|
17
|
+
onRetry?: (error: Error, attempt: number) => void | Promise<void>;
|
|
18
|
+
/** AbortSignal for cancellation */
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Execute an async function with automatic retry and configurable backoff.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const data = await retry(() => fetch('/api/data'), {
|
|
27
|
+
* retries: 5,
|
|
28
|
+
* backoff: 'exponential',
|
|
29
|
+
* jitter: true,
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare function retry<T>(fn: () => T | Promise<T>, options?: RetryOptions): Promise<T>;
|
|
34
|
+
/**
|
|
35
|
+
* Wrap a function to automatically retry on failure.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const safeFetch = retryable(fetch, { retries: 3 });
|
|
40
|
+
* const data = await safeFetch('/api/data');
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
declare function retryable<TArgs extends any[], TReturn>(fn: (...args: TArgs) => Promise<TReturn>, options?: RetryOptions): (...args: TArgs) => Promise<TReturn>;
|
|
44
|
+
|
|
45
|
+
export { type RetryOptions, retry, retryable };
|
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { B as BackoffStrategy } from './types-BsCO2J40.js';
|
|
2
|
+
|
|
3
|
+
interface RetryOptions {
|
|
4
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
5
|
+
retries?: number;
|
|
6
|
+
/** Base delay in ms between retries (default: 1000) */
|
|
7
|
+
delay?: number;
|
|
8
|
+
/** Backoff strategy (default: 'exponential') */
|
|
9
|
+
backoff?: BackoffStrategy;
|
|
10
|
+
/** Add randomized jitter to delays (default: false) */
|
|
11
|
+
jitter?: boolean | number;
|
|
12
|
+
/** Maximum delay cap in ms (default: 30000) */
|
|
13
|
+
maxDelay?: number;
|
|
14
|
+
/** Custom predicate to decide if retry should happen */
|
|
15
|
+
shouldRetry?: (error: Error, attempt: number) => boolean | Promise<boolean>;
|
|
16
|
+
/** Callback fired before each retry */
|
|
17
|
+
onRetry?: (error: Error, attempt: number) => void | Promise<void>;
|
|
18
|
+
/** AbortSignal for cancellation */
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Execute an async function with automatic retry and configurable backoff.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const data = await retry(() => fetch('/api/data'), {
|
|
27
|
+
* retries: 5,
|
|
28
|
+
* backoff: 'exponential',
|
|
29
|
+
* jitter: true,
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare function retry<T>(fn: () => T | Promise<T>, options?: RetryOptions): Promise<T>;
|
|
34
|
+
/**
|
|
35
|
+
* Wrap a function to automatically retry on failure.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const safeFetch = retryable(fetch, { retries: 3 });
|
|
40
|
+
* const data = await safeFetch('/api/data');
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
declare function retryable<TArgs extends any[], TReturn>(fn: (...args: TArgs) => Promise<TReturn>, options?: RetryOptions): (...args: TArgs) => Promise<TReturn>;
|
|
44
|
+
|
|
45
|
+
export { type RetryOptions, retry, retryable };
|