@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 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 invalidationSerializer and other
15
- * coarse-grained serializers.
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. buildOnce, streamSerializer where builds are
21
- * infrequent and latency matters more than fairness).
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 is not currently executing.
98
- * Replaces the previous `done() && !running()` two-call pattern.
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 underlying promise if running or done, null otherwise.
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
- resolved(): Promise<OnceResult<T>> | null;
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 (invalidationSerializer). Use "microtask"
141
- * for fine-grained latency-sensitive serializers (streamSerializer).
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
- /** Returns the result of the last successful execution. */
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 invalidationSerializer and other
15
- * coarse-grained serializers.
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. buildOnce, streamSerializer where builds are
21
- * infrequent and latency matters more than fairness).
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 is not currently executing.
98
- * Replaces the previous `done() && !running()` two-call pattern.
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 underlying promise if running or done, null otherwise.
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
- resolved(): Promise<OnceResult<T>> | null;
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 (invalidationSerializer). Use "microtask"
141
- * for fine-grained latency-sensitive serializers (streamSerializer).
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
- /** Returns the result of the last successful execution. */
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 t=class t extends Error{constructor(e,r){super(e,{cause:r}),this.name="SyncError",Object.setPrototypeOf(this,t.prototype)}},e=class extends t{constructor(t){super(`[ArtifactContainer] Operation timed out: ${t}`)}},r=class extends t{constructor(t){super("[Serializer] The serializer has been marked as done!",t)}},i=class{_locked=!1;_capacity;_yieldMode;waiters=[];constructor(t){this._capacity=t?.capacity??1/0,this._yieldMode=t?.yieldMode??"macrotask"}async lock(t){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 r;const i=new Promise((t=>r=t));this.waiters.push(r),null!=t?await Promise.race([i,new Promise(((i,s)=>setTimeout((()=>{const t=this.waiters.indexOf(r);-1!==t&&this.waiters.splice(t,1),s(new e("Mutex lock timed out"))}),t)))]):await i}tryLock(){return!this._locked&&(this._locked=!0,!0)}unlock(){if(!this._locked)throw new Error("Mutex is not locked");const t=this.waiters.shift();t?"microtask"===this._yieldMode?queueMicrotask(t):setTimeout(t,0):this._locked=!1}locked(){return this._locked}pending(){return this.waiters.length}};exports.Mutex=i,exports.Once=class{mutex=new i({yieldMode:"microtask"});promise=null;_value=null;_error;_done=!1;retry;throws;constructor({retry:t,throws:e}={}){this.retry=Boolean(t),this.throws=Boolean(e)}async do(t,e){return this._done?this.peek():this.promise?this._awaitWithTimeout(this.promise,e,"Once do() timed out"):(await this.mutex.lock(),this.promise?(this.mutex.unlock(),this._awaitWithTimeout(this.promise,e,"Once do() timed out")):(this.promise=(async()=>{try{const e=await t();this._value=e,this._done=!0}catch(t){if(this._error=t,this.retry||(this._done=!0),this.throws)throw t}finally{this.promise=null}return this.peek()})(),this.mutex.unlock(),this._awaitWithTimeout(this.promise,e,"Once do() timed out")))}isReady(){return this._done&&null===this.promise}running(){return null!==this.promise&&!this._done}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(){this._done=!1,this.promise=null,this._value=null,this._error=void 0}resolved(){return this.promise}done(){return this._done}_awaitWithTimeout(t,r,i="Operation timed out"){return null==r?t:Promise.race([t,new Promise(((t,s)=>setTimeout((()=>s(new e(i))),r)))])}},exports.Serializer=class{mutex;_done=!1;_lastValue=null;_lastError=void 0;constructor(t){this.mutex=new i({capacity:t?.capacity??1e3,yieldMode:t?.yieldMode??"macrotask"})}async do(t,e){if(this._done)return{value:null,error:new r};try{await this.mutex.lock(e)}catch(t){return{value:null,error:t}}let i,s=null;try{if(this._done)throw new r;s=await t(),this._lastValue=s,this._lastError=void 0}catch(t){i=t,this._lastError=t}finally{this.mutex.unlock()}return{value:s,error:i}}peek(){return{value:this._lastValue,error:this._lastError}}close(){this._done=!0}pending(){return this.mutex.pending()}running(){return this.mutex.locked()}};
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 t=class t extends Error{constructor(e,r){super(e,{cause:r}),this.name="SyncError",Object.setPrototypeOf(this,t.prototype)}},e=class extends t{constructor(t){super(`[ArtifactContainer] Operation timed out: ${t}`)}},r=class extends t{constructor(t){super("[Serializer] The serializer has been marked as done!",t)}},i=class{_locked=!1;_capacity;_yieldMode;waiters=[];constructor(t){this._capacity=t?.capacity??1/0,this._yieldMode=t?.yieldMode??"macrotask"}async lock(t){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 r;const i=new Promise((t=>r=t));this.waiters.push(r),null!=t?await Promise.race([i,new Promise(((i,s)=>setTimeout((()=>{const t=this.waiters.indexOf(r);-1!==t&&this.waiters.splice(t,1),s(new e("Mutex lock timed out"))}),t)))]):await i}tryLock(){return!this._locked&&(this._locked=!0,!0)}unlock(){if(!this._locked)throw new Error("Mutex is not locked");const t=this.waiters.shift();t?"microtask"===this._yieldMode?queueMicrotask(t):setTimeout(t,0):this._locked=!1}locked(){return this._locked}pending(){return this.waiters.length}},s=class{mutex=new i({yieldMode:"microtask"});promise=null;_value=null;_error;_done=!1;retry;throws;constructor({retry:t,throws:e}={}){this.retry=Boolean(t),this.throws=Boolean(e)}async do(t,e){return this._done?this.peek():this.promise?this._awaitWithTimeout(this.promise,e,"Once do() timed out"):(await this.mutex.lock(),this.promise?(this.mutex.unlock(),this._awaitWithTimeout(this.promise,e,"Once do() timed out")):(this.promise=(async()=>{try{const e=await t();this._value=e,this._done=!0}catch(t){if(this._error=t,this.retry||(this._done=!0),this.throws)throw t}finally{this.promise=null}return this.peek()})(),this.mutex.unlock(),this._awaitWithTimeout(this.promise,e,"Once do() timed out")))}isReady(){return this._done&&null===this.promise}running(){return null!==this.promise&&!this._done}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(){this._done=!1,this.promise=null,this._value=null,this._error=void 0}resolved(){return this.promise}done(){return this._done}_awaitWithTimeout(t,r,i="Operation timed out"){return null==r?t:Promise.race([t,new Promise(((t,s)=>setTimeout((()=>s(new e(i))),r)))])}},o=class{mutex;_done=!1;_lastValue=null;_lastError=void 0;constructor(t){this.mutex=new i({capacity:t?.capacity??1e3,yieldMode:t?.yieldMode??"macrotask"})}async do(t,e){if(this._done)return{value:null,error:new r};try{await this.mutex.lock(e)}catch(t){return{value:null,error:t}}let i,s=null;try{if(this._done)throw new r;s=await t(),this._lastValue=s,this._lastError=void 0}catch(t){i=t,this._lastError=t}finally{this.mutex.unlock()}return{value:s,error:i}}peek(){return{value:this._lastValue,error:this._lastError}}close(){this._done=!0}pending(){return this.mutex.pending()}running(){return this.mutex.locked()}};export{i as Mutex,s as Once,o as Serializer};
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asaidimu/utils-sync",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "A collection of sync utilities.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",