dflockd-client 1.8.0 → 1.8.2

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/dist/client.d.cts CHANGED
@@ -118,16 +118,24 @@ declare class DistributedLock {
118
118
  private closed;
119
119
  constructor(opts: DistributedLockOptions);
120
120
  private pickServer;
121
- /** Acquire the lock. Returns `true` on success, `false` on timeout. */
122
- acquire(): Promise<boolean>;
121
+ /**
122
+ * Acquire the lock. Returns `true` on success, `false` on timeout.
123
+ * @param opts.force - If `true`, silently close any existing connection before acquiring. Defaults to `false`, which throws if already connected.
124
+ */
125
+ acquire(opts?: {
126
+ force?: boolean;
127
+ }): Promise<boolean>;
123
128
  /** Release the lock and close the connection. */
124
129
  release(): Promise<void>;
125
130
  /**
126
131
  * Two-phase step 1: connect and join the FIFO queue.
127
132
  * Returns `"acquired"` (fast-path, lock is already held) or `"queued"`.
128
133
  * If acquired immediately, the renew loop starts automatically.
134
+ * @param opts.force - If `true`, silently close any existing connection before enqueuing. Defaults to `false`, which throws if already connected.
129
135
  */
130
- enqueue(): Promise<"acquired" | "queued">;
136
+ enqueue(opts?: {
137
+ force?: boolean;
138
+ }): Promise<"acquired" | "queued">;
131
139
  /**
132
140
  * Two-phase step 2: block until the lock is granted.
133
141
  * Returns `true` if granted, `false` on timeout.
@@ -146,7 +154,7 @@ declare class DistributedLock {
146
154
  */
147
155
  withLock<T>(fn: () => T | Promise<T>): Promise<T>;
148
156
  /** Close the underlying socket (idempotent). */
149
- close(): Promise<void>;
157
+ close(): void;
150
158
  private startRenew;
151
159
  private stopRenew;
152
160
  }
@@ -184,16 +192,24 @@ declare class DistributedSemaphore {
184
192
  private closed;
185
193
  constructor(opts: DistributedSemaphoreOptions);
186
194
  private pickServer;
187
- /** Acquire a semaphore slot. Returns `true` on success, `false` on timeout. */
188
- acquire(): Promise<boolean>;
195
+ /**
196
+ * Acquire a semaphore slot. Returns `true` on success, `false` on timeout.
197
+ * @param opts.force - If `true`, silently close any existing connection before acquiring. Defaults to `false`, which throws if already connected.
198
+ */
199
+ acquire(opts?: {
200
+ force?: boolean;
201
+ }): Promise<boolean>;
189
202
  /** Release the semaphore slot and close the connection. */
190
203
  release(): Promise<void>;
191
204
  /**
192
205
  * Two-phase step 1: connect and join the FIFO queue.
193
206
  * Returns `"acquired"` (fast-path, slot granted immediately) or `"queued"`.
194
207
  * If acquired immediately, the renew loop starts automatically.
208
+ * @param opts.force - If `true`, silently close any existing connection before enqueuing. Defaults to `false`, which throws if already connected.
195
209
  */
196
- enqueue(): Promise<"acquired" | "queued">;
210
+ enqueue(opts?: {
211
+ force?: boolean;
212
+ }): Promise<"acquired" | "queued">;
197
213
  /**
198
214
  * Two-phase step 2: block until a semaphore slot is granted.
199
215
  * Returns `true` if granted, `false` on timeout.
@@ -212,7 +228,7 @@ declare class DistributedSemaphore {
212
228
  */
213
229
  withLock<T>(fn: () => T | Promise<T>): Promise<T>;
214
230
  /** Close the underlying socket (idempotent). */
215
- close(): Promise<void>;
231
+ close(): void;
216
232
  private startRenew;
217
233
  private stopRenew;
218
234
  }
