@asaidimu/utils-sync 1.1.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -202,7 +202,7 @@ const result = await once.do(() => 42); // works with sync return
202
202
  #### Checking state without awaiting
203
203
 
204
204
  ```typescript
205
- if (once.isReady()) {
205
+ if (once.ready()) {
206
206
  const { value, error } = once.peek();
207
207
  // safely inspect cached result
208
208
  }
@@ -220,13 +220,13 @@ if (once.isReady()) {
220
220
  | Method | Return type | Description |
221
221
  |-----------------------------------|-------------------------------------|---------------------------------------------------------------------------------------------------|
222
222
  | `do(fn, timeout?)` | `Promise<OnceResult<T>>` | Executes `fn` once. Returns `{ value, error }` (unless `throws:true`). Timeout covers lock + execution. |
223
- | `isReady()` | `boolean` | `true` if operation has completed (success or non‑retryable failure) and no execution is running. |
223
+ | `ready()` | `boolean` | `true` if operation has completed (success or non‑retryable failure) and no execution is running. |
224
224
  | `running()` | `boolean` | `true` if the operation is currently executing. |
225
225
  | `peek()` | `OnceResult<T>` | Returns current cached `{ value, error }` without waiting. |
226
226
  | `get()` | `T \| null` | Returns cached value if done, otherwise throws. Throws cached error if present. |
227
227
  | `reset()` | `void` | Clears state – next `do()` will run again. |
228
228
  | `done()` | `boolean` | `true` if finished (success or final failure). |
229
- | `resolved()` | `Promise<OnceResult<T>> \| null` | Returns the underlying promise if running or done, otherwise `null`. |
229
+ | `current()` | `Promise<OnceResult<T>> \| null` | Returns the underlying promise if running, otherwise `null`. |
230
230
 
231
231
  ---
232
232
 
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;
@@ -103,12 +163,17 @@ declare class Once<T = void> {
103
163
  doSync(fn: () => T): OnceResult<T>;
104
164
  /**
105
165
  * Returns true if the operation has completed (success or non-retryable failure)
106
- * and is not currently executing.
107
- * 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()`.
108
169
  */
109
- isReady(): boolean;
170
+ ready(): boolean;
110
171
  /**
111
172
  * Returns true if the operation is currently executing.
173
+ *
174
+ * Uses ready() 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.
112
177
  */
113
178
  running(): boolean;
114
179
  /**
@@ -122,16 +187,27 @@ declare class Once<T = void> {
122
187
  get(): T | null;
123
188
  /**
124
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.
125
193
  */
126
194
  reset(): void;
127
- /**
128
- * Returns the underlying promise if running or done, null otherwise.
129
- */
130
- resolved(): Promise<OnceResult<T>> | null;
131
195
  /**
132
196
  * Returns true if the operation has finished (success or final failure).
133
197
  */
134
198
  done(): boolean;
199
+ /**
200
+ * Returns the in-flight execution promise if one is currently running, null otherwise.
201
+ *
202
+ * Use this to join an in-progress operation without triggering a new one.
203
+ */
204
+ current(): Promise<OnceResult<T>> | null;
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
+ */
135
211
  private _awaitWithTimeout;
136
212
  }
