@asaidimu/utils-sync 1.1.0 → 2.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/index.d.mts +364 -12
- package/index.d.ts +364 -12
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/package.json +1 -1
package/index.d.mts
CHANGED
|
@@ -11,14 +11,14 @@ interface MutexOptions {
|
|
|
11
11
|
* - `"macrotask"` (default): Uses setTimeout(fn, 0) to yield to the event
|
|
12
12
|
* loop between handoffs. Prevents microtask starvation under heavy
|
|
13
13
|
* contention — I/O, rendering, and other macrotasks can run between
|
|
14
|
-
* lock acquisitions. Use for
|
|
15
|
-
*
|
|
14
|
+
* lock acquisitions. Use for Serializer and other coarse-grained
|
|
15
|
+
* serializers.
|
|
16
16
|
*
|
|
17
17
|
* - `"microtask"`: Uses queueMicrotask(fn) for handoff. Near-zero latency
|
|
18
18
|
* between acquisitions — no macrotask delay. Safe when you need
|
|
19
19
|
* back-to-back operations to complete as fast as possible and starvation
|
|
20
|
-
* is not a concern (e.g.
|
|
21
|
-
*
|
|
20
|
+
* is not a concern (e.g. Once, Semaphore where builds are infrequent
|
|
21
|
+
* and latency matters more than fairness).
|
|
22
22
|
*/
|
|
23
23
|
yieldMode?: "macrotask" | "microtask";
|
|
24
24
|
}
|
|
@@ -51,6 +51,11 @@ declare class Mutex {
|
|
|
51
51
|
tryLock(): boolean;
|
|
52
52
|
/**
|
|
53
53
|
* Releases the lock, scheduling the next waiter according to yieldMode.
|
|
54
|
+
*
|
|
55
|
+
* When a waiter exists, `_locked` intentionally remains `true` — ownership
|
|
56
|
+
* transfers directly to the next waiter without ever clearing the flag.
|
|
57
|
+
* Only when the queue is empty is `_locked` set to false.
|
|
58
|
+
*
|
|
54
59
|
* @throws {Error} If the mutex is not currently locked.
|
|
55
60
|
*/
|
|
56
61
|
unlock(): void;
|
|
@@ -59,6 +64,61 @@ declare class Mutex {
|
|
|
59
64
|
/** Returns the number of operations waiting for the lock. */
|
|
60
65
|
pending(): number;
|
|
61
66
|
}
|
|
67
|
+
interface SemaphoreOptions extends MutexOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Number of concurrent holders allowed.
|
|
70
|
+
* Inherits `capacity` and `yieldMode` from MutexOptions.
|
|
71
|
+
* @default 1 (equivalent to a Mutex)
|
|
72
|
+
*/
|
|
73
|
+
slots?: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A counting semaphore — generalises Mutex to allow up to N concurrent holders.
|
|
77
|
+
*
|
|
78
|
+
* Useful for rate-limiting concurrency: e.g. "at most 3 in-flight HTTP requests".
|
|
79
|
+
* With slots=1 it behaves identically to Mutex.
|
|
80
|
+
*
|
|
81
|
+
* Accepts the same `capacity` and `yieldMode` options as Mutex, plus `slots`.
|
|
82
|
+
*/
|
|
83
|
+
declare class Semaphore {
|
|
84
|
+
private _slots;
|
|
85
|
+
private _available;
|
|
86
|
+
private _capacity;
|
|
87
|
+
private _yieldMode;
|
|
88
|
+
private waiters;
|
|
89
|
+
constructor(options?: SemaphoreOptions);
|
|
90
|
+
/**
|
|
91
|
+
* Acquires one slot. If all slots are taken, waits until one is released.
|
|
92
|
+
*
|
|
93
|
+
* @param timeout - Optional maximum wait time in milliseconds.
|
|
94
|
+
* @throws {TimeoutError} If a slot cannot be acquired within the timeout.
|
|
95
|
+
* @throws {Error} If the wait queue is full.
|
|
96
|
+
*/
|
|
97
|
+
acquire(timeout?: number): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Attempts to acquire a slot without waiting.
|
|
100
|
+
* @returns `true` if a slot was acquired, `false` otherwise.
|
|
101
|
+
*/
|
|
102
|
+
tryAcquire(): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Releases one slot. If waiters are queued, the next one is scheduled
|
|
105
|
+
* according to yieldMode.
|
|
106
|
+
*
|
|
107
|
+
* @throws {Error} If no slot is currently held (release without acquire).
|
|
108
|
+
*/
|
|
109
|
+
release(): void;
|
|
110
|
+
/**
|
|
111
|
+
* Runs a function with one acquired slot, releasing it when done.
|
|
112
|
+
* Convenience wrapper — prefer this over manual acquire/release.
|
|
113
|
+
*/
|
|
114
|
+
run<T>(fn: () => Promise<T> | T, timeout?: number): Promise<T>;
|
|
115
|
+
/** Number of slots currently free. */
|
|
116
|
+
available(): number;
|
|
117
|
+
/** Number of callers waiting for a slot. */
|
|
118
|
+
pending(): number;
|
|
119
|
+
/** Total slot count (as configured). */
|
|
120
|
+
slots(): number;
|
|
121
|
+
}
|
|
62
122
|
type OnceResult<T> = {
|
|
63
123
|
value: T | null;
|
|
64
124
|
error?: unknown;
|
|
@@ -92,14 +152,28 @@ declare class Once<T = void> {
|
|
|
92
152
|
* @param timeout - Max wait time in ms (includes lock wait + execution time).
|
|
93
153
|
*/
|
|
94
154
|
do(fn: () => Promise<T> | T, timeout?: number): Promise<OnceResult<T>>;
|
|
155
|
+
/**
|
|
156
|
+
* Executes the function synchronously if it hasn't been executed yet.
|
|
157
|
+
* If an async operation is pending or the lock is contended, it will fail immediately.
|
|
158
|
+
* The failure is returned as an error state, OR thrown if `throws: true` is configured.
|
|
159
|
+
*
|
|
160
|
+
* @param fn - The synchronous function to execute.
|
|
161
|
+
* @returns The result of the execution.
|
|
162
|
+
*/
|
|
163
|
+
doSync(fn: () => T): OnceResult<T>;
|
|
95
164
|
/**
|
|
96
165
|
* Returns true if the operation has completed (success or non-retryable failure)
|
|
97
|
-
* and
|
|
98
|
-
*
|
|
166
|
+
* and the internal promise has been cleared.
|
|
167
|
+
*
|
|
168
|
+
* This is the canonical "safe to read" check — prefer it over `done()`.
|
|
99
169
|
*/
|
|
100
170
|
isReady(): boolean;
|
|
101
171
|
/**
|
|
102
172
|
* Returns true if the operation is currently executing.
|
|
173
|
+
*
|
|
174
|
+
* Uses isReady() as the canonical check: running is its logical complement
|
|
175
|
+
* when a promise is in flight. This correctly handles the brief window where
|
|
176
|
+
* _done=true but the promise hasn't been cleared yet in finally.
|
|
103
177
|
*/
|
|
104
178
|
running(): boolean;
|
|
105
179
|
/**
|
|
@@ -113,16 +187,27 @@ declare class Once<T = void> {
|
|
|
113
187
|
get(): T | null;
|
|
114
188
|
/**
|
|
115
189
|
* Resets the instance, allowing the action to run again on the next call.
|
|
190
|
+
*
|
|
191
|
+
* @throws {Error} If called while an operation is currently executing,
|
|
192
|
+
* as this would leave the in-flight promise in an inconsistent state.
|
|
116
193
|
*/
|
|
117
194
|
reset(): void;
|
|
118
195
|
/**
|
|
119
|
-
* Returns the
|
|
196
|
+
* Returns the in-flight execution promise if one is currently running, null otherwise.
|
|
197
|
+
*
|
|
198
|
+
* Use this to join an in-progress operation without triggering a new one.
|
|
120
199
|
*/
|
|
121
|
-
|
|
200
|
+
currentExecution(): Promise<OnceResult<T>> | null;
|
|
122
201
|
/**
|
|
123
202
|
* Returns true if the operation has finished (success or final failure).
|
|
124
203
|
*/
|
|
125
204
|
done(): boolean;
|
|
205
|
+
/**
|
|
206
|
+
* Awaits a promise with an optional timeout.
|
|
207
|
+
*
|
|
208
|
+
* We clear the timer on the winning branch, consistent with the
|
|
209
|
+
* Mutex/Semaphore/Latch timeout pattern throughout this module.
|
|
210
|
+
*/
|
|
126
211
|
private _awaitWithTimeout;
|
|
127
212
|
}
|
|
128
213
|
type SerializerResult<T> = {
|
|
@@ -137,8 +222,8 @@ interface SerializerOptions {
|
|
|
137
222
|
capacity?: number;
|
|
138
223
|
/**
|
|
139
224
|
* Yield mode for the internal mutex. Defaults to "macrotask" for
|
|
140
|
-
* coarse-grained serializers
|
|
141
|
-
*
|
|
225
|
+
* coarse-grained serializers. Use "microtask" for fine-grained
|
|
226
|
+
* latency-sensitive serializers.
|
|
142
227
|
*/
|
|
143
228
|
yieldMode?: "macrotask" | "microtask";
|
|
144
229
|
}
|
|
@@ -152,6 +237,7 @@ declare class Serializer<T = void> {
|
|
|
152
237
|
private _done;
|
|
153
238
|
private _lastValue;
|
|
154
239
|
private _lastError;
|
|
240
|
+
private _hasRun;
|
|
155
241
|
constructor(options?: SerializerOptions);
|
|
156
242
|
/**
|
|
157
243
|
* Enqueue a function to be executed after all previous tasks complete.
|
|
@@ -161,8 +247,18 @@ declare class Serializer<T = void> {
|
|
|
161
247
|
* @returns Object containing the value or error.
|
|
162
248
|
*/
|
|
163
249
|
do(fn: () => Promise<T> | T, timeout?: number): Promise<SerializerResult<T | null>>;
|
|
164
|
-
/**
|
|
250
|
+
/**
|
|
251
|
+
* Returns the result of the last execution.
|
|
252
|
+
*
|
|
253
|
+
* Returns `{ value: null, error: undefined }` both when the serializer has
|
|
254
|
+
* never run and when it ran and returned null — use `hasRun()` to distinguish
|
|
255
|
+
* these two cases.
|
|
256
|
+
*/
|
|
165
257
|
peek(): SerializerResult<T | null>;
|
|
258
|
+
/**
|
|
259
|
+
* Returns true if at least one task has been executed (successfully or not).
|
|
260
|
+
*/
|
|
261
|
+
hasRun(): boolean;
|
|
166
262
|
/**
|
|
167
263
|
* Permanently closes the serializer.
|
|
168
264
|
* Subsequent calls to `do()` will fail immediately with SerializerExecutionDone.
|
|
@@ -173,5 +269,261 @@ declare class Serializer<T = void> {
|
|
|
173
269
|
/** Returns true if a task is currently executing. */
|
|
174
270
|
running(): boolean;
|
|
175
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* A one-shot gate that starts closed and opens exactly once.
|
|
274
|
+
*
|
|
275
|
+
* All current and future `wait()` callers resolve immediately once `open()` is
|
|
276
|
+
* called. Unlike Once, Latch carries no return value and has no retry logic —
|
|
277
|
+
* it is a pure signalling primitive.
|
|
278
|
+
*
|
|
279
|
+
* Typical use: coordinate startup, signal that an initialisation phase is done,
|
|
280
|
+
* or gate downstream work on a single event.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* const ready = new Latch();
|
|
284
|
+
* // Consumer
|
|
285
|
+
* await ready.wait();
|
|
286
|
+
* // Producer (elsewhere)
|
|
287
|
+
* ready.open();
|
|
288
|
+
*/
|
|
289
|
+
declare class Latch {
|
|
290
|
+
private _open;
|
|
291
|
+
private _resolve;
|
|
292
|
+
private _promise;
|
|
293
|
+
constructor();
|
|
294
|
+
/**
|
|
295
|
+
* Opens the latch. All current and future waiters resolve immediately.
|
|
296
|
+
* Calling open() more than once is a no-op.
|
|
297
|
+
*/
|
|
298
|
+
open(): void;
|
|
299
|
+
/**
|
|
300
|
+
* Returns a promise that resolves when the latch is opened.
|
|
301
|
+
* If already open, resolves on the next microtask checkpoint.
|
|
302
|
+
*
|
|
303
|
+
* @param timeout - Optional maximum wait time in milliseconds.
|
|
304
|
+
* @throws {TimeoutError} If the latch does not open within the timeout.
|
|
305
|
+
*/
|
|
306
|
+
wait(timeout?: number): Promise<void>;
|
|
307
|
+
/**
|
|
308
|
+
* Synchronously checks whether the latch has been opened.
|
|
309
|
+
*/
|
|
310
|
+
isOpen(): boolean;
|
|
311
|
+
}
|
|
312
|
+
interface RWMutexOptions {
|
|
313
|
+
/**
|
|
314
|
+
* Controls how lock handoff is scheduled when a waiter is unblocked.
|
|
315
|
+
*
|
|
316
|
+
* - `"microtask"` (default): Uses queueMicrotask(fn). Near-zero latency
|
|
317
|
+
* between handoffs. Appropriate for RWMutex because readers are woken in
|
|
318
|
+
* bulk and writers are rarely contended enough to cause starvation.
|
|
319
|
+
*
|
|
320
|
+
* - `"macrotask"`: Uses setTimeout(fn, 0) to yield to the event loop between
|
|
321
|
+
* handoffs. Use if you observe microtask starvation under extreme write
|
|
322
|
+
* contention on this specific lock.
|
|
323
|
+
*/
|
|
324
|
+
yieldMode?: "macrotask" | "microtask";
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* A read-write mutex with writer preference.
|
|
328
|
+
*
|
|
329
|
+
* - Multiple readers can hold the lock concurrently.
|
|
330
|
+
* - Writers get exclusive access — no readers or other writers.
|
|
331
|
+
* - Writer preference: once a writer is waiting, new readers queue behind it.
|
|
332
|
+
* This prevents writer starvation under read-heavy loads.
|
|
333
|
+
*
|
|
334
|
+
* Readers should call `rlock()` / `runlock()`.
|
|
335
|
+
* Writers should call `lock()` / `unlock()`.
|
|
336
|
+
* Prefer the `read()` and `write()` convenience wrappers to avoid lock leaks.
|
|
337
|
+
*/
|
|
338
|
+
declare class RWMutex {
|
|
339
|
+
private _readers;
|
|
340
|
+
private _writeLocked;
|
|
341
|
+
private _pendingWriters;
|
|
342
|
+
private _yieldMode;
|
|
343
|
+
private readerWaiters;
|
|
344
|
+
private writerWaiters;
|
|
345
|
+
constructor(options?: RWMutexOptions);
|
|
346
|
+
/**
|
|
347
|
+
* Acquires a read lock. Blocks if a writer holds or is waiting for the lock.
|
|
348
|
+
*
|
|
349
|
+
* @param timeout - Optional maximum wait time in milliseconds.
|
|
350
|
+
* @throws {TimeoutError} If the read lock cannot be acquired within the timeout.
|
|
351
|
+
*/
|
|
352
|
+
rlock(timeout?: number): Promise<void>;
|
|
353
|
+
/**
|
|
354
|
+
* Releases a read lock.
|
|
355
|
+
* @throws {Error} If no read lock is currently held.
|
|
356
|
+
*/
|
|
357
|
+
runlock(): void;
|
|
358
|
+
/**
|
|
359
|
+
* Acquires the write lock. Blocks until all current readers and any prior
|
|
360
|
+
* writers have finished.
|
|
361
|
+
*
|
|
362
|
+
* @param timeout - Optional maximum wait time in milliseconds.
|
|
363
|
+
* @throws {TimeoutError} If the write lock cannot be acquired within the timeout.
|
|
364
|
+
*/
|
|
365
|
+
lock(timeout?: number): Promise<void>;
|
|
366
|
+
/**
|
|
367
|
+
* Releases the write lock.
|
|
368
|
+
* Wakes the next pending writer if one exists; otherwise wakes all pending readers.
|
|
369
|
+
*
|
|
370
|
+
* @throws {Error} If no write lock is currently held.
|
|
371
|
+
*/
|
|
372
|
+
unlock(): void;
|
|
373
|
+
/**
|
|
374
|
+
* Runs a function under a read lock, releasing it when done.
|
|
375
|
+
* Prefer this over manual rlock/runlock to avoid lock leaks.
|
|
376
|
+
*/
|
|
377
|
+
read<T>(fn: () => Promise<T> | T, timeout?: number): Promise<T>;
|
|
378
|
+
/**
|
|
379
|
+
* Runs a function under the write lock, releasing it when done.
|
|
380
|
+
* Prefer this over manual lock/unlock to avoid lock leaks.
|
|
381
|
+
*/
|
|
382
|
+
write<T>(fn: () => Promise<T> | T, timeout?: number): Promise<T>;
|
|
383
|
+
/** Returns true if the write lock is currently held. */
|
|
384
|
+
writeLocked(): boolean;
|
|
385
|
+
/** Returns the number of active read lock holders. */
|
|
386
|
+
readers(): number;
|
|
387
|
+
/** Returns the number of writers currently waiting for the lock. */
|
|
388
|
+
pendingWriters(): number;
|
|
389
|
+
/** Returns the number of readers currently waiting for the lock. */
|
|
390
|
+
pendingReaders(): number;
|
|
391
|
+
/**
|
|
392
|
+
* Wakes the next writer if the lock is free and a writer is waiting.
|
|
393
|
+
* Returns true if a writer was woken, false otherwise.
|
|
394
|
+
*/
|
|
395
|
+
private _tryWakeWriter;
|
|
396
|
+
/**
|
|
397
|
+
* Wakes all pending readers (called when a writer releases and no writers are queued).
|
|
398
|
+
*/
|
|
399
|
+
private _wakeAllReaders;
|
|
400
|
+
/**
|
|
401
|
+
* Schedules a waiter callback according to the configured yieldMode.
|
|
402
|
+
*/
|
|
403
|
+
private _schedule;
|
|
404
|
+
}
|
|
405
|
+
interface DebouncerOptions {
|
|
406
|
+
/**
|
|
407
|
+
* Quiet period in milliseconds. The last-enqueued function runs after
|
|
408
|
+
* no new calls have arrived for this duration.
|
|
409
|
+
* @default 300
|
|
410
|
+
*/
|
|
411
|
+
delay?: number;
|
|
412
|
+
/**
|
|
413
|
+
* If true, also fires immediately on the leading edge (first call in a
|
|
414
|
+
* quiet period), then debounces subsequent calls normally.
|
|
415
|
+
*
|
|
416
|
+
* Leading-edge semantics: the leading call fires immediately and clears
|
|
417
|
+
* _pendingFn. Any calls arriving *while* the leading execution is in flight
|
|
418
|
+
* register as a new trailing call and will be resolved by the trailing timer
|
|
419
|
+
* when the quiet period expires. Calls arriving during the leading execution
|
|
420
|
+
* are never lost.
|
|
421
|
+
*
|
|
422
|
+
* @default false
|
|
423
|
+
*/
|
|
424
|
+
leading?: boolean;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Result when the debounced function successfully executed.
|
|
428
|
+
*/
|
|
429
|
+
type DebouncerOk<T> = {
|
|
430
|
+
status: "ok";
|
|
431
|
+
value: T;
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* Result when the debounced function threw an error.
|
|
435
|
+
*/
|
|
436
|
+
type DebouncerError = {
|
|
437
|
+
status: "error";
|
|
438
|
+
error: unknown;
|
|
439
|
+
};
|
|
440
|
+
/**
|
|
441
|
+
* Result when the debounced call was cancelled before execution.
|
|
442
|
+
*/
|
|
443
|
+
type DebouncerCancelled = {
|
|
444
|
+
status: "cancelled";
|
|
445
|
+
};
|
|
446
|
+
/**
|
|
447
|
+
* Discriminated union of all possible Debouncer outcomes.
|
|
448
|
+
*
|
|
449
|
+
* - `DebouncerOk<T>`: function ran and returned a value.
|
|
450
|
+
* - `DebouncerError`: function ran and threw an error.
|
|
451
|
+
* - `DebouncerCancelled`: call was cancelled before it ran.
|
|
452
|
+
*/
|
|
453
|
+
type DebouncerResult<T> = DebouncerOk<T> | DebouncerError | DebouncerCancelled;
|
|
454
|
+
/**
|
|
455
|
+
* Collapses rapid successive calls into a single execution.
|
|
456
|
+
*
|
|
457
|
+
* Each call to `do()` returns a promise that resolves with the result of the
|
|
458
|
+
* debounced execution — i.e. all callers within the same quiet window share
|
|
459
|
+
* the same result. The function that actually runs is always the *last* one
|
|
460
|
+
* enqueued before the delay expires.
|
|
461
|
+
*
|
|
462
|
+
* @example
|
|
463
|
+
* const debounced = new Debouncer({ delay: 200 });
|
|
464
|
+
* // Called rapidly three times — only the last fn runs.
|
|
465
|
+
* const [a, b, c] = await Promise.all([
|
|
466
|
+
* debounced.do(() => fetch("/api/search?q=h")),
|
|
467
|
+
* debounced.do(() => fetch("/api/search?q=he")),
|
|
468
|
+
* debounced.do(() => fetch("/api/search?q=hel")),
|
|
469
|
+
* ]);
|
|
470
|
+
* // a.status === "ok" and a.value is the result of the third fetch.
|
|
471
|
+
* // b and c share the same result as a.
|
|
472
|
+
*/
|
|
473
|
+
declare class Debouncer<T = void> {
|
|
474
|
+
private _delay;
|
|
475
|
+
private _leading;
|
|
476
|
+
private _timer;
|
|
477
|
+
private _pendingFn;
|
|
478
|
+
private _pendingResolvers;
|
|
479
|
+
private _leadingFired;
|
|
480
|
+
constructor(options?: DebouncerOptions);
|
|
481
|
+
/**
|
|
482
|
+
* Enqueues a function and returns a promise that resolves with its result.
|
|
483
|
+
*
|
|
484
|
+
* All callers within the same quiet window share the same result — the
|
|
485
|
+
* function that actually runs is the *last* one enqueued before the delay
|
|
486
|
+
* expires. Use this when you need the return value of the debounced call.
|
|
487
|
+
*
|
|
488
|
+
* For fire-and-forget use (void fns, event handlers), prefer `fire()` to
|
|
489
|
+
* avoid unhandled-promise-rejection warnings and make intent explicit.
|
|
490
|
+
*
|
|
491
|
+
* @param fn - The function to debounce.
|
|
492
|
+
* @returns A promise resolving with a DebouncerResult discriminated union.
|
|
493
|
+
*/
|
|
494
|
+
do(fn: () => Promise<T> | T): Promise<DebouncerResult<T>>;
|
|
495
|
+
/**
|
|
496
|
+
* Enqueues a function without returning a promise (fire-and-forget).
|
|
497
|
+
*
|
|
498
|
+
* Identical debounce semantics to `do()` — the last-enqueued function runs
|
|
499
|
+
* after the quiet period. Use this for void callbacks, DOM event handlers,
|
|
500
|
+
* or any caller that doesn't care about the result. No dangling promise,
|
|
501
|
+
* no unhandled-rejection risk.
|
|
502
|
+
*
|
|
503
|
+
* @param fn - The function to debounce.
|
|
504
|
+
*/
|
|
505
|
+
fire(fn: () => Promise<T> | T): void;
|
|
506
|
+
/**
|
|
507
|
+
* Shared enqueue logic for both `do()` and `fire()`.
|
|
508
|
+
*
|
|
509
|
+
*/
|
|
510
|
+
private _enqueue;
|
|
511
|
+
/**
|
|
512
|
+
* Immediately cancels any pending debounced execution.
|
|
513
|
+
* All waiting callers resolve with `{ status: "cancelled" }`.
|
|
514
|
+
*/
|
|
515
|
+
cancel(): void;
|
|
516
|
+
/**
|
|
517
|
+
* Immediately executes the pending function (if any), bypassing the remaining delay.
|
|
518
|
+
* All waiting callers receive the result.
|
|
519
|
+
*/
|
|
520
|
+
flush(): Promise<DebouncerResult<T> | null>;
|
|
521
|
+
/** Returns true if a debounced call is currently pending. */
|
|
522
|
+
pending(): boolean;
|
|
523
|
+
/**
|
|
524
|
+
* Executes the latest pending function and resolves all waiting callers.
|
|
525
|
+
*/
|
|
526
|
+
private _fire;
|
|
527
|
+
}
|
|
176
528
|
|
|
177
|
-
export { Mutex, type MutexOptions, Once, type OnceResult, Serializer, type SerializerOptions, type SerializerResult };
|
|
529
|
+
export { Debouncer, type DebouncerCancelled, type DebouncerError, type DebouncerOk, type DebouncerOptions, type DebouncerResult, Latch, Mutex, type MutexOptions, Once, type OnceResult, RWMutex, type RWMutexOptions, Semaphore, type SemaphoreOptions, Serializer, type SerializerOptions, type SerializerResult };
|
package/index.d.ts
CHANGED
|
@@ -11,14 +11,14 @@ interface MutexOptions {
|
|
|
11
11
|
* - `"macrotask"` (default): Uses setTimeout(fn, 0) to yield to the event
|
|
12
12
|
* loop between handoffs. Prevents microtask starvation under heavy
|
|
13
13
|
* contention — I/O, rendering, and other macrotasks can run between
|
|
14
|
-
* lock acquisitions. Use for
|
|
15
|
-
*
|
|
14
|
+
* lock acquisitions. Use for Serializer and other coarse-grained
|
|
15
|
+
* serializers.
|
|
16
16
|
*
|
|
17
17
|
* - `"microtask"`: Uses queueMicrotask(fn) for handoff. Near-zero latency
|
|
18
18
|
* between acquisitions — no macrotask delay. Safe when you need
|
|
19
19
|
* back-to-back operations to complete as fast as possible and starvation
|
|
20
|
-
* is not a concern (e.g.
|
|
21
|
-
*
|
|
20
|
+
* is not a concern (e.g. Once, Semaphore where builds are infrequent
|
|
21
|
+
* and latency matters more than fairness).
|
|
22
22
|
*/
|
|
23
23
|
yieldMode?: "macrotask" | "microtask";
|
|
24
24
|
}
|
|
@@ -51,6 +51,11 @@ declare class Mutex {
|
|
|
51
51
|
tryLock(): boolean;
|
|
52
52
|
/**
|
|
53
53
|
* Releases the lock, scheduling the next waiter according to yieldMode.
|
|
54
|
+
*
|
|
55
|
+
* When a waiter exists, `_locked` intentionally remains `true` — ownership
|
|
56
|
+
* transfers directly to the next waiter without ever clearing the flag.
|
|
57
|
+
* Only when the queue is empty is `_locked` set to false.
|
|
58
|
+
*
|
|
54
59
|
* @throws {Error} If the mutex is not currently locked.
|
|
55
60
|
*/
|
|
56
61
|
unlock(): void;
|
|
@@ -59,6 +64,61 @@ declare class Mutex {
|
|
|
59
64
|
/** Returns the number of operations waiting for the lock. */
|
|
60
65
|
pending(): number;
|
|
61
66
|
}
|
|
67
|
+
interface SemaphoreOptions extends MutexOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Number of concurrent holders allowed.
|
|
70
|
+
* Inherits `capacity` and `yieldMode` from MutexOptions.
|
|
71
|
+
* @default 1 (equivalent to a Mutex)
|
|
72
|
+
*/
|
|
73
|
+
slots?: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A counting semaphore — generalises Mutex to allow up to N concurrent holders.
|
|
77
|
+
*
|
|
78
|
+
* Useful for rate-limiting concurrency: e.g. "at most 3 in-flight HTTP requests".
|
|
79
|
+
* With slots=1 it behaves identically to Mutex.
|
|
80
|
+
*
|
|
81
|
+
* Accepts the same `capacity` and `yieldMode` options as Mutex, plus `slots`.
|
|
82
|
+
*/
|
|
83
|
+
declare class Semaphore {
|
|
84
|
+
private _slots;
|
|
85
|
+
private _available;
|
|
86
|
+
private _capacity;
|
|
87
|
+
private _yieldMode;
|
|
88
|
+
private waiters;
|
|
89
|
+
constructor(options?: SemaphoreOptions);
|
|
90
|
+
/**
|
|
91
|
+
* Acquires one slot. If all slots are taken, waits until one is released.
|
|
92
|
+
*
|
|
93
|
+
* @param timeout - Optional maximum wait time in milliseconds.
|
|
94
|
+
* @throws {TimeoutError} If a slot cannot be acquired within the timeout.
|
|
95
|
+
* @throws {Error} If the wait queue is full.
|
|
96
|
+
*/
|
|
97
|
+
acquire(timeout?: number): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Attempts to acquire a slot without waiting.
|
|
100
|
+
* @returns `true` if a slot was acquired, `false` otherwise.
|
|
101
|
+
*/
|
|
102
|
+
tryAcquire(): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Releases one slot. If waiters are queued, the next one is scheduled
|
|
105
|
+
* according to yieldMode.
|
|
106
|
+
*
|
|
107
|
+
* @throws {Error} If no slot is currently held (release without acquire).
|
|
108
|
+
*/
|
|
109
|
+
release(): void;
|
|
110
|
+
/**
|
|
111
|
+
* Runs a function with one acquired slot, releasing it when done.
|
|
112
|
+
* Convenience wrapper — prefer this over manual acquire/release.
|
|
113
|
+
*/
|
|
114
|
+
run<T>(fn: () => Promise<T> | T, timeout?: number): Promise<T>;
|
|
115
|
+
/** Number of slots currently free. */
|
|
116
|
+
available(): number;
|
|
117
|
+
/** Number of callers waiting for a slot. */
|
|
118
|
+
pending(): number;
|
|
119
|
+
/** Total slot count (as configured). */
|
|
120
|
+
slots(): number;
|
|
121
|
+
}
|
|
62
122
|
type OnceResult<T> = {
|
|
63
123
|
value: T | null;
|
|
64
124
|
error?: unknown;
|
|
@@ -92,14 +152,28 @@ declare class Once<T = void> {
|
|
|
92
152
|
* @param timeout - Max wait time in ms (includes lock wait + execution time).
|
|
93
153
|
*/
|
|
94
154
|
do(fn: () => Promise<T> | T, timeout?: number): Promise<OnceResult<T>>;
|
|
155
|
+
/**
|
|
156
|
+
* Executes the function synchronously if it hasn't been executed yet.
|
|
157
|
+
* If an async operation is pending or the lock is contended, it will fail immediately.
|
|
158
|
+
* The failure is returned as an error state, OR thrown if `throws: true` is configured.
|
|
159
|
+
*
|
|
160
|
+
* @param fn - The synchronous function to execute.
|
|
161
|
+
* @returns The result of the execution.
|
|
162
|
+
*/
|
|
163
|
+
doSync(fn: () => T): OnceResult<T>;
|
|
95
164
|
/**
|
|
96
165
|
* Returns true if the operation has completed (success or non-retryable failure)
|
|
97
|
-
* and
|
|
98
|
-
*
|
|
166
|
+
* and the internal promise has been cleared.
|
|
167
|
+
*
|
|
168
|
+
* This is the canonical "safe to read" check — prefer it over `done()`.
|
|
99
169
|
*/
|
|
100
170
|
isReady(): boolean;
|
|
101
171
|
/**
|
|
102
172
|
* Returns true if the operation is currently executing.
|
|
173
|
+
*
|
|
174
|
+
* Uses isReady() as the canonical check: running is its logical complement
|
|
175
|
+
* when a promise is in flight. This correctly handles the brief window where
|
|
176
|
+
* _done=true but the promise hasn't been cleared yet in finally.
|
|
103
177
|
*/
|
|
104
178
|
running(): boolean;
|
|
105
179
|
/**
|
|
@@ -113,16 +187,27 @@ declare class Once<T = void> {
|
|
|
113
187
|
get(): T | null;
|
|
114
188
|
/**
|
|
115
189
|
* Resets the instance, allowing the action to run again on the next call.
|
|
190
|
+
*
|
|
191
|
+
* @throws {Error} If called while an operation is currently executing,
|
|
192
|
+
* as this would leave the in-flight promise in an inconsistent state.
|
|
116
193
|
*/
|
|
117
194
|
reset(): void;
|
|
118
195
|
/**
|
|
119
|
-
* Returns the
|
|
196
|
+
* Returns the in-flight execution promise if one is currently running, null otherwise.
|
|
197
|
+
*
|
|
198
|
+
* Use this to join an in-progress operation without triggering a new one.
|
|
120
199
|
*/
|
|
121
|
-
|
|
200
|
+
currentExecution(): Promise<OnceResult<T>> | null;
|
|
122
201
|
/**
|
|
123
202
|
* Returns true if the operation has finished (success or final failure).
|
|
124
203
|
*/
|
|
125
204
|
done(): boolean;
|
|
205
|
+
/**
|
|
206
|
+
* Awaits a promise with an optional timeout.
|
|
207
|
+
*
|
|
208
|
+
* We clear the timer on the winning branch, consistent with the
|
|
209
|
+
* Mutex/Semaphore/Latch timeout pattern throughout this module.
|
|
210
|
+
*/
|
|
126
211
|
private _awaitWithTimeout;
|
|
127
212
|
}
|
|
128
213
|
type SerializerResult<T> = {
|
|
@@ -137,8 +222,8 @@ interface SerializerOptions {
|
|
|
137
222
|
capacity?: number;
|
|
138
223
|
/**
|
|
139
224
|
* Yield mode for the internal mutex. Defaults to "macrotask" for
|
|
140
|
-
* coarse-grained serializers
|
|
141
|
-
*
|
|
225
|
+
* coarse-grained serializers. Use "microtask" for fine-grained
|
|
226
|
+
* latency-sensitive serializers.
|
|
142
227
|
*/
|
|
143
228
|
yieldMode?: "macrotask" | "microtask";
|
|
144
229
|
}
|
|
@@ -152,6 +237,7 @@ declare class Serializer<T = void> {
|
|
|
152
237
|
private _done;
|
|
153
238
|
private _lastValue;
|
|
154
239
|
private _lastError;
|
|
240
|
+
private _hasRun;
|
|
155
241
|
constructor(options?: SerializerOptions);
|
|
156
242
|
/**
|
|
157
243
|
* Enqueue a function to be executed after all previous tasks complete.
|
|
@@ -161,8 +247,18 @@ declare class Serializer<T = void> {
|
|
|
161
247
|
* @returns Object containing the value or error.
|
|
162
248
|
*/
|
|
163
249
|
do(fn: () => Promise<T> | T, timeout?: number): Promise<SerializerResult<T | null>>;
|
|
164
|
-
/**
|
|
250
|
+
/**
|
|
251
|
+
* Returns the result of the last execution.
|
|
252
|
+
*
|
|
253
|
+
* Returns `{ value: null, error: undefined }` both when the serializer has
|
|
254
|
+
* never run and when it ran and returned null — use `hasRun()` to distinguish
|
|
255
|
+
* these two cases.
|
|
256
|
+
*/
|
|
165
257
|
peek(): SerializerResult<T | null>;
|
|
258
|
+
/**
|
|
259
|
+
* Returns true if at least one task has been executed (successfully or not).
|
|
260
|
+
*/
|
|
261
|
+
hasRun(): boolean;
|
|
166
262
|
/**
|
|
167
263
|
* Permanently closes the serializer.
|
|
168
264
|
* Subsequent calls to `do()` will fail immediately with SerializerExecutionDone.
|
|
@@ -173,5 +269,261 @@ declare class Serializer<T = void> {
|
|
|
173
269
|
/** Returns true if a task is currently executing. */
|
|
174
270
|
running(): boolean;
|
|
175
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* A one-shot gate that starts closed and opens exactly once.
|
|
274
|
+
*
|
|
275
|
+
* All current and future `wait()` callers resolve immediately once `open()` is
|
|
276
|
+
* called. Unlike Once, Latch carries no return value and has no retry logic —
|
|
277
|
+
* it is a pure signalling primitive.
|
|
278
|
+
*
|
|
279
|
+
* Typical use: coordinate startup, signal that an initialisation phase is done,
|
|
280
|
+
* or gate downstream work on a single event.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* const ready = new Latch();
|
|
284
|
+
* // Consumer
|
|
285
|
+
* await ready.wait();
|
|
286
|
+
* // Producer (elsewhere)
|
|
287
|
+
* ready.open();
|
|
288
|
+
*/
|
|
289
|
+
declare class Latch {
|
|
290
|
+
private _open;
|
|
291
|
+
private _resolve;
|
|
292
|
+
private _promise;
|
|
293
|
+
constructor();
|
|
294
|
+
/**
|
|
295
|
+
* Opens the latch. All current and future waiters resolve immediately.
|
|
296
|
+
* Calling open() more than once is a no-op.
|
|
297
|
+
*/
|
|
298
|
+
open(): void;
|
|
299
|
+
/**
|
|
300
|
+
* Returns a promise that resolves when the latch is opened.
|
|
301
|
+
* If already open, resolves on the next microtask checkpoint.
|
|
302
|
+
*
|
|
303
|
+
* @param timeout - Optional maximum wait time in milliseconds.
|
|
304
|
+
* @throws {TimeoutError} If the latch does not open within the timeout.
|
|
305
|
+
*/
|
|
306
|
+
wait(timeout?: number): Promise<void>;
|
|
307
|
+
/**
|
|
308
|
+
* Synchronously checks whether the latch has been opened.
|
|
309
|
+
*/
|
|
310
|
+
isOpen(): boolean;
|
|
311
|
+
}
|
|
312
|
+
interface RWMutexOptions {
|
|
313
|
+
/**
|
|
314
|
+
* Controls how lock handoff is scheduled when a waiter is unblocked.
|
|
315
|
+
*
|
|
316
|
+
* - `"microtask"` (default): Uses queueMicrotask(fn). Near-zero latency
|
|
317
|
+
* between handoffs. Appropriate for RWMutex because readers are woken in
|
|
318
|
+
* bulk and writers are rarely contended enough to cause starvation.
|
|
319
|
+
*
|
|
320
|
+
* - `"macrotask"`: Uses setTimeout(fn, 0) to yield to the event loop between
|
|
321
|
+
* handoffs. Use if you observe microtask starvation under extreme write
|
|
322
|
+
* contention on this specific lock.
|
|
323
|
+
*/
|
|
324
|
+
yieldMode?: "macrotask" | "microtask";
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* A read-write mutex with writer preference.
|
|
328
|
+
*
|
|
329
|
+
* - Multiple readers can hold the lock concurrently.
|
|
330
|
+
* - Writers get exclusive access — no readers or other writers.
|
|
331
|
+
* - Writer preference: once a writer is waiting, new readers queue behind it.
|
|
332
|
+
* This prevents writer starvation under read-heavy loads.
|
|
333
|
+
*
|
|
334
|
+
* Readers should call `rlock()` / `runlock()`.
|
|
335
|
+
* Writers should call `lock()` / `unlock()`.
|
|
336
|
+
* Prefer the `read()` and `write()` convenience wrappers to avoid lock leaks.
|
|
337
|
+
*/
|
|
338
|
+
declare class RWMutex {
|
|
339
|
+
private _readers;
|
|
340
|
+
private _writeLocked;
|
|
341
|
+
private _pendingWriters;
|
|
342
|
+
private _yieldMode;
|
|
343
|
+
private readerWaiters;
|
|
344
|
+
private writerWaiters;
|
|
345
|
+
constructor(options?: RWMutexOptions);
|
|
346
|
+
/**
|
|
347
|
+
* Acquires a read lock. Blocks if a writer holds or is waiting for the lock.
|
|
348
|
+
*
|
|
349
|
+
* @param timeout - Optional maximum wait time in milliseconds.
|
|
350
|
+
* @throws {TimeoutError} If the read lock cannot be acquired within the timeout.
|
|
351
|
+
*/
|
|
352
|
+
rlock(timeout?: number): Promise<void>;
|
|
353
|
+
/**
|
|
354
|
+
* Releases a read lock.
|
|
355
|
+
* @throws {Error} If no read lock is currently held.
|
|
356
|
+
*/
|
|
357
|
+
runlock(): void;
|
|
358
|
+
/**
|
|
359
|
+
* Acquires the write lock. Blocks until all current readers and any prior
|
|
360
|
+
* writers have finished.
|
|
361
|
+
*
|
|
362
|
+
* @param timeout - Optional maximum wait time in milliseconds.
|
|
363
|
+
* @throws {TimeoutError} If the write lock cannot be acquired within the timeout.
|
|
364
|
+
*/
|
|
365
|
+
lock(timeout?: number): Promise<void>;
|
|
366
|
+
/**
|
|
367
|
+
* Releases the write lock.
|
|
368
|
+
* Wakes the next pending writer if one exists; otherwise wakes all pending readers.
|
|
369
|
+
*
|
|
370
|
+
* @throws {Error} If no write lock is currently held.
|
|
371
|
+
*/
|
|
372
|
+
unlock(): void;
|
|
373
|
+
/**
|
|
374
|
+
* Runs a function under a read lock, releasing it when done.
|
|
375
|
+
* Prefer this over manual rlock/runlock to avoid lock leaks.
|
|
376
|
+
*/
|
|
377
|
+
read<T>(fn: () => Promise<T> | T, timeout?: number): Promise<T>;
|
|
378
|
+
/**
|
|
379
|
+
* Runs a function under the write lock, releasing it when done.
|
|
380
|
+
* Prefer this over manual lock/unlock to avoid lock leaks.
|
|
381
|
+
*/
|
|
382
|
+
write<T>(fn: () => Promise<T> | T, timeout?: number): Promise<T>;
|
|
383
|
+
/** Returns true if the write lock is currently held. */
|
|
384
|
+
writeLocked(): boolean;
|
|
385
|
+
/** Returns the number of active read lock holders. */
|
|
386
|
+
readers(): number;
|
|
387
|
+
/** Returns the number of writers currently waiting for the lock. */
|
|
388
|
+
pendingWriters(): number;
|
|
389
|
+
/** Returns the number of readers currently waiting for the lock. */
|
|
390
|
+
pendingReaders(): number;
|
|
391
|
+
/**
|
|
392
|
+
* Wakes the next writer if the lock is free and a writer is waiting.
|
|
393
|
+
* Returns true if a writer was woken, false otherwise.
|
|
394
|
+
*/
|
|
395
|
+
private _tryWakeWriter;
|
|
396
|
+
/**
|
|
397
|
+
* Wakes all pending readers (called when a writer releases and no writers are queued).
|
|
398
|
+
*/
|
|
399
|
+
private _wakeAllReaders;
|
|
400
|
+
/**
|
|
401
|
+
* Schedules a waiter callback according to the configured yieldMode.
|
|
402
|
+
*/
|
|
403
|
+
private _schedule;
|
|
404
|
+
}
|
|
405
|
+
interface DebouncerOptions {
|
|
406
|
+
/**
|
|
407
|
+
* Quiet period in milliseconds. The last-enqueued function runs after
|
|
408
|
+
* no new calls have arrived for this duration.
|
|
409
|
+
* @default 300
|
|
410
|
+
*/
|
|
411
|
+
delay?: number;
|
|
412
|
+
/**
|
|
413
|
+
* If true, also fires immediately on the leading edge (first call in a
|
|
414
|
+
* quiet period), then debounces subsequent calls normally.
|
|
415
|
+
*
|
|
416
|
+
* Leading-edge semantics: the leading call fires immediately and clears
|
|
417
|
+
* _pendingFn. Any calls arriving *while* the leading execution is in flight
|
|
418
|
+
* register as a new trailing call and will be resolved by the trailing timer
|
|
419
|
+
* when the quiet period expires. Calls arriving during the leading execution
|
|
420
|
+
* are never lost.
|
|
421
|
+
*
|
|
422
|
+
* @default false
|
|
423
|
+
*/
|
|
424
|
+
leading?: boolean;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Result when the debounced function successfully executed.
|
|
428
|
+
*/
|
|
429
|
+
type DebouncerOk<T> = {
|
|
430
|
+
status: "ok";
|
|
431
|
+
value: T;
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* Result when the debounced function threw an error.
|
|
435
|
+
*/
|
|
436
|
+
type DebouncerError = {
|
|
437
|
+
status: "error";
|
|
438
|
+
error: unknown;
|
|
439
|
+
};
|
|
440
|
+
/**
|
|
441
|
+
* Result when the debounced call was cancelled before execution.
|
|
442
|
+
*/
|
|
443
|
+
type DebouncerCancelled = {
|
|
444
|
+
status: "cancelled";
|
|
445
|
+
};
|
|
446
|
+
/**
|
|
447
|
+
* Discriminated union of all possible Debouncer outcomes.
|
|
448
|
+
*
|
|
449
|
+
* - `DebouncerOk<T>`: function ran and returned a value.
|
|
450
|
+
* - `DebouncerError`: function ran and threw an error.
|
|
451
|
+
* - `DebouncerCancelled`: call was cancelled before it ran.
|
|
452
|
+
*/
|
|
453
|
+
type DebouncerResult<T> = DebouncerOk<T> | DebouncerError | DebouncerCancelled;
|
|
454
|
+
/**
|
|
455
|
+
* Collapses rapid successive calls into a single execution.
|
|
456
|
+
*
|
|
457
|
+
* Each call to `do()` returns a promise that resolves with the result of the
|
|
458
|
+
* debounced execution — i.e. all callers within the same quiet window share
|
|
459
|
+
* the same result. The function that actually runs is always the *last* one
|
|
460
|
+
* enqueued before the delay expires.
|
|
461
|
+
*
|
|
462
|
+
* @example
|
|
463
|
+
* const debounced = new Debouncer({ delay: 200 });
|
|
464
|
+
* // Called rapidly three times — only the last fn runs.
|
|
465
|
+
* const [a, b, c] = await Promise.all([
|
|
466
|
+
* debounced.do(() => fetch("/api/search?q=h")),
|
|
467
|
+
* debounced.do(() => fetch("/api/search?q=he")),
|
|
468
|
+
* debounced.do(() => fetch("/api/search?q=hel")),
|
|
469
|
+
* ]);
|
|
470
|
+
* // a.status === "ok" and a.value is the result of the third fetch.
|
|
471
|
+
* // b and c share the same result as a.
|
|
472
|
+
*/
|
|
473
|
+
declare class Debouncer<T = void> {
|
|
474
|
+
private _delay;
|
|
475
|
+
private _leading;
|
|
476
|
+
private _timer;
|
|
477
|
+
private _pendingFn;
|
|
478
|
+
private _pendingResolvers;
|
|
479
|
+
private _leadingFired;
|
|
480
|
+
constructor(options?: DebouncerOptions);
|
|
481
|
+
/**
|
|
482
|
+
* Enqueues a function and returns a promise that resolves with its result.
|
|
483
|
+
*
|
|
484
|
+
* All callers within the same quiet window share the same result — the
|
|
485
|
+
* function that actually runs is the *last* one enqueued before the delay
|
|
486
|
+
* expires. Use this when you need the return value of the debounced call.
|
|
487
|
+
*
|
|
488
|
+
* For fire-and-forget use (void fns, event handlers), prefer `fire()` to
|
|
489
|
+
* avoid unhandled-promise-rejection warnings and make intent explicit.
|
|
490
|
+
*
|
|
491
|
+
* @param fn - The function to debounce.
|
|
492
|
+
* @returns A promise resolving with a DebouncerResult discriminated union.
|
|
493
|
+
*/
|
|
494
|
+
do(fn: () => Promise<T> | T): Promise<DebouncerResult<T>>;
|
|
495
|
+
/**
|
|
496
|
+
* Enqueues a function without returning a promise (fire-and-forget).
|
|
497
|
+
*
|
|
498
|
+
* Identical debounce semantics to `do()` — the last-enqueued function runs
|
|
499
|
+
* after the quiet period. Use this for void callbacks, DOM event handlers,
|
|
500
|
+
* or any caller that doesn't care about the result. No dangling promise,
|
|
501
|
+
* no unhandled-rejection risk.
|
|
502
|
+
*
|
|
503
|
+
* @param fn - The function to debounce.
|
|
504
|
+
*/
|
|
505
|
+
fire(fn: () => Promise<T> | T): void;
|
|
506
|
+
/**
|
|
507
|
+
* Shared enqueue logic for both `do()` and `fire()`.
|
|
508
|
+
*
|
|
509
|
+
*/
|
|
510
|
+
private _enqueue;
|
|
511
|
+
/**
|
|
512
|
+
* Immediately cancels any pending debounced execution.
|
|
513
|
+
* All waiting callers resolve with `{ status: "cancelled" }`.
|
|
514
|
+
*/
|
|
515
|
+
cancel(): void;
|
|
516
|
+
/**
|
|
517
|
+
* Immediately executes the pending function (if any), bypassing the remaining delay.
|
|
518
|
+
* All waiting callers receive the result.
|
|
519
|
+
*/
|
|
520
|
+
flush(): Promise<DebouncerResult<T> | null>;
|
|
521
|
+
/** Returns true if a debounced call is currently pending. */
|
|
522
|
+
pending(): boolean;
|
|
523
|
+
/**
|
|
524
|
+
* Executes the latest pending function and resolves all waiting callers.
|
|
525
|
+
*/
|
|
526
|
+
private _fire;
|
|
527
|
+
}
|
|
176
528
|
|
|
177
|
-
export { Mutex, type MutexOptions, Once, type OnceResult, Serializer, type SerializerOptions, type SerializerResult };
|
|
529
|
+
export { Debouncer, type DebouncerCancelled, type DebouncerError, type DebouncerOk, type DebouncerOptions, type DebouncerResult, Latch, Mutex, type MutexOptions, Once, type OnceResult, RWMutex, type RWMutexOptions, Semaphore, type SemaphoreOptions, Serializer, type SerializerOptions, type SerializerResult };
|
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var e=class e extends Error{constructor(t,i){super(t,{cause:i}),this.name="SyncError",Object.setPrototypeOf(this,e.prototype)}},t=class extends e{constructor(e){super(`[ArtifactContainer] Operation timed out: ${e}`)}},i=class extends e{constructor(e){super("[Serializer] The serializer has been marked as done!",e)}},r=class{_locked=!1;_capacity;_yieldMode;waiters=[];constructor(e){this._capacity=e?.capacity??1/0,this._yieldMode=e?.yieldMode??"macrotask"}async lock(e){if(!this._locked)return void(this._locked=!0);if(this.waiters.length>=this._capacity)throw new Error(`Mutex queue is full (capacity: ${this._capacity})`);let i;const r=new Promise((e=>i=e));if(this.waiters.push(i),null==e)return void await r;let s;await Promise.race([r.then((()=>clearTimeout(s))),new Promise(((r,o)=>{s=setTimeout((()=>{const e=this.waiters.indexOf(i);-1!==e&&this.waiters.splice(e,1),o(new t("Mutex lock timed out"))}),e)}))])}tryLock(){return!this._locked&&(this._locked=!0,!0)}unlock(){if(!this._locked)throw new Error("Mutex is not locked");const e=this.waiters.shift();e?"microtask"===this._yieldMode?queueMicrotask(e):setTimeout(e,0):this._locked=!1}locked(){return this._locked}pending(){return this.waiters.length}};exports.Debouncer=class{_delay;_leading;_timer;_pendingFn;_pendingResolvers=[];_leadingFired=!1;constructor(e){this._delay=e?.delay??300,this._leading=e?.leading??!1}do(e){return new Promise((t=>{this._enqueue(e,t)}))}fire(e){this._enqueue(e,void 0)}_enqueue(e,t){if(this._pendingFn=e,t&&this._pendingResolvers.push(t),this._leading&&!this._leadingFired)return this._leadingFired=!0,this._fire(),clearTimeout(this._timer),void(this._timer=setTimeout((()=>{this._leadingFired=!1,void 0!==this._pendingFn&&this._fire()}),this._delay));clearTimeout(this._timer),this._timer=setTimeout((()=>{this._leadingFired=!1,this._fire()}),this._delay)}cancel(){clearTimeout(this._timer),this._timer=void 0,this._pendingFn=void 0,this._leadingFired=!1;const e=this._pendingResolvers.splice(0);for(const t of e)t({status:"cancelled"})}async flush(){return this._pendingFn?(clearTimeout(this._timer),this._timer=void 0,this._leadingFired=!1,this._fire()):null}pending(){return void 0!==this._pendingFn}async _fire(){const e=this._pendingFn,t=this._pendingResolvers.splice(0);let i;this._pendingFn=void 0;try{i={status:"ok",value:await e()}}catch(e){i={status:"error",error:e}}for(const e of t)e(i);return i}},exports.Latch=class{_open=!1;_resolve;_promise;constructor(){this._promise=new Promise((e=>{this._resolve=e}))}open(){this._open||(this._open=!0,this._resolve())}async wait(e){if(this._open)return;if(null==e)return this._promise;let i;await Promise.race([this._promise.then((()=>clearTimeout(i))),new Promise(((r,s)=>{i=setTimeout((()=>s(new t("Latch timed out"))),e)}))])}isOpen(){return this._open}},exports.Mutex=r,exports.Once=class{mutex=new r({yieldMode:"microtask"});promise=null;_value=null;_error;_done=!1;retry;throws;constructor({retry:e,throws:t}={}){this.retry=Boolean(e),this.throws=Boolean(t)}async do(e,t){return this._done?this.peek():this.promise?this._awaitWithTimeout(this.promise,t,"Once do() timed out"):(await this.mutex.lock(),this.promise?(this.mutex.unlock(),this._awaitWithTimeout(this.promise,t,"Once do() timed out")):(this.promise=(async()=>{try{const t=await e();this._value=t,this._done=!0}catch(e){if(this._error=e,this.retry||(this._done=!0),this.throws)throw e}finally{this.promise=null}return this.peek()})(),this.mutex.unlock(),this._awaitWithTimeout(this.promise,t,"Once do() timed out")))}doSync(e){if(this._done){if(this.throws&&this._error)throw this._error;return this.peek()}if(this.promise){const e=new Error("Cannot execute doSync while an async operation is pending.");if(this.throws)throw e;return{value:null,error:e}}if(!this.mutex.tryLock()){const e=new Error("Cannot execute doSync: lock is currently held.");if(this.throws)throw e;return{value:null,error:e}}if(this.promise||this._done){if(this.mutex.unlock(),this._done){if(this.throws&&this._error)throw this._error;return this.peek()}const e=new Error("Cannot execute doSync while an async operation is pending.");if(this.throws)throw e;return{value:null,error:e}}try{const t=e();this._value=t,this._done=!0}catch(e){if(this._error=e,this.retry||(this._done=!0),this.throws)throw e}finally{this.mutex.unlock()}return this.peek()}isReady(){return this._done&&null===this.promise}running(){return null!==this.promise&&!this.isReady()}peek(){return{value:this._value,error:this._error}}get(){if(!this._done)throw new Error("Once operation is not yet complete");if(this._error)throw this._error;return this._value}reset(){if(this.running())throw new Error("Cannot reset Once while an operation is in progress.");this._done=!1,this.promise=null,this._value=null,this._error=void 0}currentExecution(){return this.promise}done(){return this._done}_awaitWithTimeout(e,i,r="Operation timed out"){if(null==i)return e;let s;return Promise.race([e.then((e=>(clearTimeout(s),e))),new Promise(((e,o)=>{s=setTimeout((()=>o(new t(r))),i)}))])}},exports.RWMutex=class{_readers=0;_writeLocked=!1;_pendingWriters=0;_yieldMode;readerWaiters=[];writerWaiters=[];constructor(e){this._yieldMode=e?.yieldMode??"microtask"}async rlock(e){if(!this._writeLocked&&0===this._pendingWriters)return void this._readers++;let i;const r=new Promise((e=>i=e));if(this.readerWaiters.push(i),null==e)return void await r;let s;await Promise.race([r.then((()=>clearTimeout(s))),new Promise(((r,o)=>{s=setTimeout((()=>{const e=this.readerWaiters.indexOf(i);-1!==e&&this.readerWaiters.splice(e,1),o(new t("RWMutex rlock timed out"))}),e)}))])}runlock(){if(this._readers<=0)throw new Error("RWMutex: runlock called without a matching rlock");this._readers--,this._tryWakeWriter()}async lock(e){if(!this._writeLocked&&0===this._readers)return void(this._writeLocked=!0);let i;this._pendingWriters++;const r=new Promise((e=>i=e));if(this.writerWaiters.push(i),null==e)return void await r;let s;await Promise.race([r.then((()=>clearTimeout(s))),new Promise(((r,o)=>{s=setTimeout((()=>{const e=this.writerWaiters.indexOf(i);-1!==e&&(this.writerWaiters.splice(e,1),this._pendingWriters--),o(new t("RWMutex lock timed out"))}),e)}))])}unlock(){if(!this._writeLocked)throw new Error("RWMutex: unlock called without a matching lock");this._writeLocked=!1,this._tryWakeWriter()||this._wakeAllReaders()}async read(e,t){await this.rlock(t);try{return await e()}finally{this.runlock()}}async write(e,t){await this.lock(t);try{return await e()}finally{this.unlock()}}writeLocked(){return this._writeLocked}readers(){return this._readers}pendingWriters(){return this._pendingWriters}pendingReaders(){return this.readerWaiters.length}_tryWakeWriter(){if(this._writeLocked||this._readers>0)return!1;const e=this.writerWaiters.shift();return!!e&&(this._writeLocked=!0,this._pendingWriters--,this._schedule(e),!0)}_wakeAllReaders(){const e=this.readerWaiters.splice(0);if(0!==e.length){this._readers+=e.length;for(const t of e)this._schedule(t)}}_schedule(e){"microtask"===this._yieldMode?queueMicrotask(e):setTimeout(e,0)}},exports.Semaphore=class{_slots;_available;_capacity;_yieldMode;waiters=[];constructor(e){if(this._slots=e?.slots??1,this._available=this._slots,this._capacity=e?.capacity??1/0,this._yieldMode=e?.yieldMode??"macrotask",this._slots<1)throw new Error("Semaphore slots must be >= 1")}async acquire(e){if(this._available>0)return void this._available--;if(this.waiters.length>=this._capacity)throw new Error(`Semaphore queue is full (capacity: ${this._capacity})`);let i;const r=new Promise((e=>i=e));if(this.waiters.push(i),null==e)return void await r;let s;await Promise.race([r.then((()=>clearTimeout(s))),new Promise(((r,o)=>{s=setTimeout((()=>{const e=this.waiters.indexOf(i);-1!==e&&this.waiters.splice(e,1),o(new t("Semaphore acquire timed out"))}),e)}))])}tryAcquire(){return this._available>0&&(this._available--,!0)}release(){if(this._available>=this._slots)throw new Error("Semaphore released more times than acquired");const e=this.waiters.shift();e?"microtask"===this._yieldMode?queueMicrotask(e):setTimeout(e,0):this._available++}async run(e,t){await this.acquire(t);try{return await e()}finally{this.release()}}available(){return this._available}pending(){return this.waiters.length}slots(){return this._slots}},exports.Serializer=class{mutex;_done=!1;_lastValue=null;_lastError=void 0;_hasRun=!1;constructor(e){this.mutex=new r({capacity:e?.capacity??1e3,yieldMode:e?.yieldMode??"macrotask"})}async do(e,t){if(this._done)return{value:null,error:new i};try{await this.mutex.lock(t)}catch(e){return{value:null,error:e}}let r,s=null;try{if(this._done)throw new i;s=await e(),this._lastValue=s,this._lastError=void 0,this._hasRun=!0}catch(e){r=e,this._lastError=e,this._hasRun=!0}finally{this.mutex.unlock()}return{value:s,error:r}}peek(){return{value:this._lastValue,error:this._lastError}}hasRun(){return this._hasRun}close(){this._done=!0}pending(){return this.mutex.pending()}running(){return this.mutex.locked()}};
|
package/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var e=class e extends Error{constructor(t,i){super(t,{cause:i}),this.name="SyncError",Object.setPrototypeOf(this,e.prototype)}},t=class extends e{constructor(e){super(`[ArtifactContainer] Operation timed out: ${e}`)}},i=class extends e{constructor(e){super("[Serializer] The serializer has been marked as done!",e)}},r=class{_locked=!1;_capacity;_yieldMode;waiters=[];constructor(e){this._capacity=e?.capacity??1/0,this._yieldMode=e?.yieldMode??"macrotask"}async lock(e){if(!this._locked)return void(this._locked=!0);if(this.waiters.length>=this._capacity)throw new Error(`Mutex queue is full (capacity: ${this._capacity})`);let i;const r=new Promise((e=>i=e));if(this.waiters.push(i),null==e)return void await r;let s;await Promise.race([r.then((()=>clearTimeout(s))),new Promise(((r,o)=>{s=setTimeout((()=>{const e=this.waiters.indexOf(i);-1!==e&&this.waiters.splice(e,1),o(new t("Mutex lock timed out"))}),e)}))])}tryLock(){return!this._locked&&(this._locked=!0,!0)}unlock(){if(!this._locked)throw new Error("Mutex is not locked");const e=this.waiters.shift();e?"microtask"===this._yieldMode?queueMicrotask(e):setTimeout(e,0):this._locked=!1}locked(){return this._locked}pending(){return this.waiters.length}},s=class{_slots;_available;_capacity;_yieldMode;waiters=[];constructor(e){if(this._slots=e?.slots??1,this._available=this._slots,this._capacity=e?.capacity??1/0,this._yieldMode=e?.yieldMode??"macrotask",this._slots<1)throw new Error("Semaphore slots must be >= 1")}async acquire(e){if(this._available>0)return void this._available--;if(this.waiters.length>=this._capacity)throw new Error(`Semaphore queue is full (capacity: ${this._capacity})`);let i;const r=new Promise((e=>i=e));if(this.waiters.push(i),null==e)return void await r;let s;await Promise.race([r.then((()=>clearTimeout(s))),new Promise(((r,o)=>{s=setTimeout((()=>{const e=this.waiters.indexOf(i);-1!==e&&this.waiters.splice(e,1),o(new t("Semaphore acquire timed out"))}),e)}))])}tryAcquire(){return this._available>0&&(this._available--,!0)}release(){if(this._available>=this._slots)throw new Error("Semaphore released more times than acquired");const e=this.waiters.shift();e?"microtask"===this._yieldMode?queueMicrotask(e):setTimeout(e,0):this._available++}async run(e,t){await this.acquire(t);try{return await e()}finally{this.release()}}available(){return this._available}pending(){return this.waiters.length}slots(){return this._slots}},o=class{mutex=new r({yieldMode:"microtask"});promise=null;_value=null;_error;_done=!1;retry;throws;constructor({retry:e,throws:t}={}){this.retry=Boolean(e),this.throws=Boolean(t)}async do(e,t){return this._done?this.peek():this.promise?this._awaitWithTimeout(this.promise,t,"Once do() timed out"):(await this.mutex.lock(),this.promise?(this.mutex.unlock(),this._awaitWithTimeout(this.promise,t,"Once do() timed out")):(this.promise=(async()=>{try{const t=await e();this._value=t,this._done=!0}catch(e){if(this._error=e,this.retry||(this._done=!0),this.throws)throw e}finally{this.promise=null}return this.peek()})(),this.mutex.unlock(),this._awaitWithTimeout(this.promise,t,"Once do() timed out")))}doSync(e){if(this._done){if(this.throws&&this._error)throw this._error;return this.peek()}if(this.promise){const e=new Error("Cannot execute doSync while an async operation is pending.");if(this.throws)throw e;return{value:null,error:e}}if(!this.mutex.tryLock()){const e=new Error("Cannot execute doSync: lock is currently held.");if(this.throws)throw e;return{value:null,error:e}}if(this.promise||this._done){if(this.mutex.unlock(),this._done){if(this.throws&&this._error)throw this._error;return this.peek()}const e=new Error("Cannot execute doSync while an async operation is pending.");if(this.throws)throw e;return{value:null,error:e}}try{const t=e();this._value=t,this._done=!0}catch(e){if(this._error=e,this.retry||(this._done=!0),this.throws)throw e}finally{this.mutex.unlock()}return this.peek()}isReady(){return this._done&&null===this.promise}running(){return null!==this.promise&&!this.isReady()}peek(){return{value:this._value,error:this._error}}get(){if(!this._done)throw new Error("Once operation is not yet complete");if(this._error)throw this._error;return this._value}reset(){if(this.running())throw new Error("Cannot reset Once while an operation is in progress.");this._done=!1,this.promise=null,this._value=null,this._error=void 0}currentExecution(){return this.promise}done(){return this._done}_awaitWithTimeout(e,i,r="Operation timed out"){if(null==i)return e;let s;return Promise.race([e.then((e=>(clearTimeout(s),e))),new Promise(((e,o)=>{s=setTimeout((()=>o(new t(r))),i)}))])}},n=class{mutex;_done=!1;_lastValue=null;_lastError=void 0;_hasRun=!1;constructor(e){this.mutex=new r({capacity:e?.capacity??1e3,yieldMode:e?.yieldMode??"macrotask"})}async do(e,t){if(this._done)return{value:null,error:new i};try{await this.mutex.lock(t)}catch(e){return{value:null,error:e}}let r,s=null;try{if(this._done)throw new i;s=await e(),this._lastValue=s,this._lastError=void 0,this._hasRun=!0}catch(e){r=e,this._lastError=e,this._hasRun=!0}finally{this.mutex.unlock()}return{value:s,error:r}}peek(){return{value:this._lastValue,error:this._lastError}}hasRun(){return this._hasRun}close(){this._done=!0}pending(){return this.mutex.pending()}running(){return this.mutex.locked()}},a=class{_open=!1;_resolve;_promise;constructor(){this._promise=new Promise((e=>{this._resolve=e}))}open(){this._open||(this._open=!0,this._resolve())}async wait(e){if(this._open)return;if(null==e)return this._promise;let i;await Promise.race([this._promise.then((()=>clearTimeout(i))),new Promise(((r,s)=>{i=setTimeout((()=>s(new t("Latch timed out"))),e)}))])}isOpen(){return this._open}},h=class{_readers=0;_writeLocked=!1;_pendingWriters=0;_yieldMode;readerWaiters=[];writerWaiters=[];constructor(e){this._yieldMode=e?.yieldMode??"microtask"}async rlock(e){if(!this._writeLocked&&0===this._pendingWriters)return void this._readers++;let i;const r=new Promise((e=>i=e));if(this.readerWaiters.push(i),null==e)return void await r;let s;await Promise.race([r.then((()=>clearTimeout(s))),new Promise(((r,o)=>{s=setTimeout((()=>{const e=this.readerWaiters.indexOf(i);-1!==e&&this.readerWaiters.splice(e,1),o(new t("RWMutex rlock timed out"))}),e)}))])}runlock(){if(this._readers<=0)throw new Error("RWMutex: runlock called without a matching rlock");this._readers--,this._tryWakeWriter()}async lock(e){if(!this._writeLocked&&0===this._readers)return void(this._writeLocked=!0);let i;this._pendingWriters++;const r=new Promise((e=>i=e));if(this.writerWaiters.push(i),null==e)return void await r;let s;await Promise.race([r.then((()=>clearTimeout(s))),new Promise(((r,o)=>{s=setTimeout((()=>{const e=this.writerWaiters.indexOf(i);-1!==e&&(this.writerWaiters.splice(e,1),this._pendingWriters--),o(new t("RWMutex lock timed out"))}),e)}))])}unlock(){if(!this._writeLocked)throw new Error("RWMutex: unlock called without a matching lock");this._writeLocked=!1,this._tryWakeWriter()||this._wakeAllReaders()}async read(e,t){await this.rlock(t);try{return await e()}finally{this.runlock()}}async write(e,t){await this.lock(t);try{return await e()}finally{this.unlock()}}writeLocked(){return this._writeLocked}readers(){return this._readers}pendingWriters(){return this._pendingWriters}pendingReaders(){return this.readerWaiters.length}_tryWakeWriter(){if(this._writeLocked||this._readers>0)return!1;const e=this.writerWaiters.shift();return!!e&&(this._writeLocked=!0,this._pendingWriters--,this._schedule(e),!0)}_wakeAllReaders(){const e=this.readerWaiters.splice(0);if(0!==e.length){this._readers+=e.length;for(const t of e)this._schedule(t)}}_schedule(e){"microtask"===this._yieldMode?queueMicrotask(e):setTimeout(e,0)}},l=class{_delay;_leading;_timer;_pendingFn;_pendingResolvers=[];_leadingFired=!1;constructor(e){this._delay=e?.delay??300,this._leading=e?.leading??!1}do(e){return new Promise((t=>{this._enqueue(e,t)}))}fire(e){this._enqueue(e,void 0)}_enqueue(e,t){if(this._pendingFn=e,t&&this._pendingResolvers.push(t),this._leading&&!this._leadingFired)return this._leadingFired=!0,this._fire(),clearTimeout(this._timer),void(this._timer=setTimeout((()=>{this._leadingFired=!1,void 0!==this._pendingFn&&this._fire()}),this._delay));clearTimeout(this._timer),this._timer=setTimeout((()=>{this._leadingFired=!1,this._fire()}),this._delay)}cancel(){clearTimeout(this._timer),this._timer=void 0,this._pendingFn=void 0,this._leadingFired=!1;const e=this._pendingResolvers.splice(0);for(const t of e)t({status:"cancelled"})}async flush(){return this._pendingFn?(clearTimeout(this._timer),this._timer=void 0,this._leadingFired=!1,this._fire()):null}pending(){return void 0!==this._pendingFn}async _fire(){const e=this._pendingFn,t=this._pendingResolvers.splice(0);let i;this._pendingFn=void 0;try{i={status:"ok",value:await e()}}catch(e){i={status:"error",error:e}}for(const e of t)e(i);return i}};export{l as Debouncer,a as Latch,r as Mutex,o as Once,h as RWMutex,s as Semaphore,n as Serializer};
|