@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distributed Locking
|
|
3
|
+
*
|
|
4
|
+
* Redis-based distributed locks with in-memory fallback.
|
|
5
|
+
* Uses Bun 1.3+ native Redis client for production.
|
|
6
|
+
*
|
|
7
|
+
* Implementation based on Redis SET NX PX pattern with Lua scripts
|
|
8
|
+
* for safe lock release and extension.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ============= Types =============
|
|
12
|
+
|
|
13
|
+
export interface LockConfig {
|
|
14
|
+
driver?: "redis" | "memory";
|
|
15
|
+
url?: string; // Redis URL
|
|
16
|
+
keyPrefix?: string;
|
|
17
|
+
defaultTTL?: number; // Default TTL in milliseconds
|
|
18
|
+
retryCount?: number; // Number of retry attempts
|
|
19
|
+
retryDelay?: number; // Delay between retries in milliseconds
|
|
20
|
+
autoExtend?: boolean; // Auto-extend lock for long operations
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LockOptions {
|
|
24
|
+
ttl?: number; // Lock TTL in milliseconds
|
|
25
|
+
retryCount?: number; // Override retry count
|
|
26
|
+
retryDelay?: number; // Override retry delay
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Lock {
|
|
30
|
+
key: string;
|
|
31
|
+
value: string;
|
|
32
|
+
acquired: boolean;
|
|
33
|
+
acquiredAt: number;
|
|
34
|
+
ttl: number;
|
|
35
|
+
expiresAt: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LockHandle {
|
|
39
|
+
/** Whether the lock was successfully acquired */
|
|
40
|
+
acquired: boolean;
|
|
41
|
+
/** Release the lock */
|
|
42
|
+
release: () => Promise<boolean>;
|
|
43
|
+
/** Extend the lock TTL */
|
|
44
|
+
extend: (ttl?: number) => Promise<boolean>;
|
|
45
|
+
/** Check if lock is still held */
|
|
46
|
+
isValid: () => Promise<boolean>;
|
|
47
|
+
/** Get remaining TTL in milliseconds */
|
|
48
|
+
getRemainingTTL: () => Promise<number>;
|
|
49
|
+
/** The lock key */
|
|
50
|
+
key: string;
|
|
51
|
+
/** The lock value (unique identifier) */
|
|
52
|
+
value: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============= In-Memory Lock Driver =============
|
|
56
|
+
|
|
57
|
+
// Shared lock store for all in-memory lock instances
|
|
58
|
+
const sharedLockStore: Map<string, { value: string; expiresAt: number }> =
|
|
59
|
+
new Map();
|
|
60
|
+
let cleanupInterval: ReturnType<typeof setInterval> | null = null;
|
|
61
|
+
|
|
62
|
+
function ensureCleanup(): void {
|
|
63
|
+
if (!cleanupInterval) {
|
|
64
|
+
cleanupInterval = setInterval(() => {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
for (const [key, lock] of sharedLockStore.entries()) {
|
|
67
|
+
if (now >= lock.expiresAt) {
|
|
68
|
+
sharedLockStore.delete(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, 10000);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class MemoryLockDriver {
|
|
76
|
+
constructor() {
|
|
77
|
+
// Ensure cleanup is running
|
|
78
|
+
ensureCleanup();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async acquire(key: string, value: string, ttl: number): Promise<boolean> {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
const existing = sharedLockStore.get(key);
|
|
84
|
+
|
|
85
|
+
// Check if lock exists and is still valid
|
|
86
|
+
if (existing && now < existing.expiresAt) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Acquire the lock
|
|
91
|
+
sharedLockStore.set(key, {
|
|
92
|
+
value,
|
|
93
|
+
expiresAt: now + ttl,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async release(key: string, value: string): Promise<boolean> {
|
|
100
|
+
const lock = sharedLockStore.get(key);
|
|
101
|
+
|
|
102
|
+
// Only release if we own the lock
|
|
103
|
+
if (lock && lock.value === value) {
|
|
104
|
+
sharedLockStore.delete(key);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async extend(key: string, value: string, ttl: number): Promise<boolean> {
|
|
112
|
+
const lock = sharedLockStore.get(key);
|
|
113
|
+
|
|
114
|
+
// Only extend if we own the lock
|
|
115
|
+
if (lock && lock.value === value) {
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
|
|
118
|
+
// Check if lock hasn't expired
|
|
119
|
+
if (now >= lock.expiresAt) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
lock.expiresAt = now + ttl;
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async isValid(key: string, value: string): Promise<boolean> {
|
|
131
|
+
const lock = sharedLockStore.get(key);
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
|
|
134
|
+
return lock !== undefined && lock.value === value && now < lock.expiresAt;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async getTTL(key: string, value: string): Promise<number> {
|
|
138
|
+
const lock = sharedLockStore.get(key);
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
|
|
141
|
+
if (!lock || lock.value !== value) {
|
|
142
|
+
return -1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const remaining = lock.expiresAt - now;
|
|
146
|
+
return remaining > 0 ? remaining : -1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
destroy(): void {
|
|
150
|
+
// Don't clear the shared store, just stop cleanup if no more instances
|
|
151
|
+
// For simplicity, we keep cleanup running
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ============= Redis Lock Driver =============
|
|
156
|
+
|
|
157
|
+
class RedisLockDriver {
|
|
158
|
+
private client: unknown = null;
|
|
159
|
+
private url: string;
|
|
160
|
+
private _isConnected = false;
|
|
161
|
+
|
|
162
|
+
// Lua script for safe lock release
|
|
163
|
+
// Only releases the lock if we own it (value matches)
|
|
164
|
+
private readonly releaseScript = `
|
|
165
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
166
|
+
return redis.call("del", KEYS[1])
|
|
167
|
+
else
|
|
168
|
+
return 0
|
|
169
|
+
end
|
|
170
|
+
`;
|
|
171
|
+
|
|
172
|
+
// Lua script for safe lock extension
|
|
173
|
+
// Only extends if we own the lock
|
|
174
|
+
private readonly extendScript = `
|
|
175
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
176
|
+
return redis.call("pexpire", KEYS[1], ARGV[2])
|
|
177
|
+
else
|
|
178
|
+
return 0
|
|
179
|
+
end
|
|
180
|
+
`;
|
|
181
|
+
|
|
182
|
+
constructor(url: string) {
|
|
183
|
+
this.url = url;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async connect(): Promise<void> {
|
|
187
|
+
try {
|
|
188
|
+
const { RedisClient } = await import("bun");
|
|
189
|
+
this.client = new RedisClient(this.url);
|
|
190
|
+
this._isConnected = true;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Failed to connect to Redis: ${error instanceof Error ? error.message : String(error)}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async disconnect(): Promise<void> {
|
|
199
|
+
const client = this.client as { close?: () => Promise<void> } | null;
|
|
200
|
+
if (client?.close) {
|
|
201
|
+
await client.close();
|
|
202
|
+
}
|
|
203
|
+
this._isConnected = false;
|
|
204
|
+
this.client = null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
get isConnected(): boolean {
|
|
208
|
+
return this._isConnected;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async acquire(key: string, value: string, ttl: number): Promise<boolean> {
|
|
212
|
+
const client = this.client as {
|
|
213
|
+
set: (
|
|
214
|
+
key: string,
|
|
215
|
+
value: string,
|
|
216
|
+
options?: { nx?: boolean; px?: number },
|
|
217
|
+
) => Promise<string | null>;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// SET key value NX PX ttl
|
|
221
|
+
// NX = only set if not exists
|
|
222
|
+
// PX = set expiry in milliseconds
|
|
223
|
+
const result = await client.set(key, value, { nx: true, px: ttl });
|
|
224
|
+
return result === "OK";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async release(key: string, value: string): Promise<boolean> {
|
|
228
|
+
const client = this.client as {
|
|
229
|
+
eval: (script: string, keys: string[], args: string[]) => Promise<number>;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const result = await client.eval(this.releaseScript, [key], [value]);
|
|
233
|
+
return result === 1;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async extend(key: string, value: string, ttl: number): Promise<boolean> {
|
|
237
|
+
const client = this.client as {
|
|
238
|
+
eval: (script: string, keys: string[], args: string[]) => Promise<number>;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const result = await client.eval(
|
|
242
|
+
this.extendScript,
|
|
243
|
+
[key],
|
|
244
|
+
[value, String(ttl)],
|
|
245
|
+
);
|
|
246
|
+
return result === 1;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async isValid(key: string, value: string): Promise<boolean> {
|
|
250
|
+
const client = this.client as {
|
|
251
|
+
get: (key: string) => Promise<string | null>;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const stored = await client.get(key);
|
|
255
|
+
return stored === value;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async getTTL(key: string, value: string): Promise<number> {
|
|
259
|
+
const client = this.client as {
|
|
260
|
+
get: (key: string) => Promise<string | null>;
|
|
261
|
+
pttl: (key: string) => Promise<number>;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const stored = await client.get(key);
|
|
265
|
+
if (stored !== value) {
|
|
266
|
+
return -1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return client.pttl(key);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ============= Distributed Lock Class =============
|
|
274
|
+
|
|
275
|
+
export class DistributedLock {
|
|
276
|
+
private driver: MemoryLockDriver | RedisLockDriver;
|
|
277
|
+
private driverType: "redis" | "memory";
|
|
278
|
+
private keyPrefix: string;
|
|
279
|
+
private defaultTTL: number;
|
|
280
|
+
private defaultRetryCount: number;
|
|
281
|
+
private defaultRetryDelay: number;
|
|
282
|
+
private _isConnected = false;
|
|
283
|
+
|
|
284
|
+
constructor(config: LockConfig = {}) {
|
|
285
|
+
this.driverType = config.driver ?? "memory";
|
|
286
|
+
this.keyPrefix = config.keyPrefix ?? "lock:";
|
|
287
|
+
this.defaultTTL = config.defaultTTL ?? 30000; // 30 seconds
|
|
288
|
+
this.defaultRetryCount = config.retryCount ?? 3;
|
|
289
|
+
this.defaultRetryDelay = config.retryDelay ?? 200; // 200ms
|
|
290
|
+
|
|
291
|
+
if (this.driverType === "redis" && config.url) {
|
|
292
|
+
this.driver = new RedisLockDriver(config.url);
|
|
293
|
+
} else {
|
|
294
|
+
this.driver = new MemoryLockDriver();
|
|
295
|
+
this._isConnected = true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Connect to the lock backend (Redis only)
|
|
301
|
+
*/
|
|
302
|
+
async connect(): Promise<void> {
|
|
303
|
+
if (this.driver instanceof RedisLockDriver) {
|
|
304
|
+
await this.driver.connect();
|
|
305
|
+
}
|
|
306
|
+
this._isConnected = true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Disconnect from the lock backend
|
|
311
|
+
*/
|
|
312
|
+
async disconnect(): Promise<void> {
|
|
313
|
+
if (this.driver instanceof RedisLockDriver) {
|
|
314
|
+
await this.driver.disconnect();
|
|
315
|
+
} else {
|
|
316
|
+
this.driver.destroy();
|
|
317
|
+
}
|
|
318
|
+
this._isConnected = false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Check if connected
|
|
323
|
+
*/
|
|
324
|
+
get isConnected(): boolean {
|
|
325
|
+
return this._isConnected;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get the driver type
|
|
330
|
+
*/
|
|
331
|
+
getDriverType(): "redis" | "memory" {
|
|
332
|
+
return this.driverType;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Generate a unique lock value
|
|
337
|
+
*/
|
|
338
|
+
private generateLockValue(): string {
|
|
339
|
+
// Generate a unique identifier using crypto
|
|
340
|
+
const bytes = new Uint8Array(16);
|
|
341
|
+
crypto.getRandomValues(bytes);
|
|
342
|
+
return Array.from(bytes)
|
|
343
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
344
|
+
.join("");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Try to acquire a lock without retry
|
|
349
|
+
*/
|
|
350
|
+
private async tryAcquire(
|
|
351
|
+
key: string,
|
|
352
|
+
value: string,
|
|
353
|
+
ttl: number,
|
|
354
|
+
): Promise<boolean> {
|
|
355
|
+
return this.driver.acquire(key, value, ttl);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Acquire a lock
|
|
360
|
+
* Returns a LockHandle that can be used to release or extend the lock
|
|
361
|
+
*/
|
|
362
|
+
async acquire(key: string, options: LockOptions = {}): Promise<LockHandle> {
|
|
363
|
+
const fullKey = this.keyPrefix + key;
|
|
364
|
+
const ttl = options.ttl ?? this.defaultTTL;
|
|
365
|
+
const retryCount = options.retryCount ?? this.defaultRetryCount;
|
|
366
|
+
const retryDelay = options.retryDelay ?? this.defaultRetryDelay;
|
|
367
|
+
const value = this.generateLockValue();
|
|
368
|
+
|
|
369
|
+
let acquired = false;
|
|
370
|
+
let attempts = 0;
|
|
371
|
+
|
|
372
|
+
// Try to acquire with retries
|
|
373
|
+
while (!acquired && attempts <= retryCount) {
|
|
374
|
+
acquired = await this.tryAcquire(fullKey, value, ttl);
|
|
375
|
+
|
|
376
|
+
if (!acquired && attempts < retryCount) {
|
|
377
|
+
await this.sleep(retryDelay);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
attempts++;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const acquiredAt = Date.now();
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
acquired,
|
|
387
|
+
key: fullKey,
|
|
388
|
+
value,
|
|
389
|
+
release: async () => {
|
|
390
|
+
if (!acquired) return false;
|
|
391
|
+
return this.driver.release(fullKey, value);
|
|
392
|
+
},
|
|
393
|
+
extend: async (newTTL?: number) => {
|
|
394
|
+
if (!acquired) return false;
|
|
395
|
+
return this.driver.extend(fullKey, value, newTTL ?? ttl);
|
|
396
|
+
},
|
|
397
|
+
isValid: async () => {
|
|
398
|
+
if (!acquired) return false;
|
|
399
|
+
return this.driver.isValid(fullKey, value);
|
|
400
|
+
},
|
|
401
|
+
getRemainingTTL: async () => {
|
|
402
|
+
if (!acquired) return -1;
|
|
403
|
+
return this.driver.getTTL(fullKey, value);
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Acquire a lock and execute a function
|
|
410
|
+
* Automatically releases the lock when done
|
|
411
|
+
*/
|
|
412
|
+
async withLock<T>(
|
|
413
|
+
key: string,
|
|
414
|
+
fn: (lock: LockHandle) => Promise<T>,
|
|
415
|
+
options: LockOptions = {},
|
|
416
|
+
): Promise<T> {
|
|
417
|
+
const lock = await this.acquire(key, options);
|
|
418
|
+
|
|
419
|
+
if (!lock.acquired) {
|
|
420
|
+
throw new LockAcquireError(`Failed to acquire lock: ${key}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
return await fn(lock);
|
|
425
|
+
} finally {
|
|
426
|
+
await lock.release();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Acquire a lock with automatic extension for long-running operations
|
|
432
|
+
*/
|
|
433
|
+
async withAutoExtend<T>(
|
|
434
|
+
key: string,
|
|
435
|
+
fn: (lock: LockHandle) => Promise<T>,
|
|
436
|
+
options: LockOptions = {},
|
|
437
|
+
): Promise<T> {
|
|
438
|
+
const lock = await this.acquire(key, options);
|
|
439
|
+
|
|
440
|
+
if (!lock.acquired) {
|
|
441
|
+
throw new LockAcquireError(`Failed to acquire lock: ${key}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const ttl = options.ttl ?? this.defaultTTL;
|
|
445
|
+
const extendInterval = ttl * 0.7; // Extend at 70% of TTL
|
|
446
|
+
|
|
447
|
+
let extendTimer: ReturnType<typeof setInterval> | null = null;
|
|
448
|
+
let completed = false;
|
|
449
|
+
|
|
450
|
+
// Setup auto-extend timer
|
|
451
|
+
extendTimer = setInterval(async () => {
|
|
452
|
+
if (!completed) {
|
|
453
|
+
const remaining = await lock.getRemainingTTL();
|
|
454
|
+
if (remaining > 0 && remaining < extendInterval) {
|
|
455
|
+
await lock.extend(ttl);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}, extendInterval);
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
const result = await fn(lock);
|
|
462
|
+
completed = true;
|
|
463
|
+
return result;
|
|
464
|
+
} finally {
|
|
465
|
+
if (extendTimer) {
|
|
466
|
+
clearInterval(extendTimer);
|
|
467
|
+
}
|
|
468
|
+
await lock.release();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Try to acquire a lock without waiting
|
|
474
|
+
* Returns immediately whether the lock was acquired
|
|
475
|
+
*/
|
|
476
|
+
async tryLock(key: string, options: LockOptions = {}): Promise<LockHandle> {
|
|
477
|
+
return this.acquire(key, { ...options, retryCount: 0 });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Check if a lock exists (anyone holds it)
|
|
482
|
+
*/
|
|
483
|
+
async isLocked(key: string): Promise<boolean> {
|
|
484
|
+
const fullKey = this.keyPrefix + key;
|
|
485
|
+
const value = this.generateLockValue();
|
|
486
|
+
|
|
487
|
+
// Try to acquire - if successful, it wasn't locked
|
|
488
|
+
const acquired = await this.driver.acquire(fullKey, value, 1);
|
|
489
|
+
|
|
490
|
+
if (acquired) {
|
|
491
|
+
// Release immediately since we just wanted to check
|
|
492
|
+
await this.driver.release(fullKey, value);
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Force release a lock (dangerous - use with caution)
|
|
501
|
+
* This will release the lock regardless of ownership
|
|
502
|
+
*/
|
|
503
|
+
async forceRelease(key: string): Promise<void> {
|
|
504
|
+
const fullKey = this.keyPrefix + key;
|
|
505
|
+
const value = this.generateLockValue();
|
|
506
|
+
await this.driver.release(fullKey, value);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private sleep(ms: number): Promise<void> {
|
|
510
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ============= Error Classes =============
|
|
515
|
+
|
|
516
|
+
export class LockAcquireError extends Error {
|
|
517
|
+
constructor(message: string) {
|
|
518
|
+
super(message);
|
|
519
|
+
this.name = "LockAcquireError";
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export class LockTimeoutError extends Error {
|
|
524
|
+
constructor(message: string) {
|
|
525
|
+
super(message);
|
|
526
|
+
this.name = "LockTimeoutError";
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ============= Factory Functions =============
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Create a distributed lock instance
|
|
534
|
+
*/
|
|
535
|
+
export function createDistributedLock(config?: LockConfig): DistributedLock {
|
|
536
|
+
return new DistributedLock(config);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Create a Redis-based distributed lock
|
|
541
|
+
*/
|
|
542
|
+
export function createRedisLock(
|
|
543
|
+
url: string,
|
|
544
|
+
options?: Omit<LockConfig, "driver" | "url">,
|
|
545
|
+
): DistributedLock {
|
|
546
|
+
return new DistributedLock({ driver: "redis", url, ...options });
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Create an in-memory lock (for development/testing)
|
|
551
|
+
*/
|
|
552
|
+
export function createMemoryLock(): DistributedLock {
|
|
553
|
+
return new DistributedLock({ driver: "memory" });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ============= Convenience Exports =============
|
|
557
|
+
|
|
558
|
+
// Default lock instance (in-memory for convenience)
|
|
559
|
+
let defaultLock: DistributedLock | null = null;
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Get the default lock instance
|
|
563
|
+
*/
|
|
564
|
+
export function getDefaultLock(): DistributedLock {
|
|
565
|
+
if (!defaultLock) {
|
|
566
|
+
defaultLock = new DistributedLock({ driver: "memory" });
|
|
567
|
+
}
|
|
568
|
+
return defaultLock;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Set the default lock instance
|
|
573
|
+
*/
|
|
574
|
+
export function setDefaultLock(lock: DistributedLock): void {
|
|
575
|
+
defaultLock = lock;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Acquire a lock using the default instance
|
|
580
|
+
*/
|
|
581
|
+
export async function lock<T>(
|
|
582
|
+
key: string,
|
|
583
|
+
fn: () => Promise<T>,
|
|
584
|
+
options?: LockOptions,
|
|
585
|
+
): Promise<T> {
|
|
586
|
+
return getDefaultLock().withLock(key, async () => fn(), options);
|
|
587
|
+
}
|