wendkeep 0.62.0 → 0.63.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.
@@ -12,6 +12,12 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
12
12
 
13
13
  export const VAULT_LOCK_BUSY = Symbol('wendkeep:vault-lock-busy');
14
14
  export const VAULT_LOCK_OWNER_FILE = '.owner.json';
15
+ const VAULT_PATH_FAILURE = Symbol('wendkeep:vault-path-failure');
16
+ const VAULT_PATH_CREATED = Symbol('wendkeep:vault-path-created');
17
+ const VAULT_LOCK_RETRY_DEADLINE = Symbol('wendkeep:vault-lock-retry-deadline');
18
+ const VAULT_LOCK_TOPOLOGY_RETRIES = 3;
19
+ const VAULT_LOCK_TOPOLOGY_RETRY_MS = 10;
20
+ const VAULT_LOCK_RELEASE_RETRY_WINDOW_MS = 50;
15
21
 
16
22
  function pathKey(value) {
17
23
  const normalized = resolve(value).replaceAll('\\', '/').replace(/^\\\\\?\//, '');
@@ -106,6 +112,10 @@ export function assertVaultPathSafe(vaultBase, targetPath, {
106
112
  } catch (cause) {
107
113
  const error = unsafe(`${label} possui componente dangling ou irresolvível: ${logicalCursor}`, code);
108
114
  error.cause = cause;
115
+ error[VAULT_PATH_FAILURE] = {
116
+ kind: 'component-realpath',
117
+ component: logicalCursor,
118
+ };
109
119
  throw error;
110
120
  }
111
121
  const expectedPhysical = join(physicalRoot, ...segments.slice(0, index + 1));
@@ -118,8 +128,23 @@ export function assertVaultPathSafe(vaultBase, targetPath, {
118
128
  }
119
129
  }
120
130
 
121
- if (!exists && !allowMissing) throw unsafe(`${label} inexistente: ${target}`, code);
122
- if (exists && mustNotExist) throw unsafe(`${label} preexistente: ${target}`, code);
131
+ if (!exists && !allowMissing) {
132
+ const error = unsafe(`${label} inexistente: ${target}`, code);
133
+ error[VAULT_PATH_FAILURE] = {
134
+ kind: 'component-missing',
135
+ component: target,
136
+ causeCode: 'ENOENT',
137
+ };
138
+ throw error;
139
+ }
140
+ if (exists && mustNotExist) {
141
+ const error = unsafe(`${label} preexistente: ${target}`, code);
142
+ error[VAULT_PATH_FAILURE] = {
143
+ kind: 'target-preexisting',
144
+ component: target,
145
+ };
146
+ throw error;
147
+ }
123
148
  if (exists) {
124
149
  if (targetStat?.isFile() && targetStat.nlink > 1) {
125
150
  throw unsafe(`${label} preexistente possui hardlink (nlink=${targetStat.nlink}): ${target}`, code);
@@ -160,16 +185,25 @@ export function mkdirVaultPath(vaultBase, targetPath, {
160
185
  let checked = assertVaultPathSafe(vaultBase, targetPath, {
161
186
  expectedType: 'directory', label, code,
162
187
  });
163
- if (exclusive || !checked.exists) {
164
- // Deliberately adjacent to mkdir: this is the last userspace check before mutation.
165
- checked = assertVaultPathSafe(vaultBase, targetPath, {
166
- expectedType: 'directory', label, code,
167
- });
168
- mkdirSync(checked.target, { recursive: exclusive ? false : recursive });
188
+ let created = false;
189
+ try {
190
+ if (exclusive || !checked.exists) {
191
+ // Deliberately adjacent to mkdir: this is the last userspace check before mutation.
192
+ checked = assertVaultPathSafe(vaultBase, targetPath, {
193
+ expectedType: 'directory', label, code,
194
+ });
195
+ mkdirSync(checked.target, { recursive: exclusive ? false : recursive });
196
+ created = true;
197
+ }
198
+ return assertVaultPathSafe(vaultBase, targetPath, {
199
+ allowMissing: false, expectedType: 'directory', label, code,
200
+ }).target;
201
+ } catch (error) {
202
+ // Callers may safely clean only a directory whose mkdir syscall actually succeeded.
203
+ // In particular, EEXIST must never authorize cleanup of a pre-existing path.
204
+ if (created) error[VAULT_PATH_CREATED] = checked.target;
205
+ throw error;
169
206
  }
170
- return assertVaultPathSafe(vaultBase, targetPath, {
171
- allowMissing: false, expectedType: 'directory', label, code,
172
- }).target;
173
207
  }
174
208
 
175
209
  export function writeVaultFileSync(vaultBase, targetPath, content, encoding = 'utf8', {
@@ -301,25 +335,69 @@ function waitBriefly(ms) {
301
335
  Atomics.wait(signal, 0, 0, ms);
302
336
  }
303
337
 
304
- // A lock may legitimately disappear between lstat and realpath while another owner
305
- // releases/replaces that exact canonical directory. Retry only when the original
306
- // failure is the resulting ENOENT and a fresh walk still resolves to either the
307
- // canonical directory or a missing suffix. Junctions/reparse points and every other
308
- // unsafe topology keep failing closed.
309
- function retryableLockTopologyRace(vaultBase, lock, error, code) {
310
- if (error?.cause?.code !== 'ENOENT'
311
- || (error?.code !== code && error?.code !== 'VAULT_PATH_UNSAFE')) return false;
312
- for (let attempt = 0; attempt < 3; attempt += 1) {
338
+ // A public lock may legitimately disappear while another owner releases it. Only ENOENT
339
+ // observed by an operation explicitly scoped to that canonical lock receives bounded
340
+ // backoff; private .pending paths and every unsafe topology fail closed.
341
+ function retryablePublicLockError(lock, error, code, { allowRaw = true } = {}) {
342
+ const failure = error?.[VAULT_PATH_FAILURE];
343
+ if (!failure) return allowRaw && error?.code === 'ENOENT' && !error?.cause;
344
+ const causeCode = error?.cause?.code || failure.causeCode;
345
+ return causeCode === 'ENOENT'
346
+ && (error?.code === code || error?.code === 'VAULT_PATH_UNSAFE')
347
+ && ['component-realpath', 'component-missing'].includes(failure.kind)
348
+ && containedBy(resolve(lock), resolve(failure.component));
349
+ }
350
+
351
+ function vaultLockRenameCollision(error, pending, lock) {
352
+ const acceptedCodes = process.platform === 'win32'
353
+ ? ['EEXIST', 'ENOTEMPTY', 'EPERM']
354
+ : ['EEXIST', 'ENOTEMPTY'];
355
+ return acceptedCodes.includes(error?.code)
356
+ && error?.syscall === 'rename'
357
+ && typeof error?.path === 'string'
358
+ && typeof error?.dest === 'string'
359
+ && pathKey(error.path) === pathKey(pending)
360
+ && pathKey(error.dest) === pathKey(lock);
361
+ }
362
+
363
+ function preexistingPublicLockError(lock, error, code) {
364
+ const failure = error?.[VAULT_PATH_FAILURE];
365
+ return failure?.kind === 'target-preexisting'
366
+ && (error?.code === code || error?.code === 'VAULT_PATH_UNSAFE')
367
+ && typeof failure.component === 'string'
368
+ && pathKey(failure.component) === pathKey(lock);
369
+ }
370
+
371
+ function vaultLockRetryDeadlineError() {
372
+ const error = new Error('deadline de retry do lock do Vault esgotado');
373
+ error[VAULT_LOCK_RETRY_DEADLINE] = true;
374
+ return error;
375
+ }
376
+
377
+ function withPublicLockRetry(lock, code, retryState, operation, initialError = null) {
378
+ let error = initialError;
379
+ while (true) {
380
+ if (error) {
381
+ if (!retryablePublicLockError(lock, error, code)
382
+ || retryState.remaining <= 0) throw error;
383
+ const remainingMs = retryState.deadline - Date.now();
384
+ if (remainingMs <= 0) throw vaultLockRetryDeadlineError();
385
+ retryState.remaining -= 1;
386
+ waitBriefly(Math.min(VAULT_LOCK_TOPOLOGY_RETRY_MS, remainingMs));
387
+ if (Date.now() >= retryState.deadline) throw vaultLockRetryDeadlineError();
388
+ }
313
389
  try {
314
- assertVaultPathSafe(vaultBase, lock, {
315
- expectedType: 'directory', label: 'lock de escrita do Vault', code,
316
- });
317
- return true;
318
- } catch (recheckError) {
319
- if (recheckError?.cause?.code !== 'ENOENT') return false;
390
+ return operation();
391
+ } catch (nextError) {
392
+ error = nextError;
320
393
  }
321
394
  }
322
- return false;
395
+ }
396
+
397
+ function inspectVaultLock(vaultBase, lock, code, retryState, initialError = null) {
398
+ return withPublicLockRetry(lock, code, retryState, () => assertVaultPathSafe(vaultBase, lock, {
399
+ expectedType: 'directory', label: 'lock de escrita do Vault', code,
400
+ }), initialError);
323
401
  }
324
402
 
325
403
  function processIsAlive(pid) {
@@ -333,23 +411,44 @@ function processIsAlive(pid) {
333
411
  }
334
412
  }
335
413
 
336
- function vaultLockOwner(vaultBase, lock, code) {
337
- const path = join(lock, VAULT_LOCK_OWNER_FILE);
338
- const checked = assertVaultPathSafe(vaultBase, path, {
339
- expectedType: 'file', label: 'owner do lock de escrita do Vault', code,
340
- });
341
- if (!checked.exists) return { owner: null, path: checked.target, raw: '' };
342
- const raw = readFileSync(checked.target, 'utf8');
343
- try {
344
- const owner = JSON.parse(raw);
345
- const valid = owner?.v === 1
346
- && Number.isSafeInteger(owner.pid) && owner.pid > 0
347
- && typeof owner.token === 'string' && owner.token.length > 0
348
- && typeof owner.created_at === 'string' && owner.created_at.length > 0;
349
- return { owner: valid ? owner : null, path: checked.target, raw };
350
- } catch {
351
- return { owner: null, path: checked.target, raw };
352
- }
414
+ function vaultLockOwner(vaultBase, lock, code, retryState = null) {
415
+ const inspect = () => {
416
+ const checkedLock = assertVaultPathSafe(vaultBase, lock, {
417
+ expectedType: 'directory', label: 'lock de escrita do Vault', code,
418
+ });
419
+ const path = join(lock, VAULT_LOCK_OWNER_FILE);
420
+ if (!checkedLock.exists) {
421
+ return {
422
+ lockExists: false, ownerExists: false, owner: null, path, raw: '',
423
+ };
424
+ }
425
+ const checked = assertVaultPathSafe(vaultBase, path, {
426
+ expectedType: 'file', label: 'owner do lock de escrita do Vault', code,
427
+ });
428
+ if (!checked.exists) {
429
+ return {
430
+ lockExists: true, ownerExists: false, owner: null, path: checked.target, raw: '',
431
+ };
432
+ }
433
+ const raw = readFileSync(checked.target, 'utf8');
434
+ try {
435
+ const owner = JSON.parse(raw);
436
+ const valid = owner?.v === 1
437
+ && Number.isSafeInteger(owner.pid) && owner.pid > 0
438
+ && typeof owner.token === 'string' && owner.token.length > 0
439
+ && typeof owner.created_at === 'string' && owner.created_at.length > 0;
440
+ return {
441
+ lockExists: true, ownerExists: true, owner: valid ? owner : null, path: checked.target, raw,
442
+ };
443
+ } catch {
444
+ return {
445
+ lockExists: true, ownerExists: true, owner: null, path: checked.target, raw,
446
+ };
447
+ }
448
+ };
449
+ return retryState
450
+ ? withPublicLockRetry(lock, code, retryState, inspect)
451
+ : inspect();
353
452
  }
354
453
 
355
454
  function vaultLockLease(lock, token) {
@@ -383,91 +482,138 @@ function removePreparedVaultLock(vaultBase, lock, token, code) {
383
482
  });
384
483
  }
385
484
 
386
- function releaseOwnedVaultLock(vaultBase, lock, { pid, token, code }) {
387
- const observed = vaultLockOwner(vaultBase, lock, code).owner;
485
+ function releaseOwnedVaultLock(vaultBase, lock, {
486
+ pid, token, code, retryState,
487
+ }) {
488
+ const observedState = vaultLockOwner(vaultBase, lock, code, retryState);
489
+ if (!observedState.lockExists) return true;
490
+ const observed = observedState.owner;
388
491
  if (observed?.pid !== pid || observed?.token !== token) return false;
389
492
  // The token-specific lease is the filesystem CAS. An old finally/reaper can only
390
493
  // remove the directory after successfully unlinking the lease it originally saw;
391
494
  // a replacement lock never contains that unguessable path.
392
- const leaseRemoved = unlinkVaultFile(vaultBase, vaultLockLease(lock, token), {
393
- label: 'lease do lock de escrita do Vault', code,
394
- });
495
+ const leaseRemoved = withPublicLockRetry(lock, code, retryState, () => unlinkVaultFile(
496
+ vaultBase, vaultLockLease(lock, token), {
497
+ label: 'lease do lock de escrita do Vault', code,
498
+ },
499
+ ));
395
500
  if (!leaseRemoved) return false;
396
- const current = vaultLockOwner(vaultBase, lock, code).owner;
501
+ const currentState = vaultLockOwner(vaultBase, lock, code, retryState);
502
+ if (!currentState.lockExists) return true;
503
+ const current = currentState.owner;
397
504
  if (current?.pid !== pid || current?.token !== token) return false;
398
505
  const ownerPath = join(lock, VAULT_LOCK_OWNER_FILE);
399
- if (!unlinkVaultFile(vaultBase, ownerPath, {
400
- label: 'owner do lock de escrita do Vault', code,
401
- })) return false;
402
- return removeVaultLockDirectory(vaultBase, lock, {
403
- missingOk: false, label: 'lock de escrita do Vault', code,
404
- });
506
+ if (!withPublicLockRetry(lock, code, retryState, () => unlinkVaultFile(
507
+ vaultBase, ownerPath, {
508
+ label: 'owner do lock de escrita do Vault', code,
509
+ },
510
+ ))) return false;
511
+ return withPublicLockRetry(lock, code, retryState, () => removeVaultLockDirectory(
512
+ vaultBase, lock, {
513
+ missingOk: false, label: 'lock de escrita do Vault', code,
514
+ },
515
+ ));
405
516
  }
406
517
 
407
- function reapDeadVaultLock(vaultBase, lock, staleMs, code) {
408
- const checked = assertVaultPathSafe(vaultBase, lock, {
409
- expectedType: 'directory', label: 'lock de escrita do Vault', code,
518
+ function reapDeadVaultLock(vaultBase, lock, staleMs, code, retryState) {
519
+ const initial = withPublicLockRetry(lock, code, retryState, () => {
520
+ const checked = assertVaultPathSafe(vaultBase, lock, {
521
+ expectedType: 'directory', label: 'lock de escrita do Vault', code,
522
+ });
523
+ if (!checked.exists) return null;
524
+ return { checked, before: statSync(checked.target) };
410
525
  });
411
- if (!checked.exists) return true;
412
- let before;
413
- try {
414
- before = statSync(checked.target);
415
- } catch (error) {
416
- if (error?.code === 'ENOENT') return true;
417
- throw error;
418
- }
526
+ if (!initial) return true;
527
+ const { checked, before } = initial;
419
528
  if (Date.now() - before.mtimeMs <= staleMs) return false;
420
- const observed = vaultLockOwner(vaultBase, checked.target, code);
529
+ const observed = vaultLockOwner(vaultBase, checked.target, code, retryState);
530
+ if (!observed.lockExists) return true;
421
531
  if (observed.owner) {
422
532
  if (processIsAlive(observed.owner.pid)) return false;
423
- const lease = assertVaultPathSafe(vaultBase, vaultLockLease(checked.target, observed.owner.token), {
424
- expectedType: 'file', label: 'lease do lock de escrita do Vault', code,
533
+ const lease = withPublicLockRetry(lock, code, retryState, () => {
534
+ const current = assertVaultPathSafe(vaultBase, lock, {
535
+ expectedType: 'directory', label: 'lock de escrita do Vault', code,
536
+ });
537
+ if (!current.exists) return null;
538
+ return assertVaultPathSafe(vaultBase, vaultLockLease(current.target, observed.owner.token), {
539
+ expectedType: 'file', label: 'lease do lock de escrita do Vault', code,
540
+ });
425
541
  });
542
+ if (!lease) return true;
426
543
  if (lease.exists) {
427
544
  return releaseOwnedVaultLock(vaultBase, checked.target, {
428
- pid: observed.owner.pid, token: observed.owner.token, code,
545
+ pid: observed.owner.pid, token: observed.owner.token, code, retryState,
429
546
  });
430
547
  }
431
548
  // Compatibility with owner-aware locks from 0.58.x, which predate token leases.
432
549
  // A dead PID plus byte-identical owner and directory identity is sufficient here;
433
550
  // a live legacy owner was returned above and is never reaped by age.
434
- const entries = readdirSync(checked.target);
551
+ const legacy = withPublicLockRetry(lock, code, retryState, () => {
552
+ const current = assertVaultPathSafe(vaultBase, lock, {
553
+ expectedType: 'directory', label: 'lock legado de escrita do Vault', code,
554
+ });
555
+ if (!current.exists) return null;
556
+ return {
557
+ entries: readdirSync(current.target),
558
+ currentStat: statSync(current.target),
559
+ currentOwner: vaultLockOwner(vaultBase, current.target, code),
560
+ };
561
+ });
562
+ if (!legacy) return true;
563
+ const { entries, currentStat, currentOwner } = legacy;
435
564
  if (entries.some((name) => name !== VAULT_LOCK_OWNER_FILE)) return false;
436
- const currentStat = statSync(checked.target);
437
- const currentOwner = vaultLockOwner(vaultBase, checked.target, code);
438
565
  if (currentStat.birthtimeMs !== before.birthtimeMs
439
566
  || currentStat.mtimeMs !== before.mtimeMs
440
567
  || currentOwner.raw !== observed.raw) return false;
441
- if (!unlinkVaultFile(vaultBase, currentOwner.path, {
442
- label: 'owner legado morto do lock de escrita do Vault', code,
443
- })) return false;
444
- return removeVaultLockDirectory(vaultBase, checked.target, {
445
- missingOk: false, label: 'lock legado de escrita do Vault', code,
446
- });
568
+ if (!withPublicLockRetry(lock, code, retryState, () => unlinkVaultFile(
569
+ vaultBase, currentOwner.path, {
570
+ label: 'owner legado morto do lock de escrita do Vault', code,
571
+ },
572
+ ))) return false;
573
+ return withPublicLockRetry(lock, code, retryState, () => removeVaultLockDirectory(
574
+ vaultBase, checked.target, {
575
+ missingOk: false, label: 'lock legado de escrita do Vault', code,
576
+ },
577
+ ));
447
578
  }
448
579
 
449
580
  // Locks are published by atomic directory rename only after owner + lease exist.
450
581
  // Thus an old empty/partial directory is legacy or crash residue, never an in-flight
451
582
  // live acquisition. Unknown children remain fail-closed.
452
- const entries = readdirSync(checked.target);
453
- for (const name of entries) {
454
- assertVaultPathSafe(vaultBase, join(checked.target, name), {
455
- allowMissing: false, expectedType: 'file', label: `resíduo do lock do Vault ${name}`, code,
583
+ const partial = withPublicLockRetry(lock, code, retryState, () => {
584
+ const current = assertVaultPathSafe(vaultBase, lock, {
585
+ expectedType: 'directory', label: 'lock de escrita do Vault', code,
456
586
  });
457
- }
587
+ if (!current.exists) return null;
588
+ const entries = readdirSync(current.target);
589
+ for (const name of entries) {
590
+ assertVaultPathSafe(vaultBase, join(current.target, name), {
591
+ allowMissing: false, expectedType: 'file', label: `resíduo do lock do Vault ${name}`, code,
592
+ });
593
+ }
594
+ return {
595
+ entries,
596
+ currentStat: statSync(current.target),
597
+ currentOwner: vaultLockOwner(vaultBase, current.target, code),
598
+ };
599
+ });
600
+ if (!partial) return true;
601
+ const { entries, currentStat, currentOwner } = partial;
458
602
  if (entries.some((name) => name !== VAULT_LOCK_OWNER_FILE)) return false;
459
- const currentStat = statSync(checked.target);
460
- const currentOwner = vaultLockOwner(vaultBase, checked.target, code);
461
603
  if (currentStat.birthtimeMs !== before.birthtimeMs
462
604
  || currentStat.mtimeMs !== before.mtimeMs
463
605
  || currentOwner.raw !== observed.raw) return false;
464
606
  if (entries.includes(VAULT_LOCK_OWNER_FILE)
465
- && !unlinkVaultFile(vaultBase, currentOwner.path, {
466
- label: 'owner parcial do lock de escrita do Vault', code,
467
- })) return false;
468
- return removeVaultLockDirectory(vaultBase, checked.target, {
469
- missingOk: false, label: 'lock de escrita do Vault', code,
470
- });
607
+ && !withPublicLockRetry(lock, code, retryState, () => unlinkVaultFile(
608
+ vaultBase, currentOwner.path, {
609
+ label: 'owner parcial do lock de escrita do Vault', code,
610
+ },
611
+ ))) return false;
612
+ return withPublicLockRetry(lock, code, retryState, () => removeVaultLockDirectory(
613
+ vaultBase, checked.target, {
614
+ missingOk: false, label: 'lock de escrita do Vault', code,
615
+ },
616
+ ));
471
617
  }
472
618
 
473
619
  export function withVaultPathLock(vaultBase, path, fn, {
@@ -478,80 +624,103 @@ export function withVaultPathLock(vaultBase, path, fn, {
478
624
  const lock = `${path}.lock`;
479
625
  const deadline = Date.now() + timeoutMs;
480
626
  const token = randomUUID();
481
- while (true) {
482
- let current;
483
- try {
484
- current = assertVaultPathSafe(vaultBase, lock, {
485
- expectedType: 'directory', label: 'lock de escrita do Vault', code,
486
- });
487
- } catch (error) {
488
- if (retryableLockTopologyRace(vaultBase, lock, error, code)) continue;
489
- throw error;
490
- }
491
- if (current.exists) {
492
- let reaped = false;
493
- try { reaped = reapDeadVaultLock(vaultBase, lock, staleMs, code); }
494
- catch (error) {
495
- if (retryableLockTopologyRace(vaultBase, lock, error, code)) continue;
496
- if (error?.code === code || error?.code === 'VAULT_PATH_UNSAFE') throw error;
627
+ const retryState = { deadline, remaining: VAULT_LOCK_TOPOLOGY_RETRIES };
628
+ let firstAttempt = true;
629
+ try {
630
+ while (true) {
631
+ if (!firstAttempt && Date.now() >= deadline) return VAULT_LOCK_BUSY;
632
+ firstAttempt = false;
633
+ const current = inspectVaultLock(vaultBase, lock, code, retryState);
634
+ if (current.exists) {
635
+ const reaped = reapDeadVaultLock(vaultBase, lock, staleMs, code, retryState);
636
+ if (reaped) continue;
637
+ const remainingMs = deadline - Date.now();
638
+ if (remainingMs <= 0) return VAULT_LOCK_BUSY;
639
+ waitBriefly(Math.min(10, remainingMs));
640
+ continue;
497
641
  }
498
- if (reaped) continue;
499
- if (Date.now() >= deadline) return VAULT_LOCK_BUSY;
500
- waitBriefly(10);
501
- continue;
502
- }
503
642
 
504
- const pending = `${lock}.${process.pid}.${token}.pending`;
505
- let pendingCreated = false;
506
- try {
507
- mkdirVaultPath(vaultBase, pending, {
508
- recursive: false, exclusive: true, label: 'lock de escrita do Vault', code,
509
- });
510
- pendingCreated = true;
511
- writeVaultFileAtomic(vaultBase, join(pending, VAULT_LOCK_OWNER_FILE), `${JSON.stringify({
512
- v: 1,
513
- pid: process.pid,
514
- token,
515
- created_at: new Date().toISOString(),
516
- })}\n`, 'utf8', { label: 'owner do lock de escrita do Vault', code });
517
- writeVaultFileAtomic(vaultBase, vaultLockLease(pending, token), `${token}\n`, 'utf8', {
518
- label: 'lease do lock de escrita do Vault', code,
519
- });
520
- const raced = assertVaultPathSafe(vaultBase, lock, {
521
- expectedType: 'directory', label: 'lock de escrita do Vault', code,
522
- });
523
- if (raced.exists) {
524
- removePreparedVaultLock(vaultBase, pending, token, code);
525
- } else {
643
+ const pending = `${lock}.${process.pid}.${token}.pending`;
644
+ let pendingCreated = false;
645
+ try {
526
646
  try {
527
- renameVaultPath(vaultBase, pending, lock, {
528
- sourceType: 'directory', label: 'publicação do lock de escrita do Vault', code,
647
+ mkdirVaultPath(vaultBase, pending, {
648
+ recursive: false, exclusive: true, label: 'lock de escrita do Vault', code,
529
649
  });
530
- break;
650
+ pendingCreated = true;
531
651
  } catch (error) {
532
- const raced = assertVaultPathSafe(vaultBase, lock, {
533
- expectedType: 'directory', label: 'lock de escrita do Vault', code,
534
- });
535
- if (!raced.exists) throw error;
652
+ pendingCreated = typeof error?.[VAULT_PATH_CREATED] === 'string'
653
+ && pathKey(error[VAULT_PATH_CREATED]) === pathKey(pending);
654
+ throw error;
655
+ }
656
+ writeVaultFileAtomic(vaultBase, join(pending, VAULT_LOCK_OWNER_FILE), `${JSON.stringify({
657
+ v: 1,
658
+ pid: process.pid,
659
+ token,
660
+ created_at: new Date().toISOString(),
661
+ })}\n`, 'utf8', { label: 'owner do lock de escrita do Vault', code });
662
+ writeVaultFileAtomic(vaultBase, vaultLockLease(pending, token), `${token}\n`, 'utf8', {
663
+ label: 'lease do lock de escrita do Vault', code,
664
+ });
665
+ const raced = inspectVaultLock(vaultBase, lock, code, retryState);
666
+ if (raced.exists) {
536
667
  removePreparedVaultLock(vaultBase, pending, token, code);
668
+ } else {
669
+ try {
670
+ renameVaultPath(vaultBase, pending, lock, {
671
+ sourceType: 'directory', label: 'publicação do lock de escrita do Vault', code,
672
+ });
673
+ break;
674
+ } catch (error) {
675
+ const retryableRenameRace = retryablePublicLockError(lock, error, code, {
676
+ allowRaw: false,
677
+ });
678
+ const nativeRenameCollision = vaultLockRenameCollision(error, pending, lock);
679
+ const preflightRenameCollision = preexistingPublicLockError(lock, error, code);
680
+ if (!retryableRenameRace && !nativeRenameCollision && !preflightRenameCollision) {
681
+ throw error;
682
+ }
683
+ const racedAfterRename = inspectVaultLock(
684
+ vaultBase, lock, code, retryState, retryableRenameRace ? error : null,
685
+ );
686
+ if (!racedAfterRename.exists) {
687
+ // EPERM is ambiguous on Windows; without an extant destination it remains fatal.
688
+ if (error?.code === 'EPERM') throw error;
689
+ removePreparedVaultLock(vaultBase, pending, token, code);
690
+ continue;
691
+ }
692
+ removePreparedVaultLock(vaultBase, pending, token, code);
693
+ }
537
694
  }
695
+ } catch (error) {
696
+ if (pendingCreated) {
697
+ try { removePreparedVaultLock(vaultBase, pending, token, code); }
698
+ catch { /* residue private remains recoverable; never clean through an alias */ }
699
+ }
700
+ throw error;
538
701
  }
539
- } catch (error) {
540
- if (pendingCreated) {
541
- try { removePreparedVaultLock(vaultBase, pending, token, code); }
542
- catch { /* residue private remains recoverable; never clean through an alias */ }
543
- }
544
- throw error;
702
+ const remainingMs = deadline - Date.now();
703
+ if (remainingMs <= 0) return VAULT_LOCK_BUSY;
704
+ waitBriefly(Math.min(10, remainingMs));
545
705
  }
546
- if (Date.now() >= deadline) return VAULT_LOCK_BUSY;
547
- waitBriefly(10);
706
+ } catch (error) {
707
+ if (error?.[VAULT_LOCK_RETRY_DEADLINE]) return VAULT_LOCK_BUSY;
708
+ throw error;
548
709
  }
549
710
 
550
711
  try {
551
712
  return fn();
552
713
  } finally {
553
714
  try {
554
- releaseOwnedVaultLock(vaultBase, lock, { pid: process.pid, token, code });
715
+ releaseOwnedVaultLock(vaultBase, lock, {
716
+ pid: process.pid,
717
+ token,
718
+ code,
719
+ retryState: {
720
+ deadline: Date.now() + VAULT_LOCK_RELEASE_RETRY_WINDOW_MS,
721
+ remaining: VAULT_LOCK_TOPOLOGY_RETRIES,
722
+ },
723
+ });
555
724
  }
556
725
  catch { /* a failed safe release leaves the lock in place; never remove through an alias */ }
557
726
  }