ioredis-toolkit 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +645 -0
  3. package/dist/cache.d.ts +298 -0
  4. package/dist/cache.js +606 -0
  5. package/dist/client.d.ts +177 -0
  6. package/dist/client.js +958 -0
  7. package/dist/cluster-slot.d.ts +4 -0
  8. package/dist/cluster-slot.js +31 -0
  9. package/dist/cluster.d.ts +79 -0
  10. package/dist/cluster.js +156 -0
  11. package/dist/errors.d.ts +30 -0
  12. package/dist/errors.js +63 -0
  13. package/dist/health.d.ts +39 -0
  14. package/dist/health.js +106 -0
  15. package/dist/index.d.ts +51 -0
  16. package/dist/index.js +44 -0
  17. package/dist/lock.d.ts +215 -0
  18. package/dist/lock.js +385 -0
  19. package/dist/logger.d.ts +12 -0
  20. package/dist/logger.js +40 -0
  21. package/dist/pubsub.d.ts +171 -0
  22. package/dist/pubsub.js +285 -0
  23. package/dist/ratelimiter.d.ts +162 -0
  24. package/dist/ratelimiter.js +289 -0
  25. package/dist/session/index.d.ts +23 -0
  26. package/dist/session/index.js +16 -0
  27. package/dist/session/revocation-store.d.ts +171 -0
  28. package/dist/session/revocation-store.js +310 -0
  29. package/dist/session/scripts/cleanup-index.lua +21 -0
  30. package/dist/session/scripts/conditional-update-encrypted.lua +60 -0
  31. package/dist/session/scripts/conditional-update.lua +63 -0
  32. package/dist/session/scripts/create.lua +68 -0
  33. package/dist/session/scripts/delete-by-user.lua +29 -0
  34. package/dist/session/scripts/delete.lua +15 -0
  35. package/dist/session/scripts/enforce-limit.lua +38 -0
  36. package/dist/session/scripts/revoke.lua +61 -0
  37. package/dist/session/scripts/rotate-encrypted.lua +107 -0
  38. package/dist/session/scripts/rotate.lua +119 -0
  39. package/dist/session/scripts/touch-encrypted.lua +89 -0
  40. package/dist/session/scripts/touch.lua +72 -0
  41. package/dist/session/scripts/validate.lua +90 -0
  42. package/dist/session/session-circuit-breaker.d.ts +42 -0
  43. package/dist/session/session-circuit-breaker.js +129 -0
  44. package/dist/session/session-config.d.ts +335 -0
  45. package/dist/session/session-config.js +162 -0
  46. package/dist/session/session-cookie.d.ts +72 -0
  47. package/dist/session/session-cookie.js +101 -0
  48. package/dist/session/session-encryption.d.ts +87 -0
  49. package/dist/session/session-encryption.js +139 -0
  50. package/dist/session/session-errors.d.ts +85 -0
  51. package/dist/session/session-errors.js +145 -0
  52. package/dist/session/session-health.d.ts +38 -0
  53. package/dist/session/session-health.js +60 -0
  54. package/dist/session/session-keys.d.ts +51 -0
  55. package/dist/session/session-keys.js +113 -0
  56. package/dist/session/session-manager.d.ts +59 -0
  57. package/dist/session/session-manager.js +94 -0
  58. package/dist/session/session-metrics.d.ts +33 -0
  59. package/dist/session/session-metrics.js +112 -0
  60. package/dist/session/session-repository.d.ts +161 -0
  61. package/dist/session/session-repository.js +683 -0
  62. package/dist/session/session-scripts.d.ts +36 -0
  63. package/dist/session/session-scripts.js +130 -0
  64. package/dist/session/session-serializer.d.ts +42 -0
  65. package/dist/session/session-serializer.js +248 -0
  66. package/dist/session/session-service.d.ts +104 -0
  67. package/dist/session/session-service.js +611 -0
  68. package/dist/session/session-token.d.ts +38 -0
  69. package/dist/session/session-token.js +86 -0
  70. package/dist/session/session-types.d.ts +253 -0
  71. package/dist/session/session-types.js +16 -0
  72. package/dist/types.d.ts +782 -0
  73. package/dist/types.js +140 -0
  74. package/package.json +97 -0