137
213
  type SerializerResult<T> = {
@@ -146,8 +222,8 @@ interface SerializerOptions {
146
222
  capacity?: number;
147
223
  /**
148
224
  * Yield mode for the internal mutex. Defaults to "macrotask" for
149
- * coarse-grained serializers (invalidationSerializer). Use "microtask"
150
- * for fine-grained latency-sensitive serializers (streamSerializer).
225
+ * coarse-grained serializers. Use "microtask" for fine-grained
226
+ * latency-sensitive serializers.
151
227
  */
152
228
  yieldMode?: "macrotask" | "microtask";
153
229
  }
@@ -161,6 +237,7 @@ declare class Serializer<T = void> {
161
237
  private _done;
162
238
  private _lastValue;
163
239
  private _lastError;
240
+ private _hasRun;
164
241
  constructor(options?: SerializerOptions);
165
242
  /**
166
243
  * Enqueue a function to be executed after all previous tasks complete.
@@ -170,8 +247,18 @@ declare class Serializer<T = void> {
170
247
  * @returns Object containing the value or error.
171
248
  */
172
249
  do(fn: () => Promise<T> | T, timeout?: number): Promise<SerializerResult<T | null>>;
173
- /** 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
+ */
174
257
  peek(): SerializerResult<T | null>;
258
+ /**
259
+ * Returns true if at least one task has been executed (successfully or not).
260
+ */
261
+ hasRun(): boolean;
175
262
  /**
176
263
  * Permanently closes the serializer.
177
264
  * Subsequent calls to `do()` will fail immediately with SerializerExecutionDone.
@@ -182,5 +269,261 @@ declare class Serializer<T = void> {
182
269
  /** Returns true if a task is currently executing. */
183
270
  running(): boolean;
184
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
+ }
185
528
 
186
- 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;
@@ -103,12 +163,17 @@ declare class Once<T = void> {
103
163
  doSync(fn: () => T): OnceResult<T>;
104
164
  /**
105
165
  * Returns true if the operation has completed (success or non-retryable failure)
106
- * and is not currently executing.
107
- * 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()`.
108
169
  */
109
- isReady(): boolean;
170
+ ready(): boolean;
110
171
  /**
111
172
  * Returns true if the operation is currently executing.
173
+ *
174
+ * Uses ready() 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.
112
177
  */
113
178
  running(): boolean;
114
179
  /**
@@ -122,16 +187,27 @@ declare class Once<T = void> {
122
187
  get(): T | null;
123
188
  /**
124
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.
125
193
  */
126
194
  reset(): void;
127
- /**
128
- * Returns the underlying promise if running or done, null otherwise.
129
- */
130
- resolved(): Promise<OnceResult<T>> | null;
131
195
  /**
132
196
  * Returns true if the operation has finished (success or final failure).
133
197
  */
134
198
  done(): boolean;
199
+ /**
200
+ * Returns the in-flight execution promise if one is currently running, null otherwise.
201
+ *
202
+ * Use this to join an in-progress operation without triggering a new one.
203
+ */
204
+ current(): Promise<OnceResult<T>> | null;
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
+ */
135
211
  private _awaitWithTimeout;
136
212
  }
137
213
  type SerializerResult<T> = {
@@ -146,8 +222,8 @@ interface SerializerOptions {
146
222
  capacity?: number;
147
223
  /**
148
224
  * Yield mode for the internal mutex. Defaults to "macrotask" for
149
- * coarse-grained serializers (invalidationSerializer). Use "microtask"
150
- * for fine-grained latency-sensitive serializers (streamSerializer).
225
+ * coarse-grained serializers. Use "microtask" for fine-grained
226
+ * latency-sensitive serializers.
151
227
  */
152
228
  yieldMode?: "macrotask" | "microtask";
153
229
  }
@@ -161,6 +237,7 @@ declare class Serializer<T = void> {
161
237
  private _done;
162
238
  private _lastValue;
163
239
  private _lastError;
240
+ private _hasRun;
164
241
  constructor(options?: SerializerOptions);
165
242
  /**
166
243
  * Enqueue a function to be executed after all previous tasks complete.
@@ -170,8 +247,18 @@ declare class Serializer<T = void> {
170
247
  * @returns Object containing the value or error.
171
248
  */
172
249
  do(fn: () => Promise<T> | T, timeout?: number): Promise<SerializerResult<T | null>>;
173
- /** 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
+ */
174
257
  peek(): SerializerResult<T | null>;
258
+ /**
259
+ * Returns true if at least one task has been executed (successfully or not).
260
+ */
261
+ hasRun(): boolean;
175
262
  /**
176
263
  * Permanently closes the serializer.
177
264
  * Subsequent calls to `do()` will fail immediately with SerializerExecutionDone.
@@ -182,5 +269,261 @@ declare class Serializer<T = void> {
182
269
  /** Returns true if a task is currently executing. */
183
270
  running(): boolean;
184
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
+ }
185
528
 
186
- 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")))}doSync(t){if(this._done){if(this.throws&&this._error)throw this._error;return this.peek()}if(this.promise){const t=new Error("Cannot execute doSync while an async operation is pending.");if(this.throws)throw t;return{value:null,error:t}}if(!this.mutex.tryLock()){const t=new Error("Cannot execute doSync: lock is currently held.");if(this.throws)throw t;return{value:null,error:t}}if(this.promise||this._done){if(this.mutex.unlock(),this._done){if(this.throws&&this._error)throw this._error;return this.peek()}const t=new Error("Cannot execute doSync while an async operation is pending.");if(this.throws)throw t;return{value:null,error:t}}try{const e=t();this._value=e,this._done=!0}catch(t){if(this._error=t,this.retry||(this._done=!0),this.throws)throw t}finally{this.mutex.unlock()}return this.peek()}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()}ready(){return this._done&&null===this.promise}running(){return null!==this.promise&&!this.ready()}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}done(){return this._done}current(){return this.promise}_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")))}doSync(t){if(this._done){if(this.throws&&this._error)throw this._error;return this.peek()}if(this.promise){const t=new Error("Cannot execute doSync while an async operation is pending.");if(this.throws)throw t;return{value:null,error:t}}if(!this.mutex.tryLock()){const t=new Error("Cannot execute doSync: lock is currently held.");if(this.throws)throw t;return{value:null,error:t}}if(this.promise||this._done){if(this.mutex.unlock(),this._done){if(this.throws&&this._error)throw this._error;return this.peek()}const t=new Error("Cannot execute doSync while an async operation is pending.");if(this.throws)throw t;return{value:null,error:t}}try{const e=t();this._value=e,this._done=!0}catch(t){if(this._error=t,this.retry||(this._done=!0),this.throws)throw t}finally{this.mutex.unlock()}return this.peek()}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()}ready(){return this._done&&null===this.promise}running(){return null!==this.promise&&!this.ready()}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}done(){return this._done}current(){return this.promise}_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.1",
3
+ "version": "2.0.1",
4
4
  "description": "A collection of sync utilities.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",