@md-oss/mutex 0.1.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 +5 -0
- package/README.md +398 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright 2026 Mirasaki Development
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
4
|
+
|
|
5
|
+
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# @md-oss/mutex
|
|
2
|
+
|
|
3
|
+
Mutex manager with automatic cleanup and TTL support for preventing race conditions in asynchronous operations.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Exclusive Execution** - Ensure only one operation runs at a time for a given key
|
|
8
|
+
- **Automatic Cleanup** - Mutexes are automatically removed after a configurable TTL
|
|
9
|
+
- **Flexible Keys** - Support for string keys or object-based keys with automatic normalization
|
|
10
|
+
- **Wait for Unlock** - Wait for a mutex to become available without acquiring it
|
|
11
|
+
- **Memory Efficient** - Unused mutexes are garbage collected based on TTL
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @md-oss/mutex
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Basic Usage
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { MutexManager } from '@md-oss/mutex';
|
|
25
|
+
|
|
26
|
+
// Create a mutex manager with 60 second TTL
|
|
27
|
+
const mutexManager = new MutexManager(60000);
|
|
28
|
+
|
|
29
|
+
// Run exclusive operation with string key
|
|
30
|
+
await mutexManager.runExclusive('user-123', async () => {
|
|
31
|
+
// This code will only run when no other operation with key 'user-123' is running
|
|
32
|
+
await updateUserBalance(123);
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Preventing Race Conditions
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { MutexManager } from '@md-oss/mutex';
|
|
40
|
+
|
|
41
|
+
const mutexManager = new MutexManager(30000);
|
|
42
|
+
|
|
43
|
+
// Without mutex - race condition possible
|
|
44
|
+
async function incrementCounter(userId: string) {
|
|
45
|
+
const current = await getCounter(userId);
|
|
46
|
+
await saveCounter(userId, current + 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// With mutex - safe from race conditions
|
|
50
|
+
async function incrementCounterSafe(userId: string) {
|
|
51
|
+
await mutexManager.runExclusive(`counter-${userId}`, async () => {
|
|
52
|
+
const current = await getCounter(userId);
|
|
53
|
+
await saveCounter(userId, current + 1);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Multiple concurrent calls are now safe
|
|
58
|
+
await Promise.all([
|
|
59
|
+
incrementCounterSafe('user-1'),
|
|
60
|
+
incrementCounterSafe('user-1'),
|
|
61
|
+
incrementCounterSafe('user-1')
|
|
62
|
+
]);
|
|
63
|
+
// Counter will be incremented exactly 3 times, no race condition
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Object-Based Keys
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// Use object keys for complex identifiers
|
|
70
|
+
await mutexManager.runExclusive(
|
|
71
|
+
{ userId: '123', action: 'update-profile' },
|
|
72
|
+
async () => {
|
|
73
|
+
await updateUserProfile(123, profileData);
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Object keys are automatically normalized
|
|
78
|
+
// These are equivalent:
|
|
79
|
+
mutexManager.getMutex({ userId: '123', action: 'update' });
|
|
80
|
+
mutexManager.getMutex({ action: 'update', userId: '123' }); // Same mutex
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Manual Mutex Management
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// Get a mutex without running code
|
|
87
|
+
const mutex = mutexManager.getMutex('resource-key');
|
|
88
|
+
|
|
89
|
+
// Acquire and release manually
|
|
90
|
+
const release = await mutex.acquire();
|
|
91
|
+
try {
|
|
92
|
+
// Do exclusive work
|
|
93
|
+
await someOperation();
|
|
94
|
+
} finally {
|
|
95
|
+
release();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check if locked
|
|
99
|
+
if (mutex.isLocked()) {
|
|
100
|
+
console.log('Mutex is currently held');
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Wait for Unlock
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// Wait for a mutex to become available without acquiring it
|
|
108
|
+
await mutexManager.waitIfLocked('resource-key');
|
|
109
|
+
console.log('Mutex is now unlocked');
|
|
110
|
+
|
|
111
|
+
// Useful for waiting on operations without blocking
|
|
112
|
+
async function waitForUserUpdate(userId: string) {
|
|
113
|
+
await mutexManager.waitIfLocked(`user-${userId}`);
|
|
114
|
+
// User update is complete, safe to read
|
|
115
|
+
return await getUser(userId);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Use Cases
|
|
120
|
+
|
|
121
|
+
### 1. Database Updates
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const dbMutex = new MutexManager(10000);
|
|
125
|
+
|
|
126
|
+
async function updateUserBalance(userId: string, amount: number) {
|
|
127
|
+
await dbMutex.runExclusive(`balance-${userId}`, async () => {
|
|
128
|
+
const balance = await db.getBalance(userId);
|
|
129
|
+
await db.setBalance(userId, balance + amount);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Safe concurrent balance updates
|
|
134
|
+
await Promise.all([
|
|
135
|
+
updateUserBalance('123', 100),
|
|
136
|
+
updateUserBalance('123', -50),
|
|
137
|
+
updateUserBalance('123', 25)
|
|
138
|
+
]);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 2. File Operations
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const fileMutex = new MutexManager(5000);
|
|
145
|
+
|
|
146
|
+
async function appendToFile(filename: string, content: string) {
|
|
147
|
+
await fileMutex.runExclusive(filename, async () => {
|
|
148
|
+
const existing = await fs.readFile(filename, 'utf-8');
|
|
149
|
+
await fs.writeFile(filename, existing + content);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 3. Cache Refreshing
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const cacheMutex = new MutexManager(60000);
|
|
158
|
+
|
|
159
|
+
async function getCachedData(key: string) {
|
|
160
|
+
if (cache.has(key)) {
|
|
161
|
+
return cache.get(key);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Only one request refreshes the cache
|
|
165
|
+
return await cacheMutex.runExclusive(`cache-${key}`, async () => {
|
|
166
|
+
// Double-check inside mutex
|
|
167
|
+
if (cache.has(key)) {
|
|
168
|
+
return cache.get(key);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const data = await fetchExpensiveData(key);
|
|
172
|
+
cache.set(key, data);
|
|
173
|
+
return data;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 4. API Rate Limiting
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const apiMutex = new MutexManager(1000);
|
|
182
|
+
|
|
183
|
+
async function makeRateLimitedRequest(apiKey: string, request: any) {
|
|
184
|
+
await apiMutex.runExclusive(apiKey, async () => {
|
|
185
|
+
await fetch('https://api.example.com', {
|
|
186
|
+
headers: { 'X-API-Key': apiKey },
|
|
187
|
+
body: JSON.stringify(request)
|
|
188
|
+
});
|
|
189
|
+
// Wait 1 second before allowing next request
|
|
190
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 5. Resource Initialization
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
const initMutex = new MutexManager(30000);
|
|
199
|
+
const resources = new Map<string, Resource>();
|
|
200
|
+
|
|
201
|
+
async function getOrCreateResource(id: string): Promise<Resource> {
|
|
202
|
+
await initMutex.runExclusive(`resource-${id}`, async () => {
|
|
203
|
+
if (!resources.has(id)) {
|
|
204
|
+
const resource = await initializeResource(id);
|
|
205
|
+
resources.set(id, resource);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return resources.get(id)!;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## TTL and Cleanup
|
|
214
|
+
|
|
215
|
+
The mutex manager automatically cleans up unused mutexes after the configured TTL:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// Create with 30 second TTL
|
|
219
|
+
const manager = new MutexManager(30000);
|
|
220
|
+
|
|
221
|
+
// Use a mutex
|
|
222
|
+
await manager.runExclusive('key-1', async () => {
|
|
223
|
+
// Do work
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Timer resets on each use
|
|
227
|
+
await manager.runExclusive('key-1', async () => {
|
|
228
|
+
// Do more work
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// After 30 seconds of inactivity, 'key-1' mutex is cleaned up
|
|
232
|
+
// Memory is freed and a new mutex will be created on next use
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### TTL Configuration Guidelines
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// Short-lived operations (API requests, quick DB queries)
|
|
239
|
+
const shortTTL = new MutexManager(5000); // 5 seconds
|
|
240
|
+
|
|
241
|
+
// Medium-lived operations (file processing, cache updates)
|
|
242
|
+
const mediumTTL = new MutexManager(30000); // 30 seconds
|
|
243
|
+
|
|
244
|
+
// Long-lived operations (background jobs, user sessions)
|
|
245
|
+
const longTTL = new MutexManager(300000); // 5 minutes
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Key Normalization
|
|
249
|
+
|
|
250
|
+
Object keys are automatically normalized for consistency:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
const manager = new MutexManager(10000);
|
|
254
|
+
|
|
255
|
+
// All of these use the same mutex
|
|
256
|
+
manager.getMutex({ userId: '123', type: 'update' });
|
|
257
|
+
manager.getMutex({ type: 'update', userId: '123' }); // Same
|
|
258
|
+
manager.getMutex({ userId: '123', type: 'update' }); // Same
|
|
259
|
+
|
|
260
|
+
// Normalized to: "mutex-type-update-userId-123"
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Error Handling
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
const manager = new MutexManager(10000);
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
await manager.runExclusive('resource', async () => {
|
|
270
|
+
throw new Error('Operation failed');
|
|
271
|
+
});
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// Error is propagated, mutex is released
|
|
274
|
+
console.error('Operation failed:', error);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Mutex is automatically released even on error
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Performance Considerations
|
|
281
|
+
|
|
282
|
+
- **Mutex Creation**: O(1) - mutexes are created on-demand
|
|
283
|
+
- **Key Normalization**: O(n log n) for object keys, O(1) for strings
|
|
284
|
+
- **Memory**: Automatic cleanup prevents memory leaks
|
|
285
|
+
- **Concurrency**: Each key has its own mutex, allowing parallel operations on different keys
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// These can run in parallel (different keys)
|
|
289
|
+
await Promise.all([
|
|
290
|
+
manager.runExclusive('user-1', async () => { /* ... */ }),
|
|
291
|
+
manager.runExclusive('user-2', async () => { /* ... */ }),
|
|
292
|
+
manager.runExclusive('user-3', async () => { /* ... */ })
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
// These run sequentially (same key)
|
|
296
|
+
await Promise.all([
|
|
297
|
+
manager.runExclusive('user-1', async () => { /* ... */ }),
|
|
298
|
+
manager.runExclusive('user-1', async () => { /* ... */ }),
|
|
299
|
+
manager.runExclusive('user-1', async () => { /* ... */ })
|
|
300
|
+
]);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## API Reference
|
|
304
|
+
|
|
305
|
+
### Constructor
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
new MutexManager(ttl: number)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Create a new mutex manager with the specified TTL in milliseconds.
|
|
312
|
+
|
|
313
|
+
### Methods
|
|
314
|
+
|
|
315
|
+
#### `getMutex(key)`
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
getMutex(key: string | Record<string, string>): Mutex
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Get or create a mutex for the given key. Resets the cleanup timer.
|
|
322
|
+
|
|
323
|
+
#### `runExclusive(key, fn)`
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
async runExclusive<T>(
|
|
327
|
+
key: string | Record<string, string>,
|
|
328
|
+
fn: () => Promise<T>
|
|
329
|
+
): Promise<T>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Acquire the mutex for the given key, execute the function exclusively, then release the mutex.
|
|
333
|
+
|
|
334
|
+
#### `waitIfLocked(key)`
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
async waitIfLocked(key: string | Record<string, string>): Promise<void>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Wait for the mutex to become available without acquiring it. Returns immediately if the mutex is not locked.
|
|
341
|
+
|
|
342
|
+
## Best Practices
|
|
343
|
+
|
|
344
|
+
### 1. Choose Appropriate TTL
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// Match TTL to operation frequency
|
|
348
|
+
const frequentOps = new MutexManager(5000); // Operations every few seconds
|
|
349
|
+
const infrequentOps = new MutexManager(60000); // Operations every few minutes
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### 2. Use Descriptive Keys
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// Good - clear and specific
|
|
356
|
+
manager.runExclusive(`user-${userId}-balance`, async () => { /* ... */ });
|
|
357
|
+
|
|
358
|
+
// Bad - ambiguous
|
|
359
|
+
manager.runExclusive(userId, async () => { /* ... */ });
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### 3. Keep Critical Sections Small
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// Good - minimal time in mutex
|
|
366
|
+
await manager.runExclusive('resource', async () => {
|
|
367
|
+
await criticalOperation();
|
|
368
|
+
});
|
|
369
|
+
await nonCriticalOperation();
|
|
370
|
+
|
|
371
|
+
// Bad - unnecessary time in mutex
|
|
372
|
+
await manager.runExclusive('resource', async () => {
|
|
373
|
+
await criticalOperation();
|
|
374
|
+
await nonCriticalOperation(); // Doesn't need mutex
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### 4. Handle Errors Properly
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
await manager.runExclusive('key', async () => {
|
|
382
|
+
try {
|
|
383
|
+
await riskyOperation();
|
|
384
|
+
} catch (error) {
|
|
385
|
+
// Handle error inside mutex if needed
|
|
386
|
+
await rollback();
|
|
387
|
+
throw error; // Re-throw if appropriate
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Underlying Library
|
|
393
|
+
|
|
394
|
+
This package is built on top of [async-mutex](https://github.com/DirtyHairy/async-mutex) and adds:
|
|
395
|
+
- Automatic cleanup with TTL
|
|
396
|
+
- Key normalization for objects
|
|
397
|
+
- Centralized mutex management
|
|
398
|
+
- Simplified API for common patterns
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var u=Object.defineProperty;var r=(s,e)=>u(s,"name",{value:e,configurable:!0});var a=require("async-mutex");class n{static{r(this,"MutexManager")}mutexMap=new Map;cleanupTimers=new Map;ttl;constructor(e){this.ttl=e}cleanup(e){this.mutexMap.delete(e);const t=this.cleanupTimers.get(e);t&&(clearTimeout(t),this.cleanupTimers.delete(e))}resetCleanupTimer(e){const t=this.cleanupTimers.get(e);t&&clearTimeout(t);const i=setTimeout(()=>{this.cleanup(e)},this.ttl);this.cleanupTimers.set(e,i)}normalizeKey(e){return typeof e=="string"?e:`mutex-${Object.entries(e).sort(([t],[i])=>t.localeCompare(i)).map(([t,i])=>`${t}-${i}`).join("-")}`}getMutex(e){const t=this.normalizeKey(e);return this.mutexMap.has(t)||this.mutexMap.set(t,new a.Mutex),this.resetCleanupTimer(t),this.mutexMap.get(t)}async runExclusive(e,t){return this.getMutex(e).runExclusive(t)}async waitIfLocked(e){const t=this.getMutex(e);t.isLocked()&&await t.waitForUnlock()}}exports.MutexManager=n;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/index.ts"],"sourcesContent":["import { Mutex } from 'async-mutex';\n\nclass MutexManager {\n\tprivate readonly mutexMap = new Map<string, Mutex>();\n\tprivate readonly cleanupTimers = new Map<string, NodeJS.Timeout>();\n\tprivate readonly ttl: number;\n\n\tconstructor(ttl: number) {\n\t\tthis.ttl = ttl;\n\t}\n\n\tprivate cleanup(key: string) {\n\t\tthis.mutexMap.delete(key);\n\t\tconst timer = this.cleanupTimers.get(key);\n\t\tif (timer) {\n\t\t\tclearTimeout(timer);\n\t\t\tthis.cleanupTimers.delete(key);\n\t\t}\n\t}\n\n\tprivate resetCleanupTimer(key: string) {\n\t\tconst existingTimer = this.cleanupTimers.get(key);\n\t\tif (existingTimer) {\n\t\t\tclearTimeout(existingTimer);\n\t\t}\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tthis.cleanup(key);\n\t\t}, this.ttl);\n\n\t\tthis.cleanupTimers.set(key, timer);\n\t}\n\n\tprivate normalizeKey(key: string | Record<string, string>): string {\n\t\tif (typeof key === 'string') return key;\n\n\t\treturn `mutex-${Object.entries(key)\n\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t.map(([k, v]) => `${k}-${v}`)\n\t\t\t.join('-')}`;\n\t}\n\n\tpublic getMutex(key: string | Record<string, string>): Mutex {\n\t\tconst normalizedKey = this.normalizeKey(key);\n\n\t\tif (!this.mutexMap.has(normalizedKey)) {\n\t\t\tthis.mutexMap.set(normalizedKey, new Mutex());\n\t\t}\n\n\t\tthis.resetCleanupTimer(normalizedKey);\n\n\t\treturn this.mutexMap.get(normalizedKey) as Mutex;\n\t}\n\n\tpublic async runExclusive<T>(\n\t\tkey: string | Record<string, string>,\n\t\tfn: () => Promise<T>\n\t): Promise<T> {\n\t\tconst mutex = this.getMutex(key);\n\t\treturn mutex.runExclusive(fn);\n\t}\n\n\tpublic async waitIfLocked(\n\t\tkey: string | Record<string, string>\n\t): Promise<void> {\n\t\tconst mutex = this.getMutex(key);\n\t\tif (mutex.isLocked()) {\n\t\t\tawait mutex.waitForUnlock();\n\t\t}\n\t}\n}\n\nexport { MutexManager };\n"],"names":["MutexManager","__name","ttl","key","timer","existingTimer","a","b","k","v","normalizedKey","Mutex","fn","mutex"],"mappings":"yHAEA,MAAMA,CAAa,OAAA,CAAAC,EAAA,qBACD,aAAe,IACf,kBAAoB,IACpB,IAEjB,YAAYC,EAAa,CACxB,KAAK,IAAMA,CACZ,CAEQ,QAAQC,EAAa,CAC5B,KAAK,SAAS,OAAOA,CAAG,EACxB,MAAMC,EAAQ,KAAK,cAAc,IAAID,CAAG,EACpCC,IACH,aAAaA,CAAK,EAClB,KAAK,cAAc,OAAOD,CAAG,EAE/B,CAEQ,kBAAkBA,EAAa,CACtC,MAAME,EAAgB,KAAK,cAAc,IAAIF,CAAG,EAC5CE,GACH,aAAaA,CAAa,EAG3B,MAAMD,EAAQ,WAAW,IAAM,CAC9B,KAAK,QAAQD,CAAG,CACjB,EAAG,KAAK,GAAG,EAEX,KAAK,cAAc,IAAIA,EAAKC,CAAK,CAClC,CAEQ,aAAaD,EAA8C,CAClE,OAAI,OAAOA,GAAQ,SAAiBA,EAE7B,SAAS,OAAO,QAAQA,CAAG,EAChC,KAAK,CAAC,CAACG,CAAC,EAAG,CAACC,CAAC,IAAMD,EAAE,cAAcC,CAAC,CAAC,EACrC,IAAI,CAAC,CAACC,EAAGC,CAAC,IAAM,GAAGD,CAAC,IAAIC,CAAC,EAAE,EAC3B,KAAK,GAAG,CAAC,EACZ,CAEO,SAASN,EAA6C,CAC5D,MAAMO,EAAgB,KAAK,aAAaP,CAAG,EAE3C,OAAK,KAAK,SAAS,IAAIO,CAAa,GACnC,KAAK,SAAS,IAAIA,EAAe,IAAIC,EAAAA,KAAO,EAG7C,KAAK,kBAAkBD,CAAa,EAE7B,KAAK,SAAS,IAAIA,CAAa,CACvC,CAEA,MAAa,aACZP,EACAS,EACa,CAEb,OADc,KAAK,SAAST,CAAG,EAClB,aAAaS,CAAE,CAC7B,CAEA,MAAa,aACZT,EACgB,CAChB,MAAMU,EAAQ,KAAK,SAASV,CAAG,EAC3BU,EAAM,YACT,MAAMA,EAAM,cAAA,CAEd,CACD"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Mutex } from 'async-mutex';
|
|
2
|
+
|
|
3
|
+
declare class MutexManager {
|
|
4
|
+
private readonly mutexMap;
|
|
5
|
+
private readonly cleanupTimers;
|
|
6
|
+
private readonly ttl;
|
|
7
|
+
constructor(ttl: number);
|
|
8
|
+
private cleanup;
|
|
9
|
+
private resetCleanupTimer;
|
|
10
|
+
private normalizeKey;
|
|
11
|
+
getMutex(key: string | Record<string, string>): Mutex;
|
|
12
|
+
runExclusive<T>(key: string | Record<string, string>, fn: () => Promise<T>): Promise<T>;
|
|
13
|
+
waitIfLocked(key: string | Record<string, string>): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { MutexManager };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Mutex } from 'async-mutex';
|
|
2
|
+
|
|
3
|
+
declare class MutexManager {
|
|
4
|
+
private readonly mutexMap;
|
|
5
|
+
private readonly cleanupTimers;
|
|
6
|
+
private readonly ttl;
|
|
7
|
+
constructor(ttl: number);
|
|
8
|
+
private cleanup;
|
|
9
|
+
private resetCleanupTimer;
|
|
10
|
+
private normalizeKey;
|
|
11
|
+
getMutex(key: string | Record<string, string>): Mutex;
|
|
12
|
+
runExclusive<T>(key: string | Record<string, string>, fn: () => Promise<T>): Promise<T>;
|
|
13
|
+
waitIfLocked(key: string | Record<string, string>): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { MutexManager };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var n=Object.defineProperty;var r=(s,e)=>n(s,"name",{value:e,configurable:!0});import{Mutex as u}from"async-mutex";class a{static{r(this,"MutexManager")}mutexMap=new Map;cleanupTimers=new Map;ttl;constructor(e){this.ttl=e}cleanup(e){this.mutexMap.delete(e);const t=this.cleanupTimers.get(e);t&&(clearTimeout(t),this.cleanupTimers.delete(e))}resetCleanupTimer(e){const t=this.cleanupTimers.get(e);t&&clearTimeout(t);const i=setTimeout(()=>{this.cleanup(e)},this.ttl);this.cleanupTimers.set(e,i)}normalizeKey(e){return typeof e=="string"?e:`mutex-${Object.entries(e).sort(([t],[i])=>t.localeCompare(i)).map(([t,i])=>`${t}-${i}`).join("-")}`}getMutex(e){const t=this.normalizeKey(e);return this.mutexMap.has(t)||this.mutexMap.set(t,new u),this.resetCleanupTimer(t),this.mutexMap.get(t)}async runExclusive(e,t){return this.getMutex(e).runExclusive(t)}async waitIfLocked(e){const t=this.getMutex(e);t.isLocked()&&await t.waitForUnlock()}}export{a as MutexManager};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../src/index.ts"],"sourcesContent":["import { Mutex } from 'async-mutex';\n\nclass MutexManager {\n\tprivate readonly mutexMap = new Map<string, Mutex>();\n\tprivate readonly cleanupTimers = new Map<string, NodeJS.Timeout>();\n\tprivate readonly ttl: number;\n\n\tconstructor(ttl: number) {\n\t\tthis.ttl = ttl;\n\t}\n\n\tprivate cleanup(key: string) {\n\t\tthis.mutexMap.delete(key);\n\t\tconst timer = this.cleanupTimers.get(key);\n\t\tif (timer) {\n\t\t\tclearTimeout(timer);\n\t\t\tthis.cleanupTimers.delete(key);\n\t\t}\n\t}\n\n\tprivate resetCleanupTimer(key: string) {\n\t\tconst existingTimer = this.cleanupTimers.get(key);\n\t\tif (existingTimer) {\n\t\t\tclearTimeout(existingTimer);\n\t\t}\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tthis.cleanup(key);\n\t\t}, this.ttl);\n\n\t\tthis.cleanupTimers.set(key, timer);\n\t}\n\n\tprivate normalizeKey(key: string | Record<string, string>): string {\n\t\tif (typeof key === 'string') return key;\n\n\t\treturn `mutex-${Object.entries(key)\n\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t.map(([k, v]) => `${k}-${v}`)\n\t\t\t.join('-')}`;\n\t}\n\n\tpublic getMutex(key: string | Record<string, string>): Mutex {\n\t\tconst normalizedKey = this.normalizeKey(key);\n\n\t\tif (!this.mutexMap.has(normalizedKey)) {\n\t\t\tthis.mutexMap.set(normalizedKey, new Mutex());\n\t\t}\n\n\t\tthis.resetCleanupTimer(normalizedKey);\n\n\t\treturn this.mutexMap.get(normalizedKey) as Mutex;\n\t}\n\n\tpublic async runExclusive<T>(\n\t\tkey: string | Record<string, string>,\n\t\tfn: () => Promise<T>\n\t): Promise<T> {\n\t\tconst mutex = this.getMutex(key);\n\t\treturn mutex.runExclusive(fn);\n\t}\n\n\tpublic async waitIfLocked(\n\t\tkey: string | Record<string, string>\n\t): Promise<void> {\n\t\tconst mutex = this.getMutex(key);\n\t\tif (mutex.isLocked()) {\n\t\t\tawait mutex.waitForUnlock();\n\t\t}\n\t}\n}\n\nexport { MutexManager };\n"],"names":["MutexManager","__name","ttl","key","timer","existingTimer","a","b","k","v","normalizedKey","Mutex","fn","mutex"],"mappings":"mHAEA,MAAMA,CAAa,OAAA,CAAAC,EAAA,qBACD,aAAe,IACf,kBAAoB,IACpB,IAEjB,YAAYC,EAAa,CACxB,KAAK,IAAMA,CACZ,CAEQ,QAAQC,EAAa,CAC5B,KAAK,SAAS,OAAOA,CAAG,EACxB,MAAMC,EAAQ,KAAK,cAAc,IAAID,CAAG,EACpCC,IACH,aAAaA,CAAK,EAClB,KAAK,cAAc,OAAOD,CAAG,EAE/B,CAEQ,kBAAkBA,EAAa,CACtC,MAAME,EAAgB,KAAK,cAAc,IAAIF,CAAG,EAC5CE,GACH,aAAaA,CAAa,EAG3B,MAAMD,EAAQ,WAAW,IAAM,CAC9B,KAAK,QAAQD,CAAG,CACjB,EAAG,KAAK,GAAG,EAEX,KAAK,cAAc,IAAIA,EAAKC,CAAK,CAClC,CAEQ,aAAaD,EAA8C,CAClE,OAAI,OAAOA,GAAQ,SAAiBA,EAE7B,SAAS,OAAO,QAAQA,CAAG,EAChC,KAAK,CAAC,CAACG,CAAC,EAAG,CAACC,CAAC,IAAMD,EAAE,cAAcC,CAAC,CAAC,EACrC,IAAI,CAAC,CAACC,EAAGC,CAAC,IAAM,GAAGD,CAAC,IAAIC,CAAC,EAAE,EAC3B,KAAK,GAAG,CAAC,EACZ,CAEO,SAASN,EAA6C,CAC5D,MAAMO,EAAgB,KAAK,aAAaP,CAAG,EAE3C,OAAK,KAAK,SAAS,IAAIO,CAAa,GACnC,KAAK,SAAS,IAAIA,EAAe,IAAIC,CAAO,EAG7C,KAAK,kBAAkBD,CAAa,EAE7B,KAAK,SAAS,IAAIA,CAAa,CACvC,CAEA,MAAa,aACZP,EACAS,EACa,CAEb,OADc,KAAK,SAAST,CAAG,EAClB,aAAaS,CAAE,CAC7B,CAEA,MAAa,aACZT,EACgB,CAChB,MAAMU,EAAQ,KAAK,SAASV,CAAG,EAC3BU,EAAM,YACT,MAAMA,EAAM,cAAA,CAEd,CACD"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@md-oss/mutex",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public",
|
|
7
|
+
"registry": "https://registry.npmjs.org/"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+ssh://git@github.com/Mirasaki-OSS/monorepo-template.git",
|
|
12
|
+
"directory": "vendor/mutex"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"description": "Mutex manager with automatic cleanup and TTL support",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.mjs",
|
|
19
|
+
"types": "./dist/index.d.cts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/**/*",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
"require": {
|
|
27
|
+
"types": "./dist/index.d.cts",
|
|
28
|
+
"default": "./dist/index.cjs"
|
|
29
|
+
},
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"default": "./dist/index.mjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"async-mutex": "^0.5.0",
|
|
37
|
+
"@md-oss/config": "^0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.0.9",
|
|
41
|
+
"pkgroll": "^2.21.5",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "pkgroll --minify --clean-dist --sourcemap --define.process.env.NODE_ENV='\"production\"' --define.DEBUG=false",
|
|
46
|
+
"clean": "git clean -xdf .turbo dist node_modules tsconfig.tsbuildinfo",
|
|
47
|
+
"dev": "tsc --watch",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
49
|
+
}
|
|
50
|
+
}
|