@asaidimu/utils-sync 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +413 -0
- package/index.d.mts +177 -0
- package/index.d.ts +177 -0
- package/index.js +1 -0
- package/index.mjs +1 -0
- package/package.json +63 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Saidimu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# `@asaidimu/utils-sync`
|
|
2
|
+
|
|
3
|
+
> **Synchronization primitives for TypeScript/JavaScript** – Mutex, Once, and Serializer utilities with fine‑grained concurrency control.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@asaidimu/utils-sync)
|
|
6
|
+
[](https://github.com/asaidimu/erp-utils/blob/main/LICENSE)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://vitest.dev/)
|
|
9
|
+
|
|
10
|
+
## Table of Contents
|
|
11
|
+
|
|
12
|
+
- [Overview & Features](#overview--features)
|
|
13
|
+
- [Installation & Setup](#installation--setup)
|
|
14
|
+
- [Usage Documentation](#usage-documentation)
|
|
15
|
+
- [Mutex](#mutex)
|
|
16
|
+
- [Once](#once)
|
|
17
|
+
- [Serializer](#serializer)
|
|
18
|
+
- [Project Architecture](#project-architecture)
|
|
19
|
+
- [Development & Contributing](#development--contributing)
|
|
20
|
+
- [Additional Information](#additional-information)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Overview & Features
|
|
25
|
+
|
|
26
|
+
Modern JavaScript applications often face subtle concurrency issues – race conditions, duplicate work, or unexpected interleaving of async operations. `@asaidimu/utils-sync` provides three battle‑tested primitives to tame asynchronous chaos:
|
|
27
|
+
|
|
28
|
+
- **`Mutex`** – A mutual exclusion lock that allows only one task at a time to access a shared resource. Configurable handoff scheduling (microtask vs macrotask) prevents microtask starvation under heavy contention.
|
|
29
|
+
- **`Once`** – Guarantees that a given asynchronous operation runs exactly once, even when called concurrently from many places. Ideal for lazy initialisation, cache population, or one‑time setup. Supports optional retry on failure and sync/async functions.
|
|
30
|
+
- **`Serializer`** – Processes queued tasks sequentially (FIFO) while maintaining the last successful result. Built‑in backpressure protection and the ability to permanently close the queue. Perfect for rate‑limited APIs, write serialisation, or sequential job processing.
|
|
31
|
+
|
|
32
|
+
All utilities are written in strict TypeScript, fully typed, and come with zero runtime dependencies.
|
|
33
|
+
|
|
34
|
+
### Key Features
|
|
35
|
+
|
|
36
|
+
- **Mutual exclusion** – `Mutex` with optional timeout and queue capacity limits.
|
|
37
|
+
- **Configurable yield behaviour** – Choose `"macrotask"` (default, prevents starvation) or `"microtask"` (zero‑delay handoff) per instance.
|
|
38
|
+
- **Once‑only execution** – `Once` deduplicates concurrent calls, caches success/failure, and optionally retries on error.
|
|
39
|
+
- **Sequential task processing** – `Serializer` maintains order, provides last‑result peeking, and can be closed permanently.
|
|
40
|
+
- **Timeout support** – All operations accept a timeout parameter (lock acquisition + execution).
|
|
41
|
+
- **Backpressure** – Configurable queue size to prevent uncontrolled growth.
|
|
42
|
+
- **Tiny & focused** – No external dependencies, tree‑shakeable exports.
|
|
43
|
+
- **First‑class TypeScript** – Generics, error types, and accurate return types.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Installation & Setup
|
|
48
|
+
|
|
49
|
+
### Prerequisites
|
|
50
|
+
|
|
51
|
+
- **Node.js** 18+ (or any modern environment with `Promise`, `queueMicrotask`, and `setTimeout`)
|
|
52
|
+
- **TypeScript** 4.7+ (if using types, but not required)
|
|
53
|
+
|
|
54
|
+
### Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install @asaidimu/utils-sync
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pnpm add @asaidimu/utils-sync
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
yarn add @asaidimu/utils-sync
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Verification
|
|
69
|
+
|
|
70
|
+
After installation, you can test that the library works correctly:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { Mutex } from '@asaidimu/utils-sync';
|
|
74
|
+
|
|
75
|
+
const mutex = new Mutex();
|
|
76
|
+
console.log(mutex.locked()); // false
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
If the import runs without errors, the package is ready.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Usage Documentation
|
|
84
|
+
|
|
85
|
+
All examples assume ES module import syntax:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { Mutex, Once, Serializer } from '@asaidimu/utils-sync';
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For CommonJS:
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
const { Mutex, Once, Serializer } = require('@asaidimu/utils-sync');
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### Mutex
|
|
100
|
+
|
|
101
|
+
A mutual exclusion lock. Use it to protect critical sections where only one async operation should run at a time.
|
|
102
|
+
|
|
103
|
+
#### Basic example
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const mutex = new Mutex();
|
|
107
|
+
|
|
108
|
+
async function criticalSection() {
|
|
109
|
+
await mutex.lock();
|
|
110
|
+
try {
|
|
111
|
+
// Only one caller executes this block at a time
|
|
112
|
+
await doSomething();
|
|
113
|
+
} finally {
|
|
114
|
+
mutex.unlock();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### With timeout
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
try {
|
|
123
|
+
await mutex.lock(1000); // wait max 1 second
|
|
124
|
+
// ... work ...
|
|
125
|
+
mutex.unlock();
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (err instanceof TimeoutError) {
|
|
128
|
+
console.log('Could not acquire lock in time');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### Non‑blocking attempt
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
if (mutex.tryLock()) {
|
|
137
|
+
try {
|
|
138
|
+
// lock acquired immediately
|
|
139
|
+
} finally {
|
|
140
|
+
mutex.unlock();
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
// lock was already held – do something else
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### Options
|
|
148
|
+
|
|
149
|
+
| Option | Type | Default | Description |
|
|
150
|
+
|--------------|--------------------------|---------------|-----------------------------------------------------------------------------------------------|
|
|
151
|
+
| `capacity` | `number` | `Infinity` | Max pending waiters. If exceeded, `lock()` throws an error. |
|
|
152
|
+
| `yieldMode` | `"macrotask"` \| `"microtask"` | `"macrotask"` | `"macrotask"` yields via `setTimeout(…,0)` (prevents starvation). `"microtask"` uses `queueMicrotask` for lower latency. |
|
|
153
|
+
|
|
154
|
+
#### API
|
|
155
|
+
|
|
156
|
+
| Method | Return type | Description |
|
|
157
|
+
|------------------------------|-----------------------|---------------------------------------------------------------------------------------------------------|
|
|
158
|
+
| `lock(timeout?: number)` | `Promise<void>` | Acquire lock, waiting if necessary. Throws `TimeoutError` if timeout elapses or queue is full. |
|
|
159
|
+
| `tryLock()` | `boolean` | Attempt to acquire lock without waiting. Returns `true` if acquired. |
|
|
160
|
+
| `unlock()` | `void` | Release the lock. Throws if not locked. Schedules next waiter according to `yieldMode`. |
|
|
161
|
+
| `locked()` | `boolean` | Returns `true` if the lock is currently held. |
|
|
162
|
+
| `pending()` | `number` | Number of tasks waiting for the lock. |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### Once
|
|
167
|
+
|
|
168
|
+
Guarantees a function runs exactly once, even when many callers invoke `do()` concurrently. The result (or error) is cached and returned to all future callers.
|
|
169
|
+
|
|
170
|
+
#### Basic example
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
const once = new Once<string>();
|
|
174
|
+
|
|
175
|
+
async function getConfig() {
|
|
176
|
+
const result = await once.do(async () => {
|
|
177
|
+
const res = await fetch('/api/config');
|
|
178
|
+
return res.json();
|
|
179
|
+
});
|
|
180
|
+
// result.value contains the config, or result.error if failed
|
|
181
|
+
return result.value;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### With retry on failure
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const once = new Once<string>({ retry: true });
|
|
189
|
+
|
|
190
|
+
// If the first attempt fails, the next call will retry
|
|
191
|
+
await once.do(failingFn); // fails, but _done = false
|
|
192
|
+
await once.do(successFn); // runs again, succeeds, caches result
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### Synchronous functions
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
const once = new Once<number>();
|
|
199
|
+
const result = await once.do(() => 42); // works with sync return
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### Checking state without awaiting
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
if (once.isReady()) {
|
|
206
|
+
const { value, error } = once.peek();
|
|
207
|
+
// safely inspect cached result
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### Options
|
|
212
|
+
|
|
213
|
+
| Option | Type | Default | Description |
|
|
214
|
+
|-----------|-----------|---------|----------------------------------------------------------------------------------|
|
|
215
|
+
| `retry` | `boolean` | `false` | If `true`, a failed execution does **not** mark the instance as done – next call will retry. |
|
|
216
|
+
| `throws` | `boolean` | `false` | If `true`, the `do()` method will **throw** the error instead of returning it in the result object. |
|
|
217
|
+
|
|
218
|
+
#### API
|
|
219
|
+
|
|
220
|
+
| Method | Return type | Description |
|
|
221
|
+
|-----------------------------------|-------------------------------------|---------------------------------------------------------------------------------------------------|
|
|
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. |
|
|
224
|
+
| `running()` | `boolean` | `true` if the operation is currently executing. |
|
|
225
|
+
| `peek()` | `OnceResult<T>` | Returns current cached `{ value, error }` without waiting. |
|
|
226
|
+
| `get()` | `T \| null` | Returns cached value if done, otherwise throws. Throws cached error if present. |
|
|
227
|
+
| `reset()` | `void` | Clears state – next `do()` will run again. |
|
|
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`. |
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### Serializer
|
|
234
|
+
|
|
235
|
+
Processes tasks sequentially (FIFO order). Each task runs only after all previous tasks have completed. Use it to serialise writes to a file, throttle API calls, or enforce ordering.
|
|
236
|
+
|
|
237
|
+
#### Basic example
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const serializer = new Serializer<string>();
|
|
241
|
+
|
|
242
|
+
async function log(message: string) {
|
|
243
|
+
const result = await serializer.do(async () => {
|
|
244
|
+
await appendToFile('log.txt', message);
|
|
245
|
+
return message;
|
|
246
|
+
});
|
|
247
|
+
return result.value; // last successful result
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### Handling failures
|
|
252
|
+
|
|
253
|
+
Even if a task fails, the serializer continues processing the next queued tasks:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
await serializer.do(failingFn); // returns { error: ... }
|
|
257
|
+
await serializer.do(successfulFn); // still runs
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### Peeking at the last result
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
const { value, error } = serializer.peek();
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### Closing the serializer permanently
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
serializer.close();
|
|
270
|
+
const result = await serializer.do(anyFn);
|
|
271
|
+
// result.error instanceof SerializerExecutionDone
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
#### Options
|
|
275
|
+
|
|
276
|
+
| Option | Type | Default | Description |
|
|
277
|
+
|--------------|--------------------------|------------|------------------------------------------------------------------|
|
|
278
|
+
| `capacity` | `number` | `1000` | Max pending tasks. When full, `do()` returns an error immediately. |
|
|
279
|
+
| `yieldMode` | `"macrotask"` \| `"microtask"` | `"macrotask"` | Handoff scheduling for the internal mutex. Default prevents microtask starvation. |
|
|
280
|
+
|
|
281
|
+
#### API
|
|
282
|
+
|
|
283
|
+
| Method | Return type | Description |
|
|
284
|
+
|-------------------------------|--------------------------------------|------------------------------------------------------------------------------------------------------|
|
|
285
|
+
| `do(fn, timeout?)` | `Promise<SerializerResult<T\|null>>` | Enqueues `fn`. Returns `{ value, error }`. If closed or queue full, error is `SerializerExecutionDone`. |
|
|
286
|
+
| `peek()` | `SerializerResult<T\|null>` | Returns the last successful result or last error. |
|
|
287
|
+
| `close()` | `void` | Permanently closes the serializer. All subsequent `do()` calls fail immediately. |
|
|
288
|
+
| `pending()` | `number` | Number of tasks waiting in the queue. |
|
|
289
|
+
| `running()` | `boolean` | `true` if a task is currently executing. |
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Project Architecture
|
|
294
|
+
|
|
295
|
+
The library is written in **TypeScript** and follows a simple, functional‑object design. Each class is independent and does not rely on shared global state.
|
|
296
|
+
|
|
297
|
+
### Core Components
|
|
298
|
+
|
|
299
|
+
- **`Mutex`** – Implements the lock with a FIFO waiter queue. Handoff uses either `setTimeout` (macrotask) or `queueMicrotask` to give callers control over fairness vs. latency.
|
|
300
|
+
- **`Once`** – Built on top of `Mutex` with `microtask` yield mode for minimal overhead. Tracks execution state (`_done`, `_value`, `_error`) and returns a cached promise to concurrent callers.
|
|
301
|
+
- **`Serializer`** – Also uses `Mutex` (default `macrotask` yield) to serialise work. Maintains the last result and supports backpressure via `capacity`.
|
|
302
|
+
|
|
303
|
+
### Data Flow
|
|
304
|
+
|
|
305
|
+
1. **Mutex** – Callers invoke `lock()`. If unlocked, they acquire immediately. Otherwise they are added to `waiters`. When `unlock()` is called, the next waiter is scheduled according to `yieldMode`.
|
|
306
|
+
2. **Once** – First caller acquires the mutex, runs the function, and stores the promise. Later callers see the existing promise and await it directly (no mutex contention). After completion, the promise is cleared and `_done` is set.
|
|
307
|
+
3. **Serializer** – Each `do()` call attempts to lock the internal mutex. Only one task holds the lock at a time. When a task finishes (success or error), the lock is released, allowing the next queued task to run.
|
|
308
|
+
|
|
309
|
+
### Extension Points
|
|
310
|
+
|
|
311
|
+
The library is designed to be used as‑is, but you can easily compose the primitives:
|
|
312
|
+
|
|
313
|
+
- Use `Mutex` to build your own synchronisation patterns (e.g., read‑write locks).
|
|
314
|
+
- Extend `Once` or `Serializer` by subclassing (both are standard ES6 classes).
|
|
315
|
+
- Replace the underlying promise scheduling by providing a custom `Mutex` with different `yieldMode` logic (though the built‑in modes cover most needs).
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Development & Contributing
|
|
320
|
+
|
|
321
|
+
### Development Setup
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
git clone https://github.com/asaidimu/erp-utils.git
|
|
325
|
+
cd erp-utils/src/sync
|
|
326
|
+
npm install
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Scripts
|
|
330
|
+
|
|
331
|
+
| Command | Description |
|
|
332
|
+
|----------------------|--------------------------------------------------|
|
|
333
|
+
| `npm test` | Run tests once (Vitest) |
|
|
334
|
+
| `npm run test:watch` | Run tests in watch mode |
|
|
335
|
+
| `npm run test:browser` | Run tests in a browser environment (Vitest) |
|
|
336
|
+
|
|
337
|
+
### Testing
|
|
338
|
+
|
|
339
|
+
Tests are written with **Vitest** and cover:
|
|
340
|
+
|
|
341
|
+
- `Once` – deduplication, retry behaviour, state transitions, error handling.
|
|
342
|
+
- `Serializer` – FIFO ordering, backpressure, closing, error resilience.
|
|
343
|
+
- `Mutex` – locking, timeout, capacity, yield modes (implicitly tested via Serializer and Once).
|
|
344
|
+
|
|
345
|
+
To run the full suite:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
npm test
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Contributing Guidelines
|
|
352
|
+
|
|
353
|
+
1. **Fork** the repository and create a feature branch.
|
|
354
|
+
2. **Write tests** for any new functionality or bug fixes.
|
|
355
|
+
3. **Ensure existing tests pass** (`npm test`).
|
|
356
|
+
4. **Follow the existing code style** (Prettier / ESLint – see root of monorepo).
|
|
357
|
+
5. **Commit messages** should follow [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat: add timeout to Mutex`).
|
|
358
|
+
6. **Open a Pull Request** against the `main` branch.
|
|
359
|
+
|
|
360
|
+
### Issue Reporting
|
|
361
|
+
|
|
362
|
+
Report bugs or request features via [GitHub Issues](https://github.com/asaidimu/erp-utils/issues). Please include:
|
|
363
|
+
|
|
364
|
+
- A clear description of the problem.
|
|
365
|
+
- Minimal code to reproduce (if bug).
|
|
366
|
+
- Environment details (Node version, package manager, OS).
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Additional Information
|
|
371
|
+
|
|
372
|
+
### Troubleshooting
|
|
373
|
+
|
|
374
|
+
| Problem | Possible solution |
|
|
375
|
+
|----------------------------------------------|-------------------------------------------------------------------------------------------------------|
|
|
376
|
+
| `Mutex` lock never resolves | Check that `unlock()` is always called (e.g., use `try/finally`). |
|
|
377
|
+
| `Serializer` tasks stop running | Did you call `close()`? Once closed, all new tasks fail immediately. |
|
|
378
|
+
| `Once` returns stale error even after retry | Ensure `retry: true` is set. Without it, a failure marks `_done = true` and never retries. |
|
|
379
|
+
| `TimeoutError` when queue seems small | Increase `capacity` in `Mutex` or `Serializer` options. |
|
|
380
|
+
| Microtask starvation in high‑contention code | Set `yieldMode: "macrotask"` (default for `Serializer` and `Mutex`). `Once` uses microtask by design. |
|
|
381
|
+
|
|
382
|
+
### FAQ
|
|
383
|
+
|
|
384
|
+
**Q: Can I use `Once` with a synchronous function?**
|
|
385
|
+
Yes – `Once.do()` accepts both `() => Promise<T>` and `() => T`. Synchronous return values are automatically wrapped in a resolved promise.
|
|
386
|
+
|
|
387
|
+
**Q: What happens if `Once.do()` times out?**
|
|
388
|
+
The timeout applies to the entire operation (including waiting for the mutex and execution). If a timeout occurs, the `do()` call rejects with `TimeoutError`, but the background execution (if already started) continues. Future callers will receive the final result.
|
|
389
|
+
|
|
390
|
+
**Q: Is `Serializer` safe for long‑running tasks?**
|
|
391
|
+
Absolutely. Tasks run sequentially, so a long task will delay subsequent tasks. Use `timeout` if you need to enforce a maximum wait per task.
|
|
392
|
+
|
|
393
|
+
**Q: Can I reuse a `Once` instance after a non‑retryable failure?**
|
|
394
|
+
Yes – call `reset()` to clear the cached error and allow a fresh execution.
|
|
395
|
+
|
|
396
|
+
**Q: Does `Mutex` re‑entrant?**
|
|
397
|
+
No – attempting to `lock()` from the same execution context that already holds the lock will deadlock. Use a single lock acquisition per critical section.
|
|
398
|
+
|
|
399
|
+
### Changelog & Roadmap
|
|
400
|
+
|
|
401
|
+
See the [CHANGELOG.md](https://github.com/asaidimu/erp-utils/blob/main/CHANGELOG.md) for version history. Future plans include:
|
|
402
|
+
|
|
403
|
+
- `Semaphore` implementation.
|
|
404
|
+
- `AsyncCondition` variable.
|
|
405
|
+
- `DebouncedSerializer` for coalescing rapid consecutive calls.
|
|
406
|
+
|
|
407
|
+
### License
|
|
408
|
+
|
|
409
|
+
[MIT](https://github.com/asaidimu/erp-utils/blob/main/LICENSE) © Saidimu
|
|
410
|
+
|
|
411
|
+
### Acknowledgments
|
|
412
|
+
|
|
413
|
+
Inspired by similar synchronisation primitives in Rust (`Mutex`, `OnceCell`), Go (`sync.Mutex`), and the classic async patterns of the JavaScript ecosystem. Built with ❤️ using TypeScript and Vitest.
|
package/index.d.mts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
interface MutexOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Maximum number of pending requests allowed in the queue.
|
|
4
|
+
* If exceeded, tryLock/lock will fail.
|
|
5
|
+
* @default Infinity
|
|
6
|
+
*/
|
|
7
|
+
capacity?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Controls how lock handoff is scheduled when a waiter is unblocked.
|
|
10
|
+
*
|
|
11
|
+
* - `"macrotask"` (default): Uses setTimeout(fn, 0) to yield to the event
|
|
12
|
+
* loop between handoffs. Prevents microtask starvation under heavy
|
|
13
|
+
* contention — I/O, rendering, and other macrotasks can run between
|
|
14
|
+
* lock acquisitions. Use for invalidationSerializer and other
|
|
15
|
+
* coarse-grained serializers.
|
|
16
|
+
*
|
|
17
|
+
* - `"microtask"`: Uses queueMicrotask(fn) for handoff. Near-zero latency
|
|
18
|
+
* between acquisitions — no macrotask delay. Safe when you need
|
|
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).
|
|
22
|
+
*/
|
|
23
|
+
yieldMode?: "macrotask" | "microtask";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A mutual exclusion lock.
|
|
27
|
+
* Allows only one execution context to access a resource at a time.
|
|
28
|
+
*
|
|
29
|
+
* Yield mode is configurable per instance:
|
|
30
|
+
* - "macrotask" (default): yields between handoffs, preventing microtask starvation.
|
|
31
|
+
* - "microtask": zero-delay handoff for latency-sensitive paths.
|
|
32
|
+
*/
|
|
33
|
+
declare class Mutex {
|
|
34
|
+
private _locked;
|
|
35
|
+
private _capacity;
|
|
36
|
+
private _yieldMode;
|
|
37
|
+
private waiters;
|
|
38
|
+
constructor(options?: MutexOptions);
|
|
39
|
+
/**
|
|
40
|
+
* Acquires the lock. If already held, waits until released or timeout reached.
|
|
41
|
+
*
|
|
42
|
+
* @param timeout - Optional maximum wait time in milliseconds.
|
|
43
|
+
* @throws {TimeoutError} If the lock cannot be acquired within the timeout.
|
|
44
|
+
* @throws {Error} If the wait queue is full (backpressure).
|
|
45
|
+
*/
|
|
46
|
+
lock(timeout?: number): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Attempts to acquire the lock without waiting.
|
|
49
|
+
* @returns `true` if the lock was acquired, `false` otherwise.
|
|
50
|
+
*/
|
|
51
|
+
tryLock(): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Releases the lock, scheduling the next waiter according to yieldMode.
|
|
54
|
+
* @throws {Error} If the mutex is not currently locked.
|
|
55
|
+
*/
|
|
56
|
+
unlock(): void;
|
|
57
|
+
/** Returns true if the mutex is currently locked. */
|
|
58
|
+
locked(): boolean;
|
|
59
|
+
/** Returns the number of operations waiting for the lock. */
|
|
60
|
+
pending(): number;
|
|
61
|
+
}
|
|
62
|
+
type OnceResult<T> = {
|
|
63
|
+
value: T | null;
|
|
64
|
+
error?: unknown;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Ensures a specific task is executed exactly once, regardless of concurrent callers.
|
|
68
|
+
* Handles race conditions, caching, and optional retries on failure.
|
|
69
|
+
*
|
|
70
|
+
* Uses microtask yield mode for its internal mutex — builds are latency-sensitive
|
|
71
|
+
* and starvation is not a concern (only one build runs at a time by design).
|
|
72
|
+
*
|
|
73
|
+
* @template T - The type of value returned by the execution.
|
|
74
|
+
*/
|
|
75
|
+
declare class Once<T = void> {
|
|
76
|
+
private mutex;
|
|
77
|
+
private promise;
|
|
78
|
+
private _value;
|
|
79
|
+
private _error;
|
|
80
|
+
private _done;
|
|
81
|
+
private retry;
|
|
82
|
+
private throws;
|
|
83
|
+
constructor({ retry, throws }?: {
|
|
84
|
+
retry?: boolean;
|
|
85
|
+
throws?: boolean;
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* Execute the function if it hasn't been executed yet.
|
|
89
|
+
* Subsequent calls return the result of the first execution.
|
|
90
|
+
*
|
|
91
|
+
* @param fn - The function to execute.
|
|
92
|
+
* @param timeout - Max wait time in ms (includes lock wait + execution time).
|
|
93
|
+
*/
|
|
94
|
+
do(fn: () => Promise<T> | T, timeout?: number): Promise<OnceResult<T>>;
|
|
95
|
+
/**
|
|
96
|
+
* 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.
|
|
99
|
+
*/
|
|
100
|
+
isReady(): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Returns true if the operation is currently executing.
|
|
103
|
+
*/
|
|
104
|
+
running(): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Returns the current state without waiting.
|
|
107
|
+
*/
|
|
108
|
+
peek(): OnceResult<T>;
|
|
109
|
+
/**
|
|
110
|
+
* Returns the stored value if successful, otherwise throws the stored error.
|
|
111
|
+
* Throws if the operation is not yet complete.
|
|
112
|
+
*/
|
|
113
|
+
get(): T | null;
|
|
114
|
+
/**
|
|
115
|
+
* Resets the instance, allowing the action to run again on the next call.
|
|
116
|
+
*/
|
|
117
|
+
reset(): void;
|
|
118
|
+
/**
|
|
119
|
+
* Returns the underlying promise if running or done, null otherwise.
|
|
120
|
+
*/
|
|
121
|
+
resolved(): Promise<OnceResult<T>> | null;
|
|
122
|
+
/**
|
|
123
|
+
* Returns true if the operation has finished (success or final failure).
|
|
124
|
+
*/
|
|
125
|
+
done(): boolean;
|
|
126
|
+
private _awaitWithTimeout;
|
|
127
|
+
}
|
|
128
|
+
type SerializerResult<T> = {
|
|
129
|
+
value: T | null;
|
|
130
|
+
error?: unknown;
|
|
131
|
+
};
|
|
132
|
+
interface SerializerOptions {
|
|
133
|
+
/**
|
|
134
|
+
* Max items in queue. If full, .do() returns an error immediately.
|
|
135
|
+
* @default 1000
|
|
136
|
+
*/
|
|
137
|
+
capacity?: number;
|
|
138
|
+
/**
|
|
139
|
+
* 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).
|
|
142
|
+
*/
|
|
143
|
+
yieldMode?: "macrotask" | "microtask";
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Ensures tasks are executed sequentially (FIFO).
|
|
147
|
+
* Maintains the result of the last successful execution.
|
|
148
|
+
* Includes backpressure protection via configurable queue capacity.
|
|
149
|
+
*/
|
|
150
|
+
declare class Serializer<T = void> {
|
|
151
|
+
private mutex;
|
|
152
|
+
private _done;
|
|
153
|
+
private _lastValue;
|
|
154
|
+
private _lastError;
|
|
155
|
+
constructor(options?: SerializerOptions);
|
|
156
|
+
/**
|
|
157
|
+
* Enqueue a function to be executed after all previous tasks complete.
|
|
158
|
+
*
|
|
159
|
+
* @param fn - The function to execute.
|
|
160
|
+
* @param timeout - Max time to wait to acquire the lock.
|
|
161
|
+
* @returns Object containing the value or error.
|
|
162
|
+
*/
|
|
163
|
+
do(fn: () => Promise<T> | T, timeout?: number): Promise<SerializerResult<T | null>>;
|
|
164
|
+
/** Returns the result of the last successful execution. */
|
|
165
|
+
peek(): SerializerResult<T | null>;
|
|
166
|
+
/**
|
|
167
|
+
* Permanently closes the serializer.
|
|
168
|
+
* Subsequent calls to `do()` will fail immediately with SerializerExecutionDone.
|
|
169
|
+
*/
|
|
170
|
+
close(): void;
|
|
171
|
+
/** Returns the number of tasks currently waiting. */
|
|
172
|
+
pending(): number;
|
|
173
|
+
/** Returns true if a task is currently executing. */
|
|
174
|
+
running(): boolean;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export { Mutex, type MutexOptions, Once, type OnceResult, Serializer, type SerializerOptions, type SerializerResult };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
interface MutexOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Maximum number of pending requests allowed in the queue.
|
|
4
|
+
* If exceeded, tryLock/lock will fail.
|
|
5
|
+
* @default Infinity
|
|
6
|
+
*/
|
|
7
|
+
capacity?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Controls how lock handoff is scheduled when a waiter is unblocked.
|
|
10
|
+
*
|
|
11
|
+
* - `"macrotask"` (default): Uses setTimeout(fn, 0) to yield to the event
|
|
12
|
+
* loop between handoffs. Prevents microtask starvation under heavy
|
|
13
|
+
* contention — I/O, rendering, and other macrotasks can run between
|
|
14
|
+
* lock acquisitions. Use for invalidationSerializer and other
|
|
15
|
+
* coarse-grained serializers.
|
|
16
|
+
*
|
|
17
|
+
* - `"microtask"`: Uses queueMicrotask(fn) for handoff. Near-zero latency
|
|
18
|
+
* between acquisitions — no macrotask delay. Safe when you need
|
|
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).
|
|
22
|
+
*/
|
|
23
|
+
yieldMode?: "macrotask" | "microtask";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A mutual exclusion lock.
|
|
27
|
+
* Allows only one execution context to access a resource at a time.
|
|
28
|
+
*
|
|
29
|
+
* Yield mode is configurable per instance:
|
|
30
|
+
* - "macrotask" (default): yields between handoffs, preventing microtask starvation.
|
|
31
|
+
* - "microtask": zero-delay handoff for latency-sensitive paths.
|
|
32
|
+
*/
|
|
33
|
+
declare class Mutex {
|
|
34
|
+
private _locked;
|
|
35
|
+
private _capacity;
|
|
36
|
+
private _yieldMode;
|
|
37
|
+
private waiters;
|
|
38
|
+
constructor(options?: MutexOptions);
|
|
39
|
+
/**
|
|
40
|
+
* Acquires the lock. If already held, waits until released or timeout reached.
|
|
41
|
+
*
|
|
42
|
+
* @param timeout - Optional maximum wait time in milliseconds.
|
|
43
|
+
* @throws {TimeoutError} If the lock cannot be acquired within the timeout.
|
|
44
|
+
* @throws {Error} If the wait queue is full (backpressure).
|
|
45
|
+
*/
|
|
46
|
+
lock(timeout?: number): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Attempts to acquire the lock without waiting.
|
|
49
|
+
* @returns `true` if the lock was acquired, `false` otherwise.
|
|
50
|
+
*/
|
|
51
|
+
tryLock(): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Releases the lock, scheduling the next waiter according to yieldMode.
|
|
54
|
+
* @throws {Error} If the mutex is not currently locked.
|
|
55
|
+
*/
|
|
56
|
+
unlock(): void;
|
|
57
|
+
/** Returns true if the mutex is currently locked. */
|
|
58
|
+
locked(): boolean;
|
|
59
|
+
/** Returns the number of operations waiting for the lock. */
|
|
60
|
+
pending(): number;
|
|
61
|
+
}
|
|
62
|
+
type OnceResult<T> = {
|
|
63
|
+
value: T | null;
|
|
64
|
+
error?: unknown;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Ensures a specific task is executed exactly once, regardless of concurrent callers.
|
|
68
|
+
* Handles race conditions, caching, and optional retries on failure.
|
|
69
|
+
*
|
|
70
|
+
* Uses microtask yield mode for its internal mutex — builds are latency-sensitive
|
|
71
|
+
* and starvation is not a concern (only one build runs at a time by design).
|
|
72
|
+
*
|
|
73
|
+
* @template T - The type of value returned by the execution.
|
|
74
|
+
*/
|
|
75
|
+
declare class Once<T = void> {
|
|
76
|
+
private mutex;
|
|
77
|
+
private promise;
|
|
78
|
+
private _value;
|
|
79
|
+
private _error;
|
|
80
|
+
private _done;
|
|
81
|
+
private retry;
|
|
82
|
+
private throws;
|
|
83
|
+
constructor({ retry, throws }?: {
|
|
84
|
+
retry?: boolean;
|
|
85
|
+
throws?: boolean;
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* Execute the function if it hasn't been executed yet.
|
|
89
|
+
* Subsequent calls return the result of the first execution.
|
|
90
|
+
*
|
|
91
|
+
* @param fn - The function to execute.
|
|
92
|
+
* @param timeout - Max wait time in ms (includes lock wait + execution time).
|
|
93
|
+
*/
|
|
94
|
+
do(fn: () => Promise<T> | T, timeout?: number): Promise<OnceResult<T>>;
|
|
95
|
+
/**
|
|
96
|
+
* 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.
|
|
99
|
+
*/
|
|
100
|
+
isReady(): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Returns true if the operation is currently executing.
|
|
103
|
+
*/
|
|
104
|
+
running(): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Returns the current state without waiting.
|
|
107
|
+
*/
|
|
108
|
+
peek(): OnceResult<T>;
|
|
109
|
+
/**
|
|
110
|
+
* Returns the stored value if successful, otherwise throws the stored error.
|
|
111
|
+
* Throws if the operation is not yet complete.
|
|
112
|
+
*/
|
|
113
|
+
get(): T | null;
|
|
114
|
+
/**
|
|
115
|
+
* Resets the instance, allowing the action to run again on the next call.
|
|
116
|
+
*/
|
|
117
|
+
reset(): void;
|
|
118
|
+
/**
|
|
119
|
+
* Returns the underlying promise if running or done, null otherwise.
|
|
120
|
+
*/
|
|
121
|
+
resolved(): Promise<OnceResult<T>> | null;
|
|
122
|
+
/**
|
|
123
|
+
* Returns true if the operation has finished (success or final failure).
|
|
124
|
+
*/
|
|
125
|
+
done(): boolean;
|
|
126
|
+
private _awaitWithTimeout;
|
|
127
|
+
}
|
|
128
|
+
type SerializerResult<T> = {
|
|
129
|
+
value: T | null;
|
|
130
|
+
error?: unknown;
|
|
131
|
+
};
|
|
132
|
+
interface SerializerOptions {
|
|
133
|
+
/**
|
|
134
|
+
* Max items in queue. If full, .do() returns an error immediately.
|
|
135
|
+
* @default 1000
|
|
136
|
+
*/
|
|
137
|
+
capacity?: number;
|
|
138
|
+
/**
|
|
139
|
+
* 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).
|
|
142
|
+
*/
|
|
143
|
+
yieldMode?: "macrotask" | "microtask";
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Ensures tasks are executed sequentially (FIFO).
|
|
147
|
+
* Maintains the result of the last successful execution.
|
|
148
|
+
* Includes backpressure protection via configurable queue capacity.
|
|
149
|
+
*/
|
|
150
|
+
declare class Serializer<T = void> {
|
|
151
|
+
private mutex;
|
|
152
|
+
private _done;
|
|
153
|
+
private _lastValue;
|
|
154
|
+
private _lastError;
|
|
155
|
+
constructor(options?: SerializerOptions);
|
|
156
|
+
/**
|
|
157
|
+
* Enqueue a function to be executed after all previous tasks complete.
|
|
158
|
+
*
|
|
159
|
+
* @param fn - The function to execute.
|
|
160
|
+
* @param timeout - Max time to wait to acquire the lock.
|
|
161
|
+
* @returns Object containing the value or error.
|
|
162
|
+
*/
|
|
163
|
+
do(fn: () => Promise<T> | T, timeout?: number): Promise<SerializerResult<T | null>>;
|
|
164
|
+
/** Returns the result of the last successful execution. */
|
|
165
|
+
peek(): SerializerResult<T | null>;
|
|
166
|
+
/**
|
|
167
|
+
* Permanently closes the serializer.
|
|
168
|
+
* Subsequent calls to `do()` will fail immediately with SerializerExecutionDone.
|
|
169
|
+
*/
|
|
170
|
+
close(): void;
|
|
171
|
+
/** Returns the number of tasks currently waiting. */
|
|
172
|
+
pending(): number;
|
|
173
|
+
/** Returns true if a task is currently executing. */
|
|
174
|
+
running(): boolean;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export { Mutex, type MutexOptions, Once, type OnceResult, Serializer, type SerializerOptions, type SerializerResult };
|
package/index.js
ADDED
|
@@ -0,0 +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()}};
|
package/index.mjs
ADDED
|
@@ -0,0 +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};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@asaidimu/utils-sync",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A collection of sync utilities.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "index.mjs",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"typescript",
|
|
10
|
+
"utility"
|
|
11
|
+
],
|
|
12
|
+
"author": "Saidimu <47994458+asaidimu@users.noreply.github.com>",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/asaidimu/erp-utils.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/asaidimu/erp-utils/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/asaidimu/erp-utils/tree/main/src/sync#readme",
|
|
22
|
+
"files": [
|
|
23
|
+
"./*"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": {
|
|
28
|
+
"types": "./index.d.ts",
|
|
29
|
+
"default": "./index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"require": {
|
|
32
|
+
"types": "./index.d.ts",
|
|
33
|
+
"default": "./index.js"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"registry": "https://registry.npmjs.org/",
|
|
40
|
+
"tag": "latest",
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"release": {
|
|
44
|
+
"plugins": [
|
|
45
|
+
[
|
|
46
|
+
"@semantic-release/npm",
|
|
47
|
+
{
|
|
48
|
+
"pkgRoot": "./dist"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
"@semantic-release/git",
|
|
53
|
+
{
|
|
54
|
+
"assets": [
|
|
55
|
+
"CHANGELOG.md",
|
|
56
|
+
"package.json"
|
|
57
|
+
],
|
|
58
|
+
"message": "chore(release): Release @asaidimu/utils-sync v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
}
|