package/dist/client.d.ts CHANGED
@@ -118,16 +118,24 @@ declare class DistributedLock {
118
118
  private closed;
119
119
  constructor(opts: DistributedLockOptions);
120
120
  private pickServer;
121
- /** Acquire the lock. Returns `true` on success, `false` on timeout. */
122
- acquire(): Promise<boolean>;
121
+ /**
122
+ * Acquire the lock. Returns `true` on success, `false` on timeout.
123
+ * @param opts.force - If `true`, silently close any existing connection before acquiring. Defaults to `false`, which throws if already connected.
124
+ */
125
+ acquire(opts?: {
126
+ force?: boolean;
127
+ }): Promise<boolean>;
123
128
  /** Release the lock and close the connection. */
124
129
  release(): Promise<void>;
125
130
  /**
126
131
  * Two-phase step 1: connect and join the FIFO queue.
127
132
  * Returns `"acquired"` (fast-path, lock is already held) or `"queued"`.
128
133
  * If acquired immediately, the renew loop starts automatically.
134
+ * @param opts.force - If `true`, silently close any existing connection before enqueuing. Defaults to `false`, which throws if already connected.
129
135
  */
130
- enqueue(): Promise<"acquired" | "queued">;
136
+ enqueue(opts?: {
137
+ force?: boolean;
138
+ }): Promise<"acquired" | "queued">;
131
139
  /**
132
140
  * Two-phase step 2: block until the lock is granted.
133
141
  * Returns `true` if granted, `false` on timeout.
@@ -146,7 +154,7 @@ declare class DistributedLock {
146
154
  */
147
155
  withLock<T>(fn: () => T | Promise<T>): Promise<T>;
148
156
  /** Close the underlying socket (idempotent). */
149
- close(): Promise<void>;
157
+ close(): void;
150
158
  private startRenew;
151
159
  private stopRenew;
152
160
  }
@@ -184,16 +192,24 @@ declare class DistributedSemaphore {
184
192
  private closed;
185
193
  constructor(opts: DistributedSemaphoreOptions);
186
194
  private pickServer;
187
- /** Acquire a semaphore slot. Returns `true` on success, `false` on timeout. */
188
- acquire(): Promise<boolean>;
195
+ /**
196
+ * Acquire a semaphore slot. Returns `true` on success, `false` on timeout.
197
+ * @param opts.force - If `true`, silently close any existing connection before acquiring. Defaults to `false`, which throws if already connected.
198
+ */
199
+ acquire(opts?: {
200
+ force?: boolean;
201
+ }): Promise<boolean>;
189
202
  /** Release the semaphore slot and close the connection. */
190
203
  release(): Promise<void>;
191
204
  /**
192
205
  * Two-phase step 1: connect and join the FIFO queue.
193
206
  * Returns `"acquired"` (fast-path, slot granted immediately) or `"queued"`.
194
207
  * If acquired immediately, the renew loop starts automatically.
208
+ * @param opts.force - If `true`, silently close any existing connection before enqueuing. Defaults to `false`, which throws if already connected.
195
209
  */
196
- enqueue(): Promise<"acquired" | "queued">;
210
+ enqueue(opts?: {
211
+ force?: boolean;
212
+ }): Promise<"acquired" | "queued">;
197
213
  /**
198
214
  * Two-phase step 2: block until a semaphore slot is granted.
199
215
  * Returns `true` if granted, `false` on timeout.
@@ -212,7 +228,7 @@ declare class DistributedSemaphore {
212
228
  */
213
229
  withLock<T>(fn: () => T | Promise<T>): Promise<T>;
214
230
  /** Close the underlying socket (idempotent). */
215
- close(): Promise<void>;
231
+ close(): void;
216
232
  private startRenew;
217
233
  private stopRenew;
218
234
  }