package/dist/lock.d.ts ADDED
@@ -0,0 +1,215 @@
1
+ import { RedisClientWrapper } from './client.js';
2
+ import { LoggerLike } from './logger.js';
3
+ /**
4
+ * Information about a distributed lock.
5
+ */
6
+ export interface LockInfo {
7
+ /** Whether the lock is currently held. */
8
+ locked: boolean;
9
+ /** Remaining TTL in seconds (when held and TTL set). */
10
+ ttl?: number;
11
+ /** Unique owner id of the lock. */
12
+ lockId?: string;
13
+ }
14
+ /**
15
+ * Options for the distributed lock.
16
+ */
17
+ export interface DistributedLockOptions {
18
+ /** Lock TTL in milliseconds. Default: `30000`. */
19
+ ttl?: number;
20
+ /** Number of acquisition attempts. Default: `3`. */
21
+ retryCount?: number;
22
+ /** Base delay between retries in ms (grows exponentially). Default: `200`. */
23
+ retryDelay?: number;
24
+ }
25
+ /**
26
+ * Distributed mutual-exclusion lock backed by Redis.
27
+ *
28
+ * Works in standalone, sentinel and cluster modes. Acquisition uses atomic
29
+ * `SET ... PX NX`; release and extension use Lua scripts so only the lock owner
30
+ * can release or extend. `withLock` auto-extends the lock at half TTL while the
31
+ * critical section runs and always releases afterwards.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const lock = new DistributedLock(client, { ttl: 30000, retryCount: 5 });
36
+ * const acquired = await lock.acquire('order:42');
37
+ * if (acquired) {
38
+ * try {
39
+ * // critical section
40
+ * } finally {
41
+ * await lock.release('order:42');
42
+ * }
43
+ * }
44
+ * ```
45
+ */
46
+ export declare class DistributedLock {
47
+ private client;
48
+ private logger;
49
+ private defaultTTL;
50
+ private defaultRetryCount;
51
+ private defaultRetryDelay;
52
+ /**
53
+ * Creates a distributed lock bound to a Redis client.
54
+ *
55
+ * @param client - The underlying {@link RedisClientWrapper}.
56
+ * @param logger - Optional pino-compatible logger; defaults to `console`.
57
+ * @param options - Defaults for `ttl` (ms), `retryCount` and `retryDelay`.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const lock = new DistributedLock(client, { ttl: 10000, retryCount: 3 });
62
+ * ```
63
+ */
64
+ constructor(client: RedisClientWrapper, logger?: LoggerLike, options?: Partial<DistributedLockOptions>);
65
+ private getLockKey;
66
+ private generateLockId;
67
+ private executeWithRetry;
68
+ /**
69
+ * Attempts to acquire the lock for a key.
70
+ *
71
+ * Uses atomic `SET lock:<key> <id> PX <ttl> NX` with exponential backoff
72
+ * retries. Locks expire automatically after `ttl` ms, so a crashed holder
73
+ * never blocks others forever.
74
+ *
75
+ * @param key - The resource to lock, e.g. `'order:42'` (stored as `lock:order:42`).
76
+ * @param ttl - Lock TTL in milliseconds (default: `30000`).
77
+ * @returns `true` when the lock was acquired.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const acquired = await lock.acquire('order:42', 10000);
82
+ * ```
83
+ */
84
+ acquire(key: string, ttl?: number): Promise<boolean>;
85
+ /**
86
+ * Releases the lock, but only if this process still owns it.
87
+ *
88
+ * Uses an atomic Lua check-and-delete so a lock whose TTL expired (and was
89
+ * re-acquired by someone else) is never removed by the old owner.
90
+ *
91
+ * @param key - The locked resource.
92
+ * @returns `true` if the lock was released, `false` if not owned or missing.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * await lock.release('order:42');
97
+ * ```
98
+ */
99
+ release(key: string): Promise<boolean>;
100
+ /**
101
+ * Force-releases a lock without checking ownership.
102
+ *
103
+ * Use with care: only for emergency cleanup or when the holder is known to
104
+ * be gone. This is what `withLock` falls back to when a normal release fails.
105
+ *
106
+ * @param key - The locked resource.
107
+ * @returns `true` if a lock existed and was deleted.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * await lock.releaseForce('order:42');
112
+ * ```
113
+ */
114
+ releaseForce(key: string): Promise<boolean>;
115
+ /**
116
+ * Extends the TTL of a lock this process still owns.
117
+ *
118
+ * Uses an atomic Lua script so a re-acquired lock is never extended by the
119
+ * old owner.
120
+ *
121
+ * @param key - The locked resource.
122
+ * @param ttl - New TTL in milliseconds (default: `30000`).
123
+ * @returns `true` if the lock was extended.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * const extended = await lock.extend('order:42', 30000);
128
+ * ```
129
+ */
130
+ extend(key: string, ttl?: number): Promise<boolean>;
131
+ /**
132
+ * Runs a critical section while holding a lock.
133
+ *
134
+ * Acquires the lock (with retries), auto-extends it at half TTL while `fn`
135
+ * runs, detects a lost lock, and always releases afterwards (force-releasing
136
+ * if a normal release fails).
137
+ *
138
+ * @param key - The resource to lock.
139
+ * @param fn - The critical section to run exclusively.
140
+ * @param options - Per-call `ttl` (ms), `retryCount`, `retryDelay`.
141
+ * @returns The return value of `fn`.
142
+ * @throws {@link RedisError} with code `LOCK_ACQUISITION_FAILED` when the lock
143
+ * cannot be acquired, or `LOCK_LOST` when the lock expired mid-execution.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * const result = await lock.withLock('inventory:sku-1', async () => {
148
+ * return await updateStock();
149
+ * });
150
+ * ```
151
+ */
152
+ withLock<T>(key: string, fn: () => Promise<T>, options?: DistributedLockOptions): Promise<T>;
153
+ /**
154
+ * Checks whether a lock is currently held.
155
+ *
156
+ * @param key - The locked resource.
157
+ * @returns `true` if the lock exists (held by anyone).
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * const busy = await lock.isLocked('order:42');
162
+ * ```
163
+ */
164
+ isLocked(key: string): Promise<boolean>;
165
+ /**
166
+ * Returns details about a lock.
167
+ *
168
+ * @param key - The locked resource.
169
+ * @returns `{ locked: false }` when not held, otherwise `{ locked: true, ttl, lockId }`.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * const info = await lock.getLockInfo('order:42');
174
+ * // { locked: true, ttl: 29, lockId: 'a1b2c3...' }
175
+ * ```
176
+ */
177
+ getLockInfo(key: string): Promise<LockInfo>;
178
+ /**
179
+ * Returns the owner id of a lock.
180
+ *
181
+ * @param key - The locked resource.
182
+ * @returns The lock id (random hex token), or `null` when not held.
183
+ *
184
+ * @example
185
+ * ```ts
186
+ * const owner = await lock.getLockOwner('order:42');
187
+ * ```
188
+ */
189
+ getLockOwner(key: string): Promise<string | null>;
190
+ /**
191
+ * Returns the remaining TTL of a lock in seconds.
192
+ *
193
+ * @param key - The locked resource.
194
+ * @returns Remaining seconds (`0` when not held or expired).
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * const remaining = await lock.getLockTTL('order:42');
199
+ * ```
200
+ */
201
+ getLockTTL(key: string): Promise<number>;
202
+ /**
203
+ * Deletes every lock key (`lock:*`) from Redis.
204
+ *
205
+ * Intended for tests and emergency recovery only.
206
+ *
207
+ * @returns The number of deleted locks.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * const removed = await lock.cleanupAll();
212
+ * ```
213
+ */
214
+ cleanupAll(): Promise<number>;
215
+ }
package/dist/lock.js ADDED
@@ -0,0 +1,385 @@
1
+ import { RedisError } from './errors.js';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { defaultLogger } from './logger.js';
4
+ /**
5
+ * Distributed mutual-exclusion lock backed by Redis.
6
+ *
7
+ * Works in standalone, sentinel and cluster modes. Acquisition uses atomic
8
+ * `SET ... PX NX`; release and extension use Lua scripts so only the lock owner
9
+ * can release or extend. `withLock` auto-extends the lock at half TTL while the
10
+ * critical section runs and always releases afterwards.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const lock = new DistributedLock(client, { ttl: 30000, retryCount: 5 });
15
+ * const acquired = await lock.acquire('order:42');
16
+ * if (acquired) {
17
+ * try {
18
+ * // critical section
19
+ * } finally {
20
+ * await lock.release('order:42');
21
+ * }
22
+ * }
23
+ * ```
24
+ */
25
+ export class DistributedLock {
26
+ client;
27
+ logger;
28
+ defaultTTL;
29
+ defaultRetryCount;
30
+ defaultRetryDelay;
31
+ /**
32
+ * Creates a distributed lock bound to a Redis client.
33
+ *
34
+ * @param client - The underlying {@link RedisClientWrapper}.
35
+ * @param logger - Optional pino-compatible logger; defaults to `console`.
36
+ * @param options - Defaults for `ttl` (ms), `retryCount` and `retryDelay`.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const lock = new DistributedLock(client, { ttl: 10000, retryCount: 3 });
41
+ * ```
42
+ */
43
+ constructor(client, logger = defaultLogger, options = {}) {
44
+ this.client = client;
45
+ this.logger = logger.child({ component: 'DistributedLock' });
46
+ this.defaultTTL = options.ttl || 30000;
47
+ this.defaultRetryCount = options.retryCount || 3;
48
+ this.defaultRetryDelay = options.retryDelay || 200;
49
+ }
50
+ getLockKey(key) {
51
+ return `lock:${key}`;
52
+ }
53
+ generateLockId() {
54
+ return randomBytes(16).toString('hex');
55
+ }
56
+ async executeWithRetry(fn, retryCount = this.defaultRetryCount, retryDelay = this.defaultRetryDelay) {
57
+ let lastError = null;
58
+ for (let i = 0; i < retryCount; i++) {
59
+ try {
60
+ return await fn();
61
+ }
62
+ catch (error) {
63
+ lastError = error;
64
+ if (i < retryCount - 1) {
65
+ const delay = retryDelay * Math.pow(2, i) * (0.5 + Math.random() * 0.5);
66
+ await new Promise(resolve => setTimeout(resolve, delay));
67
+ }
68
+ }
69
+ }
70
+ throw lastError || new Error('Retry failed');
71
+ }
72
+ /**
73
+ * Attempts to acquire the lock for a key.
74
+ *
75
+ * Uses atomic `SET lock:<key> <id> PX <ttl> NX` with exponential backoff
76
+ * retries. Locks expire automatically after `ttl` ms, so a crashed holder
77
+ * never blocks others forever.
78
+ *
79
+ * @param key - The resource to lock, e.g. `'order:42'` (stored as `lock:order:42`).
80
+ * @param ttl - Lock TTL in milliseconds (default: `30000`).
81
+ * @returns `true` when the lock was acquired.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const acquired = await lock.acquire('order:42', 10000);
86
+ * ```
87
+ */
88
+ async acquire(key, ttl = this.defaultTTL) {
89
+ const lockKey = this.getLockKey(key);
90
+ const lockId = this.generateLockId();
91
+ return this.executeWithRetry(async () => {
92
+ // Using SET with PX and NX for atomic lock acquisition
93
+ const result = await this.client.raw.set(lockKey, lockId, 'PX', ttl, 'NX');
94
+ return result === 'OK';
95
+ });
96
+ }
97
+ /**
98
+ * Releases the lock, but only if this process still owns it.
99
+ *
100
+ * Uses an atomic Lua check-and-delete so a lock whose TTL expired (and was
101
+ * re-acquired by someone else) is never removed by the old owner.
102
+ *
103
+ * @param key - The locked resource.
104
+ * @returns `true` if the lock was released, `false` if not owned or missing.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * await lock.release('order:42');
109
+ * ```
110
+ */
111
+ async release(key) {
112
+ const lockKey = this.getLockKey(key);
113
+ try {
114
+ // Use Lua script for atomic check-and-delete
115
+ const script = `
116
+ if redis.call('get', KEYS[1]) == ARGV[1] then
117
+ return redis.call('del', KEYS[1])
118
+ else
119
+ return 0
120
+ end
121
+ `;
122
+ const lockId = await this.client.raw.get(lockKey);
123
+ if (!lockId) {
124
+ this.logger.warn('Lock not found for release', { key });
125
+ return false;
126
+ }
127
+ const result = await this.client.raw.eval(script, 1, lockKey, lockId);
128
+ return result === 1;
129
+ }
130
+ catch (error) {
131
+ this.logger.error('Failed to release lock', { key, error });
132
+ return false;
133
+ }
134
+ }
135
+ /**
136
+ * Force-releases a lock without checking ownership.
137
+ *
138
+ * Use with care: only for emergency cleanup or when the holder is known to
139
+ * be gone. This is what `withLock` falls back to when a normal release fails.
140
+ *
141
+ * @param key - The locked resource.
142
+ * @returns `true` if a lock existed and was deleted.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * await lock.releaseForce('order:42');
147
+ * ```
148
+ */
149
+ async releaseForce(key) {
150
+ const lockKey = this.getLockKey(key);
151
+ const result = await this.client.raw.del(lockKey);
152
+ return result === 1;
153
+ }
154
+ /**
155
+ * Extends the TTL of a lock this process still owns.
156
+ *
157
+ * Uses an atomic Lua script so a re-acquired lock is never extended by the
158
+ * old owner.
159
+ *
160
+ * @param key - The locked resource.
161
+ * @param ttl - New TTL in milliseconds (default: `30000`).
162
+ * @returns `true` if the lock was extended.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * const extended = await lock.extend('order:42', 30000);
167
+ * ```
168
+ */
169
+ async extend(key, ttl = this.defaultTTL) {
170
+ const lockKey = this.getLockKey(key);
171
+ const script = `
172
+ if redis.call('get', KEYS[1]) == ARGV[1] then
173
+ return redis.call('pexpire', KEYS[1], ARGV[2])
174
+ else
175
+ return 0
176
+ end
177
+ `;
178
+ try {
179
+ const lockId = await this.client.raw.get(lockKey);
180
+ if (!lockId) {
181
+ return false;
182
+ }
183
+ const result = await this.client.raw.eval(script, 1, lockKey, lockId, ttl);
184
+ return result === 1;
185
+ }
186
+ catch (error) {
187
+ this.logger.error('Failed to extend lock', { key, error });
188
+ return false;
189
+ }
190
+ }
191
+ /**
192
+ * Runs a critical section while holding a lock.
193
+ *
194
+ * Acquires the lock (with retries), auto-extends it at half TTL while `fn`
195
+ * runs, detects a lost lock, and always releases afterwards (force-releasing
196
+ * if a normal release fails).
197
+ *
198
+ * @param key - The resource to lock.
199
+ * @param fn - The critical section to run exclusively.
200
+ * @param options - Per-call `ttl` (ms), `retryCount`, `retryDelay`.
201
+ * @returns The return value of `fn`.
202
+ * @throws {@link RedisError} with code `LOCK_ACQUISITION_FAILED` when the lock
203
+ * cannot be acquired, or `LOCK_LOST` when the lock expired mid-execution.
204
+ *
205
+ * @example
206
+ * ```ts
207
+ * const result = await lock.withLock('inventory:sku-1', async () => {
208
+ * return await updateStock();
209
+ * });
210
+ * ```
211
+ */
212
+ async withLock(key, fn, options = {}) {
213
+ const ttl = options.ttl || this.defaultTTL;
214
+ const retryCount = options.retryCount || this.defaultRetryCount;
215
+ const retryDelay = options.retryDelay || this.defaultRetryDelay;
216
+ // Try to acquire the lock with retries
217
+ const acquired = await this.acquire(key, ttl);
218
+ if (!acquired) {
219
+ throw new RedisError(`Failed to acquire lock for key: ${key} after ${retryCount} attempts`, 'LOCK_ACQUISITION_FAILED');
220
+ }
221
+ let extensionTimer = null;
222
+ let lockRenewed = true;
223
+ try {
224
+ // Start auto-extension timer at half TTL
225
+ const extendInterval = Math.floor(ttl / 2);
226
+ let isExtending = false;
227
+ const extendLock = async () => {
228
+ if (isExtending || !lockRenewed)
229
+ return;
230
+ isExtending = true;
231
+ try {
232
+ const extended = await this.extend(key, ttl);
233
+ if (!extended) {
234
+ lockRenewed = false;
235
+ this.logger.warn('Lock extension failed', { key });
236
+ }
237
+ }
238
+ catch (error) {
239
+ this.logger.error('Lock extension error', { key, error });
240
+ lockRenewed = false;
241
+ }
242
+ finally {
243
+ isExtending = false;
244
+ }
245
+ };
246
+ // Schedule auto-extension
247
+ extensionTimer = setInterval(() => {
248
+ extendLock().catch((error) => {
249
+ this.logger.error('Extension interval error', { key, error });
250
+ });
251
+ }, extendInterval);
252
+ // Execute the function
253
+ const result = await fn();
254
+ // Check if lock was maintained during execution
255
+ if (!lockRenewed) {
256
+ throw new RedisError(`Lock was lost during execution for key: ${key}`, 'LOCK_LOST');
257
+ }
258
+ return result;
259
+ }
260
+ catch (error) {
261
+ this.logger.error('Error in locked operation', { key, error });
262
+ throw error;
263
+ }
264
+ finally {
265
+ // Clean up extension timer
266
+ if (extensionTimer) {
267
+ clearInterval(extensionTimer);
268
+ extensionTimer = null;
269
+ }
270
+ // Release the lock
271
+ try {
272
+ await this.release(key);
273
+ }
274
+ catch (releaseError) {
275
+ this.logger.error('Failed to release lock', { key, releaseError });
276
+ try {
277
+ await this.releaseForce(key);
278
+ }
279
+ catch (forceError) {
280
+ this.logger.error('Failed to force release lock', { key, forceError });
281
+ }
282
+ }
283
+ }
284
+ }
285
+ /**
286
+ * Checks whether a lock is currently held.
287
+ *
288
+ * @param key - The locked resource.
289
+ * @returns `true` if the lock exists (held by anyone).
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * const busy = await lock.isLocked('order:42');
294
+ * ```
295
+ */
296
+ async isLocked(key) {
297
+ const lockKey = this.getLockKey(key);
298
+ const exists = await this.client.raw.exists(lockKey);
299
+ return exists === 1;
300
+ }
301
+ /**
302
+ * Returns details about a lock.
303
+ *
304
+ * @param key - The locked resource.
305
+ * @returns `{ locked: false }` when not held, otherwise `{ locked: true, ttl, lockId }`.
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * const info = await lock.getLockInfo('order:42');
310
+ * // { locked: true, ttl: 29, lockId: 'a1b2c3...' }
311
+ * ```
312
+ */
313
+ async getLockInfo(key) {
314
+ const lockKey = this.getLockKey(key);
315
+ const exists = await this.client.raw.exists(lockKey);
316
+ if (!exists) {
317
+ return { locked: false };
318
+ }
319
+ const [lockId, ttl] = await Promise.all([
320
+ this.client.raw.get(lockKey),
321
+ this.client.raw.ttl(lockKey),
322
+ ]);
323
+ // Build the result object with proper undefined handling
324
+ const result = { locked: true };
325
+ if (lockId !== null && lockId !== undefined) {
326
+ result.lockId = lockId;
327
+ }
328
+ if (ttl !== null && ttl !== undefined && ttl > 0) {
329
+ result.ttl = ttl;
330
+ }
331
+ return result;
332
+ }
333
+ /**
334
+ * Returns the owner id of a lock.
335
+ *
336
+ * @param key - The locked resource.
337
+ * @returns The lock id (random hex token), or `null` when not held.
338
+ *
339
+ * @example
340
+ * ```ts
341
+ * const owner = await lock.getLockOwner('order:42');
342
+ * ```
343
+ */
344
+ async getLockOwner(key) {
345
+ const lockKey = this.getLockKey(key);
346
+ return this.client.raw.get(lockKey);
347
+ }
348
+ /**
349
+ * Returns the remaining TTL of a lock in seconds.
350
+ *
351
+ * @param key - The locked resource.
352
+ * @returns Remaining seconds (`0` when not held or expired).
353
+ *
354
+ * @example
355
+ * ```ts
356
+ * const remaining = await lock.getLockTTL('order:42');
357
+ * ```
358
+ */
359
+ async getLockTTL(key) {
360
+ const lockKey = this.getLockKey(key);
361
+ const ttl = await this.client.raw.ttl(lockKey);
362
+ return ttl > 0 ? ttl : 0;
363
+ }
364
+ // Clean up all locks (for testing or emergency)
365
+ /**
366
+ * Deletes every lock key (`lock:*`) from Redis.
367
+ *
368
+ * Intended for tests and emergency recovery only.
369
+ *
370
+ * @returns The number of deleted locks.
371
+ *
372
+ * @example
373
+ * ```ts
374
+ * const removed = await lock.cleanupAll();
375
+ * ```
376
+ */
377
+ async cleanupAll() {
378
+ let deleted = 0;
379
+ for await (const key of this.client.scanIterator('lock:*')) {
380
+ const result = await this.client.raw.del(key);
381
+ deleted += result;
382
+ }
383
+ return deleted;
384
+ }
385
+ }
@@ -0,0 +1,12 @@
1
+ export type LogMeta = Record<string, unknown>;
2
+ export interface LoggerLike {
3
+ trace(message: string, meta?: LogMeta): void;
4
+ debug(message: string, meta?: LogMeta): void;
5
+ info(message: string, meta?: LogMeta): void;
6
+ warn(message: string, meta?: LogMeta): void;
7
+ error(message: string, meta?: LogMeta): void;
8
+ fatal(message: string, meta?: LogMeta): void;
9
+ child(bindings: LogMeta): LoggerLike;
10
+ }
11
+ export declare const defaultLogger: LoggerLike;
12
+ export declare function createConsoleLogger(bindings?: LogMeta): LoggerLike;
package/dist/logger.js ADDED
@@ -0,0 +1,40 @@
1
+ function mergeMeta(bindings, meta) {
2
+ return {
3
+ ...bindings,
4
+ ...(meta ?? {}),
5
+ };
6
+ }
7
+ class ConsoleLogger {
8
+ bindings;
9
+ constructor(bindings = {}) {
10
+ this.bindings = bindings;
11
+ }
12
+ trace(message, meta) {
13
+ console.trace(message, mergeMeta(this.bindings, meta));
14
+ }
15
+ debug(message, meta) {
16
+ console.debug(message, mergeMeta(this.bindings, meta));
17
+ }
18
+ info(message, meta) {
19
+ console.info(message, mergeMeta(this.bindings, meta));
20
+ }
21
+ warn(message, meta) {
22
+ console.warn(message, mergeMeta(this.bindings, meta));
23
+ }
24
+ error(message, meta) {
25
+ console.error(message, mergeMeta(this.bindings, meta));
26
+ }
27
+ fatal(message, meta) {
28
+ console.error(message, mergeMeta(this.bindings, meta));
29
+ }
30
+ child(bindings) {
31
+ return new ConsoleLogger({
32
+ ...this.bindings,
33
+ ...bindings,
34
+ });
35
+ }
36
+ }
37
+ export const defaultLogger = new ConsoleLogger();
38
+ export function createConsoleLogger(bindings) {
39
+ return new ConsoleLogger(bindings);
40
+ }