@nestjs-redisx/locks 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 +21 -0
- package/README.md +50 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +635 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +625 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lock/api/decorators/with-lock.decorator.d.ts +105 -0
- package/dist/lock/api/decorators/with-lock.decorator.d.ts.map +1 -0
- package/dist/lock/application/ports/lock-service.port.d.ts +120 -0
- package/dist/lock/application/ports/lock-service.port.d.ts.map +1 -0
- package/dist/lock/application/ports/lock-store.port.d.ts +63 -0
- package/dist/lock/application/ports/lock-store.port.d.ts.map +1 -0
- package/dist/lock/application/services/lock-decorator-initializer.service.d.ts +19 -0
- package/dist/lock/application/services/lock-decorator-initializer.service.d.ts.map +1 -0
- package/dist/lock/application/services/lock.service.d.ts +90 -0
- package/dist/lock/application/services/lock.service.d.ts.map +1 -0
- package/dist/lock/domain/entities/lock.entity.d.ts +130 -0
- package/dist/lock/domain/entities/lock.entity.d.ts.map +1 -0
- package/dist/lock/infrastructure/adapters/redis-lock-store.adapter.d.ts +45 -0
- package/dist/lock/infrastructure/adapters/redis-lock-store.adapter.d.ts.map +1 -0
- package/dist/lock/infrastructure/scripts/lua-scripts.d.ts +24 -0
- package/dist/lock/infrastructure/scripts/lua-scripts.d.ts.map +1 -0
- package/dist/locks.plugin.d.ts +41 -0
- package/dist/locks.plugin.d.ts.map +1 -0
- package/dist/shared/constants/index.d.ts +13 -0
- package/dist/shared/constants/index.d.ts.map +1 -0
- package/dist/shared/errors/index.d.ts +64 -0
- package/dist/shared/errors/index.d.ts.map +1 -0
- package/dist/shared/types/index.d.ts +97 -0
- package/dist/shared/types/index.d.ts.map +1 -0
- package/package.json +78 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core$1 = require('@nestjs/core');
|
|
4
|
+
var common = require('@nestjs/common');
|
|
5
|
+
require('reflect-metadata');
|
|
6
|
+
var core = require('@nestjs-redisx/core');
|
|
7
|
+
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
10
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
11
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
12
|
+
if (decorator = decorators[i])
|
|
13
|
+
result = (decorator(result)) || result;
|
|
14
|
+
return result;
|
|
15
|
+
};
|
|
16
|
+
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
17
|
+
|
|
18
|
+
// src/shared/constants/index.ts
|
|
19
|
+
var LOCKS_PLUGIN_OPTIONS = /* @__PURE__ */ Symbol.for("LOCKS_PLUGIN_OPTIONS");
|
|
20
|
+
var LOCK_SERVICE = /* @__PURE__ */ Symbol.for("LOCK_SERVICE");
|
|
21
|
+
var LOCK_STORE = /* @__PURE__ */ Symbol.for("LOCK_STORE");
|
|
22
|
+
var LockError = class extends core.RedisXError {
|
|
23
|
+
constructor(message, code, lockKey, cause) {
|
|
24
|
+
super(message, code, cause, { lockKey });
|
|
25
|
+
this.lockKey = lockKey;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var LockAcquisitionError = class extends LockError {
|
|
29
|
+
constructor(key, reason, cause) {
|
|
30
|
+
super(`Failed to acquire lock "${key}": ${reason}`, reason === "timeout" ? core.ErrorCode.LOCK_ACQUISITION_TIMEOUT : core.ErrorCode.LOCK_ACQUISITION_FAILED, key, cause);
|
|
31
|
+
this.reason = reason;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var LockNotOwnedError = class extends LockError {
|
|
35
|
+
constructor(key, token) {
|
|
36
|
+
super(`Lock "${key}" not owned by token "${token}"`, core.ErrorCode.LOCK_NOT_OWNED, key);
|
|
37
|
+
this.token = token;
|
|
38
|
+
}
|
|
39
|
+
toJSON() {
|
|
40
|
+
return {
|
|
41
|
+
...super.toJSON(),
|
|
42
|
+
token: this.token
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var LockExtensionError = class extends LockError {
|
|
47
|
+
constructor(key, token, cause) {
|
|
48
|
+
super(`Failed to extend lock "${key}"`, core.ErrorCode.LOCK_EXTENSION_FAILED, key, cause);
|
|
49
|
+
this.token = token;
|
|
50
|
+
}
|
|
51
|
+
toJSON() {
|
|
52
|
+
return {
|
|
53
|
+
...super.toJSON(),
|
|
54
|
+
token: this.token
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var LockExpiredError = class extends LockError {
|
|
59
|
+
constructor(key) {
|
|
60
|
+
super(`Lock "${key}" expired`, core.ErrorCode.LOCK_EXPIRED, key);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/lock/api/decorators/with-lock.decorator.ts
|
|
65
|
+
var logger = new common.Logger("WithLock");
|
|
66
|
+
var WITH_LOCK_OPTIONS = /* @__PURE__ */ Symbol.for("WITH_LOCK_OPTIONS");
|
|
67
|
+
var globalLockServiceGetter = null;
|
|
68
|
+
function registerLockServiceGetter(getter) {
|
|
69
|
+
globalLockServiceGetter = getter;
|
|
70
|
+
}
|
|
71
|
+
function WithLock(options) {
|
|
72
|
+
return (target, propertyKey, descriptor) => {
|
|
73
|
+
const originalMethod = descriptor.value;
|
|
74
|
+
descriptor.value = async function(...args) {
|
|
75
|
+
if (!globalLockServiceGetter) {
|
|
76
|
+
logger.warn(`@WithLock: LockService not yet available, executing method without lock`);
|
|
77
|
+
return originalMethod.apply(this, args);
|
|
78
|
+
}
|
|
79
|
+
const lockService = globalLockServiceGetter();
|
|
80
|
+
if (!lockService) {
|
|
81
|
+
logger.warn(`@WithLock: LockService getter returned null, executing method without lock`);
|
|
82
|
+
return originalMethod.apply(this, args);
|
|
83
|
+
}
|
|
84
|
+
const key = buildLockKey(args, options);
|
|
85
|
+
let lock = null;
|
|
86
|
+
try {
|
|
87
|
+
lock = await lockService.acquire(key, {
|
|
88
|
+
ttl: options.ttl,
|
|
89
|
+
waitTimeout: options.waitTimeout,
|
|
90
|
+
autoRenew: options.autoRenew
|
|
91
|
+
});
|
|
92
|
+
const result = await originalMethod.apply(this, args);
|
|
93
|
+
return result;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof LockAcquisitionError) {
|
|
96
|
+
return handleLockFailed(key, options, error);
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
} finally {
|
|
100
|
+
if (lock) {
|
|
101
|
+
await lock.release().catch((err) => {
|
|
102
|
+
logger.error(`@WithLock: Failed to release lock ${key}:`, err);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
Object.defineProperty(descriptor.value, "name", {
|
|
108
|
+
value: originalMethod.name,
|
|
109
|
+
writable: false
|
|
110
|
+
});
|
|
111
|
+
Reflect.defineMetadata(WITH_LOCK_OPTIONS, options, descriptor.value);
|
|
112
|
+
return descriptor;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function buildLockKey(args, options) {
|
|
116
|
+
if (typeof options.key === "function") {
|
|
117
|
+
return options.key(...args);
|
|
118
|
+
}
|
|
119
|
+
return interpolateKey(options.key, args);
|
|
120
|
+
}
|
|
121
|
+
function interpolateKey(template, args) {
|
|
122
|
+
return template.replace(/\{(\d+)(?:\.(\w+))?\}/g, (_, index, prop) => {
|
|
123
|
+
const arg = args[Number(index)];
|
|
124
|
+
if (prop && typeof arg === "object" && arg !== null) {
|
|
125
|
+
return String(arg[prop]);
|
|
126
|
+
}
|
|
127
|
+
return String(arg);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function handleLockFailed(key, options, error) {
|
|
131
|
+
const handler = options.onLockFailed ?? "throw";
|
|
132
|
+
if (handler === "throw") {
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
if (handler === "skip") {
|
|
136
|
+
return void 0;
|
|
137
|
+
}
|
|
138
|
+
if (typeof handler === "function") {
|
|
139
|
+
throw handler(key);
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/lock/application/services/lock-decorator-initializer.service.ts
|
|
145
|
+
var LockDecoratorInitializerService = class {
|
|
146
|
+
constructor(lockService) {
|
|
147
|
+
this.lockService = lockService;
|
|
148
|
+
}
|
|
149
|
+
logger = new common.Logger(LockDecoratorInitializerService.name);
|
|
150
|
+
/**
|
|
151
|
+
* Called after all modules are initialized.
|
|
152
|
+
* Registers lock service getter for @WithLock decorator.
|
|
153
|
+
*/
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
155
|
+
async onModuleInit() {
|
|
156
|
+
this.logger.debug("Registering LockService getter for @WithLock decorator");
|
|
157
|
+
registerLockServiceGetter(() => this.lockService);
|
|
158
|
+
this.logger.log("@WithLock decorator initialized and ready to use");
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
LockDecoratorInitializerService = __decorateClass([
|
|
162
|
+
common.Injectable(),
|
|
163
|
+
__decorateParam(0, common.Inject(LOCK_SERVICE))
|
|
164
|
+
], LockDecoratorInitializerService);
|
|
165
|
+
|
|
166
|
+
// src/lock/domain/entities/lock.entity.ts
|
|
167
|
+
var Lock = class {
|
|
168
|
+
/**
|
|
169
|
+
* Creates a new Lock instance.
|
|
170
|
+
*
|
|
171
|
+
* @param key - Lock key in Redis
|
|
172
|
+
* @param token - Unique ownership token
|
|
173
|
+
* @param ttl - Time-to-live in milliseconds
|
|
174
|
+
* @param store - Lock store for persistence operations
|
|
175
|
+
*/
|
|
176
|
+
constructor(key, token, ttl, store) {
|
|
177
|
+
this.store = store;
|
|
178
|
+
this.key = key;
|
|
179
|
+
this.token = token;
|
|
180
|
+
this.ttl = ttl;
|
|
181
|
+
this.acquiredAt = /* @__PURE__ */ new Date();
|
|
182
|
+
this._expiresAt = new Date(Date.now() + ttl);
|
|
183
|
+
}
|
|
184
|
+
key;
|
|
185
|
+
token;
|
|
186
|
+
ttl;
|
|
187
|
+
acquiredAt;
|
|
188
|
+
_expiresAt;
|
|
189
|
+
autoRenewTimer = null;
|
|
190
|
+
released = false;
|
|
191
|
+
/**
|
|
192
|
+
* Gets the expiration timestamp.
|
|
193
|
+
*/
|
|
194
|
+
get expiresAt() {
|
|
195
|
+
return this._expiresAt;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Checks if auto-renewal is active.
|
|
199
|
+
*/
|
|
200
|
+
get isAutoRenewing() {
|
|
201
|
+
return this.autoRenewTimer !== null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Releases the lock.
|
|
205
|
+
*
|
|
206
|
+
* Stops auto-renewal and removes the lock from Redis.
|
|
207
|
+
* Idempotent - can be called multiple times safely.
|
|
208
|
+
*
|
|
209
|
+
* @throws {LockNotOwnedError} If lock is not owned by this token
|
|
210
|
+
*/
|
|
211
|
+
async release() {
|
|
212
|
+
if (this.released) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
this.stopAutoRenew();
|
|
216
|
+
const success = await this.store.release(this.key, this.token);
|
|
217
|
+
if (!success) {
|
|
218
|
+
throw new LockNotOwnedError(this.key, this.token);
|
|
219
|
+
}
|
|
220
|
+
this.released = true;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Extends the lock TTL.
|
|
224
|
+
*
|
|
225
|
+
* @param ttl - New TTL in milliseconds
|
|
226
|
+
* @throws {LockNotOwnedError} If lock was already released
|
|
227
|
+
* @throws {LockExtensionError} If extension fails (lock expired or not owned)
|
|
228
|
+
*/
|
|
229
|
+
async extend(ttl) {
|
|
230
|
+
if (this.released) {
|
|
231
|
+
throw new LockNotOwnedError(this.key, this.token);
|
|
232
|
+
}
|
|
233
|
+
const success = await this.store.extend(this.key, this.token, ttl);
|
|
234
|
+
if (!success) {
|
|
235
|
+
throw new LockExtensionError(this.key, this.token);
|
|
236
|
+
}
|
|
237
|
+
this._expiresAt = new Date(Date.now() + ttl);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Checks if lock is still held.
|
|
241
|
+
*
|
|
242
|
+
* @returns False if released, otherwise checks Redis
|
|
243
|
+
*/
|
|
244
|
+
async isHeld() {
|
|
245
|
+
if (this.released) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
return this.store.isHeldBy(this.key, this.token);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Starts automatic lock renewal.
|
|
252
|
+
*
|
|
253
|
+
* The lock will be extended at the specified interval
|
|
254
|
+
* until stopAutoRenew() is called or extension fails.
|
|
255
|
+
*
|
|
256
|
+
* @param intervalMs - Renewal interval in milliseconds
|
|
257
|
+
*/
|
|
258
|
+
startAutoRenew(intervalMs) {
|
|
259
|
+
if (this.autoRenewTimer) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
this.autoRenewTimer = setInterval(async () => {
|
|
263
|
+
try {
|
|
264
|
+
await this.extend(this.ttl);
|
|
265
|
+
} catch {
|
|
266
|
+
this.stopAutoRenew();
|
|
267
|
+
}
|
|
268
|
+
}, intervalMs);
|
|
269
|
+
if (this.autoRenewTimer.unref) {
|
|
270
|
+
this.autoRenewTimer.unref();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Stops automatic lock renewal.
|
|
275
|
+
*
|
|
276
|
+
* Safe to call multiple times.
|
|
277
|
+
*/
|
|
278
|
+
stopAutoRenew() {
|
|
279
|
+
if (this.autoRenewTimer) {
|
|
280
|
+
clearInterval(this.autoRenewTimer);
|
|
281
|
+
this.autoRenewTimer = null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// src/lock/application/services/lock.service.ts
|
|
287
|
+
var METRICS_SERVICE = /* @__PURE__ */ Symbol.for("METRICS_SERVICE");
|
|
288
|
+
var TRACING_SERVICE = /* @__PURE__ */ Symbol.for("TRACING_SERVICE");
|
|
289
|
+
exports.LockService = class LockService {
|
|
290
|
+
constructor(config, store, metrics, tracing) {
|
|
291
|
+
this.config = config;
|
|
292
|
+
this.store = store;
|
|
293
|
+
this.metrics = metrics;
|
|
294
|
+
this.tracing = tracing;
|
|
295
|
+
}
|
|
296
|
+
logger = new common.Logger(exports.LockService.name);
|
|
297
|
+
activeLocks = /* @__PURE__ */ new Set();
|
|
298
|
+
/**
|
|
299
|
+
* Lifecycle hook: releases all active locks on shutdown.
|
|
300
|
+
*/
|
|
301
|
+
async onModuleDestroy() {
|
|
302
|
+
const releasePromises = Array.from(this.activeLocks).map(
|
|
303
|
+
(lock) => lock.release().catch((error) => {
|
|
304
|
+
this.logger.error(`Failed to release lock ${lock.key} during shutdown:`, error);
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
await Promise.all(releasePromises);
|
|
308
|
+
this.activeLocks.clear();
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Acquires lock with exponential backoff retry.
|
|
312
|
+
*/
|
|
313
|
+
async acquire(key, options = {}) {
|
|
314
|
+
const span = this.tracing?.startSpan("lock.acquire", {
|
|
315
|
+
kind: "INTERNAL",
|
|
316
|
+
attributes: { "lock.key": key, "lock.ttl": options.ttl }
|
|
317
|
+
});
|
|
318
|
+
const fullKey = this.buildKey(key);
|
|
319
|
+
const ttl = this.resolveTtl(options.ttl);
|
|
320
|
+
const token = this.generateToken();
|
|
321
|
+
const retry = this.resolveRetryConfig(options);
|
|
322
|
+
const startTime = Date.now();
|
|
323
|
+
let delay = retry.initialDelay;
|
|
324
|
+
try {
|
|
325
|
+
for (let attempt = 0; attempt <= retry.maxRetries; attempt++) {
|
|
326
|
+
const acquired = await this.store.acquire(fullKey, token, ttl);
|
|
327
|
+
if (acquired) {
|
|
328
|
+
const waitDuration = (Date.now() - startTime) / 1e3;
|
|
329
|
+
this.metrics?.observeHistogram("redisx_lock_wait_duration_seconds", waitDuration);
|
|
330
|
+
this.metrics?.incrementCounter("redisx_lock_acquisitions_total", { status: "acquired" });
|
|
331
|
+
this.metrics?.incrementGauge("redisx_locks_active");
|
|
332
|
+
span?.setAttribute("lock.acquired", true);
|
|
333
|
+
span?.setAttribute("lock.attempts", attempt + 1);
|
|
334
|
+
span?.setStatus("OK");
|
|
335
|
+
const lock = this.createLock(fullKey, token, ttl, options);
|
|
336
|
+
this.activeLocks.add(lock);
|
|
337
|
+
return lock;
|
|
338
|
+
}
|
|
339
|
+
if (attempt === retry.maxRetries) {
|
|
340
|
+
this.metrics?.incrementCounter("redisx_lock_acquisitions_total", { status: "failed" });
|
|
341
|
+
span?.setAttribute("lock.acquired", false);
|
|
342
|
+
span?.setAttribute("lock.attempts", attempt + 1);
|
|
343
|
+
span?.setStatus("ERROR");
|
|
344
|
+
throw new LockAcquisitionError(key, "timeout");
|
|
345
|
+
}
|
|
346
|
+
await this.sleep(delay);
|
|
347
|
+
delay = Math.min(delay * retry.multiplier, retry.maxDelay);
|
|
348
|
+
}
|
|
349
|
+
this.metrics?.incrementCounter("redisx_lock_acquisitions_total", { status: "failed" });
|
|
350
|
+
throw new LockAcquisitionError(key, "timeout");
|
|
351
|
+
} catch (error) {
|
|
352
|
+
span?.recordException(error);
|
|
353
|
+
span?.setStatus("ERROR");
|
|
354
|
+
throw error;
|
|
355
|
+
} finally {
|
|
356
|
+
span?.end();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Tries to acquire lock once without retry.
|
|
361
|
+
*/
|
|
362
|
+
async tryAcquire(key, options = {}) {
|
|
363
|
+
const fullKey = this.buildKey(key);
|
|
364
|
+
const ttl = this.resolveTtl(options.ttl);
|
|
365
|
+
const token = this.generateToken();
|
|
366
|
+
const acquired = await this.store.acquire(fullKey, token, ttl);
|
|
367
|
+
if (!acquired) {
|
|
368
|
+
this.metrics?.incrementCounter("redisx_lock_acquisitions_total", { status: "failed" });
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
this.metrics?.incrementCounter("redisx_lock_acquisitions_total", { status: "acquired" });
|
|
372
|
+
this.metrics?.incrementGauge("redisx_locks_active");
|
|
373
|
+
const lock = this.createLock(fullKey, token, ttl, options);
|
|
374
|
+
this.activeLocks.add(lock);
|
|
375
|
+
return lock;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Executes function with automatic lock management.
|
|
379
|
+
*/
|
|
380
|
+
async withLock(key, fn, options = {}) {
|
|
381
|
+
const lock = await this.acquire(key, options);
|
|
382
|
+
const holdStart = Date.now();
|
|
383
|
+
try {
|
|
384
|
+
return await fn();
|
|
385
|
+
} finally {
|
|
386
|
+
const holdDuration = (Date.now() - holdStart) / 1e3;
|
|
387
|
+
this.metrics?.observeHistogram("redisx_lock_hold_duration_seconds", holdDuration);
|
|
388
|
+
this.metrics?.decrementGauge("redisx_locks_active");
|
|
389
|
+
await lock.release().catch((error) => {
|
|
390
|
+
this.logger.error(`Lock release failed for ${key}:`, error);
|
|
391
|
+
}).finally(() => {
|
|
392
|
+
this.activeLocks.delete(lock);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Checks if key is locked.
|
|
398
|
+
*/
|
|
399
|
+
async isLocked(key) {
|
|
400
|
+
const fullKey = this.buildKey(key);
|
|
401
|
+
return this.store.exists(fullKey);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Force releases lock without ownership check.
|
|
405
|
+
*/
|
|
406
|
+
async forceRelease(key) {
|
|
407
|
+
const fullKey = this.buildKey(key);
|
|
408
|
+
return this.store.forceRelease(fullKey);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Creates lock instance with optional auto-renewal.
|
|
412
|
+
*/
|
|
413
|
+
createLock(fullKey, token, ttl, options) {
|
|
414
|
+
const lock = new Lock(fullKey, token, ttl, this.store);
|
|
415
|
+
const autoRenewEnabled = options.autoRenew ?? this.config.autoRenew?.enabled ?? true;
|
|
416
|
+
if (autoRenewEnabled) {
|
|
417
|
+
const intervalFraction = this.config.autoRenew?.intervalFraction ?? 0.5;
|
|
418
|
+
const interval = ttl * intervalFraction;
|
|
419
|
+
lock.startAutoRenew(interval);
|
|
420
|
+
}
|
|
421
|
+
return lock;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Builds full lock key with prefix.
|
|
425
|
+
*/
|
|
426
|
+
buildKey(key) {
|
|
427
|
+
const prefix = this.config.keyPrefix ?? "_lock:";
|
|
428
|
+
return `${prefix}${key}`;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Generates unique lock token.
|
|
432
|
+
*/
|
|
433
|
+
generateToken() {
|
|
434
|
+
return `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Resolves TTL with defaults and limits.
|
|
438
|
+
*/
|
|
439
|
+
resolveTtl(ttl) {
|
|
440
|
+
const resolvedTtl = ttl ?? this.config.defaultTtl ?? 3e4;
|
|
441
|
+
const maxTtl = this.config.maxTtl ?? 3e5;
|
|
442
|
+
return Math.min(resolvedTtl, maxTtl);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Resolves retry configuration.
|
|
446
|
+
*/
|
|
447
|
+
resolveRetryConfig(options) {
|
|
448
|
+
return {
|
|
449
|
+
maxRetries: options.retry?.maxRetries ?? this.config.retry?.maxRetries ?? 3,
|
|
450
|
+
initialDelay: options.retry?.initialDelay ?? this.config.retry?.initialDelay ?? 100,
|
|
451
|
+
maxDelay: this.config.retry?.maxDelay ?? 3e3,
|
|
452
|
+
multiplier: this.config.retry?.multiplier ?? 2
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Sleeps for specified milliseconds.
|
|
457
|
+
*/
|
|
458
|
+
sleep(ms) {
|
|
459
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
exports.LockService = __decorateClass([
|
|
463
|
+
common.Injectable(),
|
|
464
|
+
__decorateParam(0, common.Inject(LOCKS_PLUGIN_OPTIONS)),
|
|
465
|
+
__decorateParam(1, common.Inject(LOCK_STORE)),
|
|
466
|
+
__decorateParam(2, common.Optional()),
|
|
467
|
+
__decorateParam(2, common.Inject(METRICS_SERVICE)),
|
|
468
|
+
__decorateParam(3, common.Optional()),
|
|
469
|
+
__decorateParam(3, common.Inject(TRACING_SERVICE))
|
|
470
|
+
], exports.LockService);
|
|
471
|
+
|
|
472
|
+
// src/lock/infrastructure/scripts/lua-scripts.ts
|
|
473
|
+
var RELEASE_LOCK_SCRIPT = `
|
|
474
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
475
|
+
return redis.call("del", KEYS[1])
|
|
476
|
+
else
|
|
477
|
+
return 0
|
|
478
|
+
end
|
|
479
|
+
`.trim();
|
|
480
|
+
var EXTEND_LOCK_SCRIPT = `
|
|
481
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
482
|
+
return redis.call("pexpire", KEYS[1], ARGV[2])
|
|
483
|
+
else
|
|
484
|
+
return 0
|
|
485
|
+
end
|
|
486
|
+
`.trim();
|
|
487
|
+
|
|
488
|
+
// src/lock/infrastructure/adapters/redis-lock-store.adapter.ts
|
|
489
|
+
var RedisLockStoreAdapter = class {
|
|
490
|
+
constructor(driver) {
|
|
491
|
+
this.driver = driver;
|
|
492
|
+
}
|
|
493
|
+
releaseSha = null;
|
|
494
|
+
extendSha = null;
|
|
495
|
+
/**
|
|
496
|
+
* Lifecycle hook: loads Lua scripts into Redis on initialization.
|
|
497
|
+
*/
|
|
498
|
+
async onModuleInit() {
|
|
499
|
+
this.releaseSha = await this.driver.scriptLoad(RELEASE_LOCK_SCRIPT);
|
|
500
|
+
this.extendSha = await this.driver.scriptLoad(EXTEND_LOCK_SCRIPT);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Acquires lock using SET NX PX.
|
|
504
|
+
*/
|
|
505
|
+
async acquire(key, token, ttlMs) {
|
|
506
|
+
const result = await this.driver.set(key, token, {
|
|
507
|
+
nx: true,
|
|
508
|
+
// Only set if key doesn't exist
|
|
509
|
+
px: ttlMs
|
|
510
|
+
// TTL in milliseconds
|
|
511
|
+
});
|
|
512
|
+
return result === "OK";
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Releases lock if owned by token (Lua script).
|
|
516
|
+
*/
|
|
517
|
+
async release(key, token) {
|
|
518
|
+
if (!this.releaseSha) {
|
|
519
|
+
this.releaseSha = await this.driver.scriptLoad(RELEASE_LOCK_SCRIPT);
|
|
520
|
+
}
|
|
521
|
+
const result = await this.driver.evalsha(this.releaseSha, [key], [token]);
|
|
522
|
+
return result === 1;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Extends lock TTL if owned by token (Lua script).
|
|
526
|
+
*/
|
|
527
|
+
async extend(key, token, ttlMs) {
|
|
528
|
+
if (!this.extendSha) {
|
|
529
|
+
this.extendSha = await this.driver.scriptLoad(EXTEND_LOCK_SCRIPT);
|
|
530
|
+
}
|
|
531
|
+
const result = await this.driver.evalsha(this.extendSha, [key], [token, ttlMs]);
|
|
532
|
+
return result === 1;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Checks if lock key exists.
|
|
536
|
+
*/
|
|
537
|
+
async exists(key) {
|
|
538
|
+
const count = await this.driver.exists(key);
|
|
539
|
+
return count > 0;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Checks if lock is held by specific token.
|
|
543
|
+
*/
|
|
544
|
+
async isHeldBy(key, token) {
|
|
545
|
+
const value = await this.driver.get(key);
|
|
546
|
+
return value === token;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Force removes lock without ownership check.
|
|
550
|
+
*/
|
|
551
|
+
async forceRelease(key) {
|
|
552
|
+
const count = await this.driver.del(key);
|
|
553
|
+
return count > 0;
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
RedisLockStoreAdapter = __decorateClass([
|
|
557
|
+
common.Injectable(),
|
|
558
|
+
__decorateParam(0, common.Inject(core.REDIS_DRIVER))
|
|
559
|
+
], RedisLockStoreAdapter);
|
|
560
|
+
|
|
561
|
+
// src/locks.plugin.ts
|
|
562
|
+
var DEFAULT_LOCKS_CONFIG = {
|
|
563
|
+
defaultTtl: 3e4,
|
|
564
|
+
maxTtl: 3e5,
|
|
565
|
+
keyPrefix: "_lock:",
|
|
566
|
+
retry: {
|
|
567
|
+
maxRetries: 3,
|
|
568
|
+
initialDelay: 100,
|
|
569
|
+
maxDelay: 3e3,
|
|
570
|
+
multiplier: 2
|
|
571
|
+
},
|
|
572
|
+
autoRenew: {
|
|
573
|
+
enabled: true,
|
|
574
|
+
intervalFraction: 0.5
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
var LocksPlugin = class {
|
|
578
|
+
constructor(options = {}) {
|
|
579
|
+
this.options = options;
|
|
580
|
+
}
|
|
581
|
+
name = "locks";
|
|
582
|
+
version = "0.1.0";
|
|
583
|
+
description = "Distributed locking with auto-renewal and retry strategies";
|
|
584
|
+
getProviders() {
|
|
585
|
+
const config = {
|
|
586
|
+
defaultTtl: this.options.defaultTtl ?? DEFAULT_LOCKS_CONFIG.defaultTtl,
|
|
587
|
+
maxTtl: this.options.maxTtl ?? DEFAULT_LOCKS_CONFIG.maxTtl,
|
|
588
|
+
keyPrefix: this.options.keyPrefix ?? DEFAULT_LOCKS_CONFIG.keyPrefix,
|
|
589
|
+
retry: {
|
|
590
|
+
...DEFAULT_LOCKS_CONFIG.retry,
|
|
591
|
+
...this.options.retry
|
|
592
|
+
},
|
|
593
|
+
autoRenew: {
|
|
594
|
+
...DEFAULT_LOCKS_CONFIG.autoRenew,
|
|
595
|
+
...this.options.autoRenew
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
return [
|
|
599
|
+
// Configuration
|
|
600
|
+
{
|
|
601
|
+
provide: LOCKS_PLUGIN_OPTIONS,
|
|
602
|
+
useValue: config
|
|
603
|
+
},
|
|
604
|
+
// Store adapter
|
|
605
|
+
{
|
|
606
|
+
provide: LOCK_STORE,
|
|
607
|
+
useClass: RedisLockStoreAdapter
|
|
608
|
+
},
|
|
609
|
+
// Application service
|
|
610
|
+
{
|
|
611
|
+
provide: LOCK_SERVICE,
|
|
612
|
+
useClass: exports.LockService
|
|
613
|
+
},
|
|
614
|
+
// @WithLock decorator initialization (proxy-based)
|
|
615
|
+
LockDecoratorInitializerService,
|
|
616
|
+
// Reflector is needed for decorator metadata
|
|
617
|
+
core$1.Reflector
|
|
618
|
+
];
|
|
619
|
+
}
|
|
620
|
+
getExports() {
|
|
621
|
+
return [LOCK_SERVICE];
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
exports.LOCKS_PLUGIN_OPTIONS = LOCKS_PLUGIN_OPTIONS;
|
|
626
|
+
exports.LOCK_SERVICE = LOCK_SERVICE;
|
|
627
|
+
exports.LockAcquisitionError = LockAcquisitionError;
|
|
628
|
+
exports.LockError = LockError;
|
|
629
|
+
exports.LockExpiredError = LockExpiredError;
|
|
630
|
+
exports.LockExtensionError = LockExtensionError;
|
|
631
|
+
exports.LockNotOwnedError = LockNotOwnedError;
|
|
632
|
+
exports.LocksPlugin = LocksPlugin;
|
|
633
|
+
exports.WithLock = WithLock;
|
|
634
|
+
//# sourceMappingURL=index.js.map
|
|
635
|
+
//# sourceMappingURL=index.js.map
|