package/dist/client.js CHANGED
@@ -24,6 +24,14 @@ var AuthError = class extends LockError {
24
24
  function encodeLines(...lines) {
25
25
  return Buffer.from(lines.map((l) => l + "\n").join(""), "utf-8");
26
26
  }
27
+ function writeAll(sock, data) {
28
+ return new Promise((resolve, reject) => {
29
+ sock.write(data, (err) => {
30
+ if (err) reject(err);
31
+ else resolve();
32
+ });
33
+ });
34
+ }
27
35
  var _readlineBuf = /* @__PURE__ */ new WeakMap();
28
36
  function readline(sock) {
29
37
  return new Promise((resolve, reject) => {
@@ -47,10 +55,12 @@ function readline(sock) {
47
55
  };
48
56
  const onError = (err) => {
49
57
  cleanup();
58
+ _readlineBuf.delete(sock);
50
59
  reject(err);
51
60
  };
52
61
  const onClose = () => {
53
62
  cleanup();
63
+ _readlineBuf.delete(sock);
54
64
  reject(new LockError("server closed connection"));
55
65
  };
56
66
  const cleanup = () => {
@@ -79,10 +89,11 @@ async function connect2(host, port, tlsOptions, auth) {
79
89
  s.on("error", reject);
80
90
  }
81
91
  });
92
+ sock.setNoDelay(true);
82
93
  sock.on("error", () => {
83
94
  });
84
95
  if (auth != null && auth !== "") {
85
- sock.write(encodeLines("auth", "_", auth));
96
+ await writeAll(sock, encodeLines("auth", "_", auth));
86
97
  const resp = await readline(sock);
87
98
  if (resp === "ok") {
88
99
  return sock;
@@ -115,7 +126,7 @@ function stableHashShard(key, numServers) {
115
126
  }
116
127
  async function acquire(sock, key, acquireTimeoutS, leaseTtlS) {
117
128
  const arg = leaseTtlS == null ? String(acquireTimeoutS) : `${acquireTimeoutS} ${leaseTtlS}`;
118
- sock.write(encodeLines("l", key, arg));
129
+ await writeAll(sock, encodeLines("l", key, arg));
119
130
  const resp = await readline(sock);
120
131
  if (resp === "timeout") {
121
132
  throw new AcquireTimeoutError(key);
@@ -124,18 +135,15 @@ async function acquire(sock, key, acquireTimeoutS, leaseTtlS) {
124
135
  throw new LockError(`acquire failed: '${resp}'`);
125
136
  }
126
137
  const parts = resp.split(" ");
127
- if (parts.length < 2) {
128
- throw new LockError(`bad ok response: '${resp}'`);
129
- }
130
138
  const token = parts[1];
131
139
  const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 30;
132
140
  return { token, lease };
133
141
  }
134
142
  async function renew(sock, key, token, leaseTtlS) {
135
143
  const arg = leaseTtlS == null ? token : `${token} ${leaseTtlS}`;
136
- sock.write(encodeLines("n", key, arg));
144
+ await writeAll(sock, encodeLines("n", key, arg));
137
145
  const resp = await readline(sock);
138
- if (!resp.startsWith("ok")) {
146
+ if (resp !== "ok" && !resp.startsWith("ok ")) {
139
147
  throw new LockError(`renew failed: '${resp}'`);
140
148
  }
141
149
  const parts = resp.split(" ");
@@ -146,12 +154,12 @@ async function renew(sock, key, token, leaseTtlS) {
146
154
  }
147
155
  async function enqueue(sock, key, leaseTtlS) {
148
156
  const arg = leaseTtlS == null ? "" : String(leaseTtlS);
149
- sock.write(encodeLines("e", key, arg));
157
+ await writeAll(sock, encodeLines("e", key, arg));
150
158
  const resp = await readline(sock);
151
159
  if (resp.startsWith("acquired ")) {
152
160
  const parts = resp.split(" ");
153
161
  const token = parts[1];
154
- const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 33;
162
+ const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 30;
155
163
  return { status: "acquired", token, lease };
156
164
  }
157
165
  if (resp === "queued") {
@@ -160,7 +168,7 @@ async function enqueue(sock, key, leaseTtlS) {
160
168
  throw new LockError(`enqueue failed: '${resp}'`);
161
169
  }
162
170
  async function waitForLock(sock, key, waitTimeoutS) {
163
- sock.write(encodeLines("w", key, String(waitTimeoutS)));
171
+ await writeAll(sock, encodeLines("w", key, String(waitTimeoutS)));
164
172
  const resp = await readline(sock);
165
173
  if (resp === "timeout") {
166
174
  throw new AcquireTimeoutError(key);
@@ -170,11 +178,11 @@ async function waitForLock(sock, key, waitTimeoutS) {
170
178
  }
171
179
  const parts = resp.split(" ");
172
180
  const token = parts[1];
173
- const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 33;
181
+ const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 30;
174
182
  return { token, lease };
175
183
  }
176
184
  async function release(sock, key, token) {
177
- sock.write(encodeLines("r", key, token));
185
+ await writeAll(sock, encodeLines("r", key, token));
178
186
  const resp = await readline(sock);
179
187
  if (resp !== "ok") {
180
188
  throw new LockError(`release failed: '${resp}'`);
@@ -182,7 +190,7 @@ async function release(sock, key, token) {
182
190
  }
183
191
  async function semAcquire(sock, key, acquireTimeoutS, limit, leaseTtlS) {
184
192
  const arg = leaseTtlS == null ? `${acquireTimeoutS} ${limit}` : `${acquireTimeoutS} ${limit} ${leaseTtlS}`;
185
- sock.write(encodeLines("sl", key, arg));
193
+ await writeAll(sock, encodeLines("sl", key, arg));
186
194
  const resp = await readline(sock);
187
195
  if (resp === "timeout") {
188
196
  throw new AcquireTimeoutError(key);
@@ -191,18 +199,15 @@ async function semAcquire(sock, key, acquireTimeoutS, limit, leaseTtlS) {
191
199
  throw new LockError(`sem_acquire failed: '${resp}'`);
192
200
  }
193
201
  const parts = resp.split(" ");
194
- if (parts.length < 2) {
195
- throw new LockError(`bad ok response: '${resp}'`);
196
- }
197
202
  const token = parts[1];
198
203
  const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 30;
199
204
  return { token, lease };
200
205
  }
201
206
  async function semRenew(sock, key, token, leaseTtlS) {
202
207
  const arg = leaseTtlS == null ? token : `${token} ${leaseTtlS}`;
203
- sock.write(encodeLines("sn", key, arg));
208
+ await writeAll(sock, encodeLines("sn", key, arg));
204
209
  const resp = await readline(sock);
205
- if (!resp.startsWith("ok")) {
210
+ if (resp !== "ok" && !resp.startsWith("ok ")) {
206
211
  throw new LockError(`sem_renew failed: '${resp}'`);
207
212
  }
208
213
  const parts = resp.split(" ");
@@ -213,7 +218,7 @@ async function semRenew(sock, key, token, leaseTtlS) {
213
218
  }
214
219
  async function semEnqueue(sock, key, limit, leaseTtlS) {
215
220
  const arg = leaseTtlS == null ? String(limit) : `${limit} ${leaseTtlS}`;
216
- sock.write(encodeLines("se", key, arg));
221
+ await writeAll(sock, encodeLines("se", key, arg));
217
222
  const resp = await readline(sock);
218
223
  if (resp.startsWith("acquired ")) {
219
224
  const parts = resp.split(" ");
@@ -227,7 +232,7 @@ async function semEnqueue(sock, key, limit, leaseTtlS) {
227
232
  throw new LockError(`sem_enqueue failed: '${resp}'`);
228
233
  }
229
234
  async function semWaitForLock(sock, key, waitTimeoutS) {
230
- sock.write(encodeLines("sw", key, String(waitTimeoutS)));
235
+ await writeAll(sock, encodeLines("sw", key, String(waitTimeoutS)));
231
236
  const resp = await readline(sock);
232
237
  if (resp === "timeout") {
233
238
  throw new AcquireTimeoutError(key);
@@ -241,20 +246,24 @@ async function semWaitForLock(sock, key, waitTimeoutS) {
241
246
  return { token, lease };
242
247
  }
243
248
  async function semRelease(sock, key, token) {
244
- sock.write(encodeLines("sr", key, token));
249
+ await writeAll(sock, encodeLines("sr", key, token));
245
250
  const resp = await readline(sock);
246
251
  if (resp !== "ok") {
247
252
  throw new LockError(`sem_release failed: '${resp}'`);
248
253
  }
249
254
  }
250
255
  async function statsProto(sock) {
251
- sock.write(encodeLines("stats", "_", ""));
256
+ await writeAll(sock, encodeLines("stats", "_", ""));
252
257
  const resp = await readline(sock);
253
258
  if (!resp.startsWith("ok ")) {
254
259
  throw new LockError(`stats failed: '${resp}'`);
255
260
  }
256
261
  const json = resp.slice(3);
257
- return JSON.parse(json);
262
+ try {
263
+ return JSON.parse(json);
264
+ } catch {
265
+ throw new LockError(`stats: malformed JSON response: '${json}'`);
266
+ }
258
267
  }
259
268
  async function stats(options) {
260
269
  const host = options?.host ?? DEFAULT_HOST;
@@ -292,7 +301,7 @@ var DistributedLock = class {
292
301
  if (opts.servers.length === 0) {
293
302
  throw new LockError("servers list must not be empty");
294
303
  }
295
- this.servers = opts.servers;
304
+ this.servers = [...opts.servers];
296
305
  } else {
297
306
  this.servers = [[opts.host ?? DEFAULT_HOST, opts.port ?? DEFAULT_PORT]];
298
307
  }
@@ -308,9 +317,17 @@ var DistributedLock = class {
308
317
  }
309
318
  return this.servers[idx];
310
319
  }
311
- /** Acquire the lock. Returns `true` on success, `false` on timeout. */
312
- async acquire() {
313
- await this.close();
320
+ /**
321
+ * Acquire the lock. Returns `true` on success, `false` on timeout.
322
+ * @param opts.force - If `true`, silently close any existing connection before acquiring. Defaults to `false`, which throws if already connected.
323
+ */
324
+ async acquire(opts) {
325
+ if (this.sock && !this.closed) {
326
+ if (!opts?.force) {
327
+ throw new LockError("already connected; call release() or close() first, or pass { force: true }");
328
+ }
329
+ this.close();
330
+ }
314
331
  this.closed = false;
315
332
  const [host, port] = this.pickServer();
316
333
  this.sock = await connect2(host, port, this.tls, this.auth);
@@ -324,7 +341,7 @@ var DistributedLock = class {
324
341
  this.token = result.token;
325
342
  this.lease = result.lease;
326
343
  } catch (err) {
327
- await this.close();
344
+ this.close();
328
345
  if (err instanceof AcquireTimeoutError) return false;
329
346
  throw err;
330
347
  }
@@ -339,16 +356,22 @@ var DistributedLock = class {
339
356
  await release(this.sock, this.key, this.token);
340
357
  }
341
358
  } finally {
342
- await this.close();
359
+ this.close();
343
360
  }
344
361
  }
345
362
  /**
346
363
  * Two-phase step 1: connect and join the FIFO queue.
347
364
  * Returns `"acquired"` (fast-path, lock is already held) or `"queued"`.
348
365
  * If acquired immediately, the renew loop starts automatically.
366
+ * @param opts.force - If `true`, silently close any existing connection before enqueuing. Defaults to `false`, which throws if already connected.
349
367
  */
350
- async enqueue() {
351
- await this.close();
368
+ async enqueue(opts) {
369
+ if (this.sock && !this.closed) {
370
+ if (!opts?.force) {
371
+ throw new LockError("already connected; call release() or close() first, or pass { force: true }");
372
+ }
373
+ this.close();
374
+ }
352
375
  this.closed = false;
353
376
  const [host, port] = this.pickServer();
354
377
  this.sock = await connect2(host, port, this.tls, this.auth);
@@ -361,7 +384,7 @@ var DistributedLock = class {
361
384
  }
362
385
  return result.status;
363
386
  } catch (err) {
364
- await this.close();
387
+ this.close();
365
388
  throw err;
366
389
  }
367
390
  }
@@ -383,7 +406,7 @@ var DistributedLock = class {
383
406
  this.token = result.token;
384
407
  this.lease = result.lease;
385
408
  } catch (err) {
386
- await this.close();
409
+ this.close();
387
410
  if (err instanceof AcquireTimeoutError) return false;
388
411
  throw err;
389
412
  }
@@ -412,7 +435,7 @@ var DistributedLock = class {
412
435
  }
413
436
  }
414
437
  /** Close the underlying socket (idempotent). */
415
- async close() {
438
+ close() {
416
439
  if (this.closed) return;
417
440
  this.closed = true;
418
441
  this.stopRenew();
@@ -424,21 +447,26 @@ var DistributedLock = class {
424
447
  }
425
448
  // -- internals --
426
449
  startRenew() {
427
- const interval = Math.max(1, this.lease * this.renewRatio) * 1e3;
428
450
  const loop = async () => {
429
- if (!this.sock || !this.token) return;
451
+ const savedToken = this.token;
452
+ if (!this.sock || !savedToken) return;
453
+ const start = Date.now();
430
454
  try {
431
- await renew(this.sock, this.key, this.token, this.leaseTtlS);
455
+ this.lease = await renew(this.sock, this.key, savedToken, this.leaseTtlS);
432
456
  } catch {
433
- const lostToken = this.token;
434
- this.token = null;
435
- if (this.onLockLost) {
436
- this.onLockLost(this.key, lostToken);
457
+ if (this.token === savedToken) {
458
+ this.token = null;
459
+ if (this.onLockLost) {
460
+ this.onLockLost(this.key, savedToken);
461
+ }
437
462
  }
438
463
  return;
439
464
  }
440
- this.renewTimer = setTimeout(loop, interval);
465
+ const elapsed = Date.now() - start;
466
+ const interval2 = Math.max(1, this.lease * this.renewRatio) * 1e3;
467
+ this.renewTimer = setTimeout(loop, Math.max(0, interval2 - elapsed));
441
468
  };
469
+ const interval = Math.max(1, this.lease * this.renewRatio) * 1e3;
442
470
  this.renewTimer = setTimeout(loop, interval);
443
471
  }
444
472
  stopRenew() {
@@ -476,7 +504,7 @@ var DistributedSemaphore = class {
476
504
  if (opts.servers.length === 0) {
477
505
  throw new LockError("servers list must not be empty");
478
506
  }
479
- this.servers = opts.servers;
507
+ this.servers = [...opts.servers];
480
508
  } else {
481
509
  this.servers = [[opts.host ?? DEFAULT_HOST, opts.port ?? DEFAULT_PORT]];
482
510
  }
@@ -492,9 +520,17 @@ var DistributedSemaphore = class {
492
520
  }
493
521
  return this.servers[idx];
494
522
  }
495
- /** Acquire a semaphore slot. Returns `true` on success, `false` on timeout. */
496
- async acquire() {
497
- await this.close();
523
+ /**
524
+ * Acquire a semaphore slot. Returns `true` on success, `false` on timeout.
525
+ * @param opts.force - If `true`, silently close any existing connection before acquiring. Defaults to `false`, which throws if already connected.
526
+ */
527
+ async acquire(opts) {
528
+ if (this.sock && !this.closed) {
529
+ if (!opts?.force) {
530
+ throw new LockError("already connected; call release() or close() first, or pass { force: true }");
531
+ }
532
+ this.close();
533
+ }
498
534
  this.closed = false;
499
535
  const [host, port] = this.pickServer();
500
536
  this.sock = await connect2(host, port, this.tls, this.auth);
@@ -509,7 +545,7 @@ var DistributedSemaphore = class {
509
545
  this.token = result.token;
510
546
  this.lease = result.lease;
511
547
  } catch (err) {
512
- await this.close();
548
+ this.close();
513
549
  if (err instanceof AcquireTimeoutError) return false;
514
550
  throw err;
515
551
  }
@@ -524,16 +560,22 @@ var DistributedSemaphore = class {
524
560
  await semRelease(this.sock, this.key, this.token);
525
561
  }
526
562
  } finally {
527
- await this.close();
563
+ this.close();
528
564
  }
529
565
  }
530
566
  /**
531
567
  * Two-phase step 1: connect and join the FIFO queue.
532
568
  * Returns `"acquired"` (fast-path, slot granted immediately) or `"queued"`.
533
569
  * If acquired immediately, the renew loop starts automatically.
570
+ * @param opts.force - If `true`, silently close any existing connection before enqueuing. Defaults to `false`, which throws if already connected.
534
571
  */
535
- async enqueue() {
536
- await this.close();
572
+ async enqueue(opts) {
573
+ if (this.sock && !this.closed) {
574
+ if (!opts?.force) {
575
+ throw new LockError("already connected; call release() or close() first, or pass { force: true }");
576
+ }
577
+ this.close();
578
+ }
537
579
  this.closed = false;
538
580
  const [host, port] = this.pickServer();
539
581
  this.sock = await connect2(host, port, this.tls, this.auth);
@@ -546,7 +588,7 @@ var DistributedSemaphore = class {
546
588
  }
547
589
  return result.status;
548
590
  } catch (err) {
549
- await this.close();
591
+ this.close();
550
592
  throw err;
551
593
  }
552
594
  }
@@ -568,7 +610,7 @@ var DistributedSemaphore = class {
568
610
  this.token = result.token;
569
611
  this.lease = result.lease;
570
612
  } catch (err) {
571
- await this.close();
613
+ this.close();
572
614
  if (err instanceof AcquireTimeoutError) return false;
573
615
  throw err;
574
616
  }
@@ -597,7 +639,7 @@ var DistributedSemaphore = class {
597
639
  }
598
640
  }
599
641
  /** Close the underlying socket (idempotent). */
600
- async close() {
642
+ close() {
601
643
  if (this.closed) return;
602
644
  this.closed = true;
603
645
  this.stopRenew();
@@ -609,21 +651,26 @@ var DistributedSemaphore = class {
609
651
  }
610
652
  // -- internals --
611
653
  startRenew() {
612
- const interval = Math.max(1, this.lease * this.renewRatio) * 1e3;
613
654
  const loop = async () => {
614
- if (!this.sock || !this.token) return;
655
+ const savedToken = this.token;
656
+ if (!this.sock || !savedToken) return;
657
+ const start = Date.now();
615
658
  try {
616
- await semRenew(this.sock, this.key, this.token, this.leaseTtlS);
659
+ this.lease = await semRenew(this.sock, this.key, savedToken, this.leaseTtlS);
617
660
  } catch {
618
- const lostToken = this.token;
619
- this.token = null;
620
- if (this.onLockLost) {
621
- this.onLockLost(this.key, lostToken);
661
+ if (this.token === savedToken) {
662
+ this.token = null;
663
+ if (this.onLockLost) {
664
+ this.onLockLost(this.key, savedToken);
665
+ }
622
666
  }
623
667
  return;
624
668
  }
625
- this.renewTimer = setTimeout(loop, interval);
669
+ const elapsed = Date.now() - start;
670
+ const interval2 = Math.max(1, this.lease * this.renewRatio) * 1e3;
671
+ this.renewTimer = setTimeout(loop, Math.max(0, interval2 - elapsed));
626
672
  };
673
+ const interval = Math.max(1, this.lease * this.renewRatio) * 1e3;
627
674
  this.renewTimer = setTimeout(loop, interval);
628
675
  }
629
676
  stopRenew() {