@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 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"}
@@ -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 };
@@ -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
+ }