@cortexkit/opencode-openai-auth 0.6.2 → 0.6.4

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.
@@ -0,0 +1,103 @@
1
+ import { realpathSync } from 'node:fs'
2
+ import { homedir } from 'node:os'
3
+ import { basename, dirname, join, resolve } from 'node:path'
4
+
5
+ /**
6
+ * Where the account store lives.
7
+ *
8
+ * This is the ONLY definition. It used to be duplicated verbatim in both
9
+ * `accounts.ts` and `refresh-file-lock.ts`, because `accounts.ts` imports the
10
+ * lock and the reverse import would have been a cycle. Two copies of a path
11
+ * resolver is a bad trade for that: the lock and the write must agree on the
12
+ * exact file, and if the copies ever drifted, a writer would take a lock on one
13
+ * path while writing another — mutual exclusion silently lost on a file holding
14
+ * credentials. Nothing would fail a test, since each copy works on its own.
15
+ *
16
+ * This module imports nothing from either side, so both can depend on it.
17
+ */
18
+
19
+ export const ACCOUNT_FILE_NAME = 'openai-auth.json'
20
+ export const ACCOUNT_STATE_FILE_NAME = 'openai-auth-state.json'
21
+
22
+ function getConfigDir() {
23
+ if (process.env.OPENCODE_CONFIG_DIR?.trim()) {
24
+ return process.env.OPENCODE_CONFIG_DIR.trim()
25
+ }
26
+ return join(
27
+ process.env.XDG_CONFIG_HOME || join(homedir(), '.config'),
28
+ 'opencode',
29
+ )
30
+ }
31
+
32
+ export function getAccountStoragePath() {
33
+ return (
34
+ process.env.OPENCODE_OPENAI_AUTH_FILE?.trim() ||
35
+ join(getConfigDir(), ACCOUNT_FILE_NAME)
36
+ )
37
+ }
38
+
39
+ /** Derive the state-file path from the config path without reading env vars. */
40
+ export function deriveStatePath(configPath: string): string {
41
+ return basename(configPath) === ACCOUNT_FILE_NAME
42
+ ? join(dirname(configPath), ACCOUNT_STATE_FILE_NAME)
43
+ : `${configPath}.state.json`
44
+ }
45
+
46
+ function normalizePathForComparison(path: string, platform: NodeJS.Platform) {
47
+ const resolved = resolve(path)
48
+ return platform === 'win32' || platform === 'darwin'
49
+ ? resolved.toLowerCase()
50
+ : resolved
51
+ }
52
+
53
+ function realpathForComparison(path: string): string | undefined {
54
+ try {
55
+ return realpathSync.native(path)
56
+ } catch {
57
+ try {
58
+ return join(realpathSync.native(dirname(path)), basename(path))
59
+ } catch {
60
+ return undefined
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Detect path aliases without requiring either file to exist. This is defense
67
+ * in depth: hardlinks and bind mounts can still make distinct paths share a
68
+ * file, because neither is distinguishable through pathname identity.
69
+ */
70
+ export function accountPathsCollide(
71
+ configPath: string,
72
+ statePath: string,
73
+ platform: NodeJS.Platform = process.platform,
74
+ ) {
75
+ if (
76
+ normalizePathForComparison(configPath, platform) ===
77
+ normalizePathForComparison(statePath, platform)
78
+ ) {
79
+ return true
80
+ }
81
+
82
+ const configIdentity = realpathForComparison(configPath)
83
+ const stateIdentity = realpathForComparison(statePath)
84
+ return (
85
+ configIdentity !== undefined &&
86
+ stateIdentity !== undefined &&
87
+ normalizePathForComparison(configIdentity, platform) ===
88
+ normalizePathForComparison(stateIdentity, platform)
89
+ )
90
+ }
91
+
92
+ export function getAccountStatePath(configPath = getAccountStoragePath()) {
93
+ const explicit = process.env.OPENCODE_OPENAI_AUTH_STATE_FILE?.trim()
94
+ if (explicit) {
95
+ if (accountPathsCollide(configPath, explicit)) {
96
+ throw new Error(
97
+ `OPENCODE_OPENAI_AUTH_STATE_FILE resolves to the config path (${resolve(configPath)}). Set OPENCODE_OPENAI_AUTH_STATE_FILE to a different file.`,
98
+ )
99
+ }
100
+ return explicit
101
+ }
102
+ return deriveStatePath(configPath)
103
+ }
@@ -1,24 +1,11 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
  import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
3
  import { dirname, join } from 'node:path'
4
+ import { getAccountStoragePath } from './account-paths'
5
5
 
6
6
  const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis)
7
7
  const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis)
8
8
 
9
- export const ACCOUNT_FILE_NAME = 'openai-auth.json'
10
- export const ACCOUNT_STATE_FILE_NAME = 'openai-auth-state.json'
11
-
12
- function getConfigDir() {
13
- if (process.env.OPENCODE_CONFIG_DIR?.trim()) {
14
- return process.env.OPENCODE_CONFIG_DIR.trim()
15
- }
16
- return join(
17
- process.env.XDG_CONFIG_HOME || join(homedir(), '.config'),
18
- 'opencode',
19
- )
20
- }
21
-
22
9
  // A concurrent contender renaming the freshly-created eviction-marker directory
23
10
  // away surfaces the vanished parent differently per platform: ENOENT on Linux,
24
11
  // EINVAL or ENOTDIR on macOS/APFS. All three mean the marker is no longer ours
@@ -28,21 +15,6 @@ export function isLostMarkerRaceError(error: unknown): boolean {
28
15
  return code === 'ENOENT' || code === 'EINVAL' || code === 'ENOTDIR'
29
16
  }
30
17
 
31
- export function getAccountStoragePath() {
32
- return (
33
- process.env.OPENCODE_OPENAI_AUTH_FILE?.trim() ||
34
- join(getConfigDir(), ACCOUNT_FILE_NAME)
35
- )
36
- }
37
-
38
- export function getAccountStatePath(configPath = getAccountStoragePath()) {
39
- const explicit = process.env.OPENCODE_OPENAI_AUTH_STATE_FILE?.trim()
40
- if (explicit) return explicit
41
- return configPath.endsWith(ACCOUNT_FILE_NAME)
42
- ? join(dirname(configPath), ACCOUNT_STATE_FILE_NAME)
43
- : `${configPath}.state.json`
44
- }
45
-
46
18
  export async function acquireRefreshFileLock(options: {
47
19
  name: string
48
20
  ttlMs: number
@@ -55,7 +27,14 @@ export async function acquireRefreshFileLock(options: {
55
27
  | 'stale-marker-stat'
56
28
  | 'stale-marker-claimed'
57
29
  | 'stale-lock-confirmed'
58
- | 'eviction-marker-acquired',
30
+ | 'eviction-marker-acquired'
31
+ | 'renewal-owner-confirmed'
32
+ | 'renewal-marker-unavailable'
33
+ | 'renewal-write-fenced'
34
+ | 'renewal-write-ready'
35
+ | 'relinquish-read'
36
+ | 'renewal-finished'
37
+ | 'release-owner-confirmed',
59
38
  ) => void | Promise<void>
60
39
  }): Promise<{ release: () => Promise<void> } | null> {
61
40
  const accountPath = options.path ?? getAccountStoragePath()
@@ -65,6 +44,18 @@ export async function acquireRefreshFileLock(options: {
65
44
  const now = options.now ?? Date.now
66
45
  let renewTimer: ReturnType<typeof setTimeout> | null = null
67
46
  let released = false
47
+ let renewalInFlight: Promise<void> | null = null
48
+ // Fencing-token eviction: a directory-based marker (mkdir O_EXCL) serializes
49
+ // destructive removal and generation-sensitive owner mutations. The marker
50
+ // holds an owner file so ownership survives a stale-marker recovery rename:
51
+ // the recovering contender renames the stale directory, then must re-check
52
+ // ownership before acting — preventing the 3rd interleaving where a stale
53
+ // observer renames the FRESH marker the mkdir-winner created.
54
+ const evictPath = `${lockPath}.evicting`
55
+ const evictOwnerPath = join(evictPath, 'owner.json')
56
+ const evictOwnerId = randomUUID()
57
+ const EVICT_TTL = 5_000
58
+ const MAX_STEAL_ATTEMPTS = 8
68
59
 
69
60
  async function readOwner() {
70
61
  try {
@@ -115,105 +106,197 @@ export async function acquireRefreshFileLock(options: {
115
106
  }
116
107
  }
117
108
 
118
- function scheduleRenewal() {
119
- if (!options.renew || released) return
120
- const intervalMs =
121
- options.renewIntervalMs ?? Math.max(1_000, Math.floor(options.ttlMs / 3))
122
- renewTimer = setRefreshLockRenewalTimeout(() => {
123
- void (async () => {
124
- try {
125
- const owner = await readOwner()
126
- const currentNow = now()
127
- if (
128
- released ||
129
- owner?.ownerId !== ownerId ||
130
- Number(owner?.expiresAt) <= currentNow
131
- ) {
132
- return
133
- }
134
- await writeOwner()
135
- scheduleRenewal()
136
- } catch {
137
- // If renewal fails, contenders will wait until the last written expiry.
138
- }
139
- })()
140
- }, intervalMs)
141
- if ('unref' in renewTimer) renewTimer.unref()
109
+ async function backoff() {
110
+ await new Promise((resolve) =>
111
+ setTimeout(resolve, Math.floor(Math.random() * 4)),
112
+ )
142
113
  }
143
114
 
144
- let acquired = await tryAcquire()
145
- if (!acquired) {
146
- // Fencing-token eviction: a directory-based marker (mkdir O_EXCL) serializes
147
- // destructive removal to one contender at a time. The marker holds an owner
148
- // file so ownership survives a stale-marker recovery rename: the recovering
149
- // contender renames the stale directory, then must re-check ownership before
150
- // acting — preventing the 3rd interleaving where a stale observer renames the
151
- // FRESH marker the mkdir-winner created.
152
- const evictPath = `${lockPath}.evicting`
153
- const evictOwnerPath = join(evictPath, 'owner.json')
154
- const evictOwnerId = randomUUID()
155
- const EVICT_TTL = 5_000
156
- const MAX_STEAL_ATTEMPTS = 8
115
+ async function lockIsLive() {
116
+ try {
117
+ const currentOwner = await readOwner()
118
+ return Number(currentOwner?.expiresAt) > now()
119
+ } catch {
120
+ try {
121
+ const current = await stat(lockPath)
122
+ return current.mtimeMs + options.ttlMs > now()
123
+ } catch {
124
+ // Lock doesn't exist — safe to acquire.
125
+ return false
126
+ }
127
+ }
128
+ }
129
+
130
+ // Fail-closed: any read error means we do NOT own the marker.
131
+ async function ownsEvictionMarker() {
132
+ try {
133
+ const owner = JSON.parse(await readFile(evictOwnerPath, 'utf8'))
134
+ return owner?.ownerId === evictOwnerId
135
+ } catch {
136
+ return false
137
+ }
138
+ }
139
+
140
+ async function releaseEvictionMarker() {
141
+ if (await ownsEvictionMarker()) {
142
+ await rm(evictPath, { recursive: true, force: true }).catch(() => {})
143
+ }
144
+ }
157
145
 
158
- async function backoff() {
159
- await new Promise((resolve) =>
160
- setTimeout(resolve, Math.floor(Math.random() * 4)),
146
+ async function tryAcquireEvictionMarker() {
147
+ await mkdir(evictPath)
148
+ try {
149
+ await writeFile(
150
+ evictOwnerPath,
151
+ `${JSON.stringify({ ownerId: evictOwnerId, createdAt: now() })}\n`,
152
+ { encoding: 'utf8', mode: 0o600, flag: 'wx' },
161
153
  )
154
+ } catch (error) {
155
+ // A competing contender can rename our just-created marker directory
156
+ // away between the mkdir above and this write (the stale-marker steal
157
+ // path below does exactly that). That is a lost race, not a failure, so
158
+ // report it as such and let the caller back off and retry rather than
159
+ // failing the whole lock acquisition.
160
+ if (isLostMarkerRaceError(error)) return false
161
+ await releaseEvictionMarker()
162
+ throw error
162
163
  }
164
+ if (options.onStep) await options.onStep('eviction-marker-acquired')
165
+ return true
166
+ }
163
167
 
164
- async function lockIsLive() {
165
- try {
166
- const currentOwner = await readOwner()
167
- return Number(currentOwner?.expiresAt) > now()
168
- } catch {
169
- try {
170
- const current = await stat(lockPath)
171
- return current.mtimeMs + options.ttlMs > now()
172
- } catch {
173
- // Lock doesn't exist — safe to acquire.
174
- return false
175
- }
176
- }
168
+ async function recoverStaleEvictionMarker(): Promise<
169
+ 'fresh' | 'missing' | 'recovered'
170
+ > {
171
+ let evictStat: Awaited<ReturnType<typeof stat>>
172
+ try {
173
+ evictStat = await stat(evictPath)
174
+ } catch (error) {
175
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing'
176
+ throw error
177
+ }
178
+ if (evictStat.mtimeMs + EVICT_TTL > now()) return 'fresh'
179
+
180
+ if (options.onStep) await options.onStep('stale-marker-stat')
181
+ const claimedPath = `${evictPath}.${randomUUID()}`
182
+ try {
183
+ await rename(evictPath, claimedPath)
184
+ } catch (error) {
185
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing'
186
+ throw error
187
+ }
188
+ if (options.onStep) await options.onStep('stale-marker-claimed')
189
+ await rm(claimedPath, { recursive: true, force: true }).catch(() => {})
190
+ return 'recovered'
191
+ }
192
+
193
+ async function withEvictionMarker(action: () => Promise<void>) {
194
+ try {
195
+ if (!(await tryAcquireEvictionMarker())) return false
196
+ } catch (error) {
197
+ if ((error as NodeJS.ErrnoException).code === 'EEXIST') return false
198
+ throw error
177
199
  }
178
200
 
179
- // Fail-closed: any read error means we do NOT own the marker.
180
- async function ownsEvictionMarker() {
201
+ try {
202
+ await action()
203
+ } finally {
204
+ await releaseEvictionMarker()
205
+ }
206
+ return true
207
+ }
208
+
209
+ // Marker loss after a write may mean our record replaced a successor's.
210
+ // Delete only a record still owned by us; a concurrent successor write can
211
+ // then yield zero winners, never two.
212
+ async function relinquishLockAfterMarkerLoss() {
213
+ for (let attempt = 0; attempt < MAX_STEAL_ATTEMPTS; attempt++) {
214
+ if (options.onStep) await options.onStep('relinquish-read')
215
+ let owner: { ownerId?: string } | undefined
181
216
  try {
182
- const owner = JSON.parse(await readFile(evictOwnerPath, 'utf8'))
183
- return owner?.ownerId === evictOwnerId
217
+ owner = await readOwner()
184
218
  } catch {
185
- return false
219
+ return
186
220
  }
187
- }
188
-
189
- async function tryAcquireEvictionMarker() {
190
- await mkdir(evictPath)
221
+ if (owner?.ownerId !== ownerId) return
191
222
  try {
192
- await writeFile(
193
- evictOwnerPath,
194
- `${JSON.stringify({ ownerId: evictOwnerId, createdAt: now() })}\n`,
195
- { encoding: 'utf8', mode: 0o600, flag: 'wx' },
196
- )
197
- } catch (error) {
198
- // A competing contender can rename our just-created marker directory
199
- // away between the mkdir above and this write (the stale-marker steal
200
- // path below does exactly that). That is a lost race, not a failure, so
201
- // report it as such and let the caller back off and retry rather than
202
- // failing the whole lock acquisition.
203
- if (isLostMarkerRaceError(error)) return false
204
- await releaseEvictionMarker()
205
- throw error
223
+ await rm(lockPath, { recursive: true, force: true })
224
+ return
225
+ } catch {
226
+ await backoff()
206
227
  }
207
- await options.onStep?.('eviction-marker-acquired')
208
- return true
209
228
  }
229
+ }
210
230
 
211
- async function releaseEvictionMarker() {
212
- if (await ownsEvictionMarker()) {
213
- await rm(evictPath, { recursive: true, force: true }).catch(() => {})
214
- }
215
- }
231
+ function scheduleRenewal() {
232
+ if (!options.renew || released) return
233
+ const intervalMs =
234
+ options.renewIntervalMs ?? Math.max(1_000, Math.floor(options.ttlMs / 3))
235
+ renewTimer = setRefreshLockRenewalTimeout(() => {
236
+ const renewal = (async () => {
237
+ let shouldReschedule = !released
238
+ try {
239
+ const markerAcquired = await withEvictionMarker(async () => {
240
+ const owner = await readOwner()
241
+ const currentNow = now()
242
+ if (released || owner?.ownerId !== ownerId) {
243
+ shouldReschedule = false
244
+ return
245
+ }
246
+ // An expired lease is no longer ours to extend; a contender may
247
+ // already be eligible to acquire it.
248
+ if (Number(owner?.expiresAt) <= currentNow) {
249
+ shouldReschedule = false
250
+ return
251
+ }
252
+ if (options.onStep) await options.onStep('renewal-owner-confirmed')
253
+ if (released) {
254
+ shouldReschedule = false
255
+ return
256
+ }
257
+ if (!(await ownsEvictionMarker())) return
258
+ if (options.onStep) await options.onStep('renewal-write-fenced')
259
+ if (released) {
260
+ shouldReschedule = false
261
+ return
262
+ }
263
+ if (!(await ownsEvictionMarker())) return
264
+ if (options.onStep) await options.onStep('renewal-write-ready')
265
+ await writeOwner()
266
+ if (!(await ownsEvictionMarker())) {
267
+ // Marker read errors fail closed: prompt relinquish avoids ambiguity
268
+ // instead of waiting for TTL; both outcomes keep zero or one winner.
269
+ shouldReschedule = false
270
+ await relinquishLockAfterMarkerLoss()
271
+ return
272
+ }
273
+ })
274
+ if (!markerAcquired && options.onStep) {
275
+ await options.onStep('renewal-marker-unavailable')
276
+ }
277
+ } catch {
278
+ // Transient marker and filesystem failures retry on the next interval.
279
+ } finally {
280
+ if (options.onStep) {
281
+ try {
282
+ await options.onStep('renewal-finished')
283
+ } catch {
284
+ // Test seams must not turn an otherwise-safe renewal into a rejection.
285
+ }
286
+ }
287
+ if (shouldReschedule && !released) scheduleRenewal()
288
+ }
289
+ })()
290
+ renewalInFlight = renewal
291
+ void renewal.finally(() => {
292
+ if (renewalInFlight === renewal) renewalInFlight = null
293
+ })
294
+ }, intervalMs)
295
+ if ('unref' in renewTimer) renewTimer.unref()
296
+ }
216
297
 
298
+ let acquired = await tryAcquire()
299
+ if (!acquired) {
217
300
  for (let attempt = 0; attempt < MAX_STEAL_ATTEMPTS; attempt++) {
218
301
  acquired = await tryAcquire()
219
302
  if (acquired) break
@@ -228,31 +311,8 @@ export async function acquireRefreshFileLock(options: {
228
311
  const code = (evictError as NodeJS.ErrnoException).code
229
312
  if (code !== 'EEXIST') throw evictError
230
313
 
231
- let evictStat: Awaited<ReturnType<typeof stat>>
232
- try {
233
- evictStat = await stat(evictPath)
234
- } catch (statError) {
235
- if ((statError as NodeJS.ErrnoException).code === 'ENOENT') {
236
- await backoff()
237
- continue
238
- }
239
- throw statError
240
- }
241
- if (evictStat.mtimeMs + EVICT_TTL > now()) return null
242
-
243
- await options.onStep?.('stale-marker-stat')
244
- const claimedPath = `${evictPath}.${randomUUID()}`
245
- try {
246
- await rename(evictPath, claimedPath)
247
- } catch (renameError) {
248
- if ((renameError as NodeJS.ErrnoException).code === 'ENOENT') {
249
- await backoff()
250
- continue
251
- }
252
- throw renameError
253
- }
254
- await options.onStep?.('stale-marker-claimed')
255
- await rm(claimedPath, { recursive: true, force: true }).catch(() => {})
314
+ const recovered = await recoverStaleEvictionMarker()
315
+ if (recovered === 'fresh') return null
256
316
  await backoff()
257
317
  continue
258
318
  }
@@ -262,7 +322,7 @@ export async function acquireRefreshFileLock(options: {
262
322
  // Fence check 1: verify we still own the marker before acting on the
263
323
  // stale-lock-confirmed decision.
264
324
  if (!(await ownsEvictionMarker())) return null
265
- await options.onStep?.('stale-lock-confirmed')
325
+ if (options.onStep) await options.onStep('stale-lock-confirmed')
266
326
  // Fence check 2: re-verify ownership after the seam (another contender
267
327
  // may have renamed our fresh marker while we were paused here).
268
328
  if (!(await ownsEvictionMarker())) return null
@@ -297,13 +357,25 @@ export async function acquireRefreshFileLock(options: {
297
357
  clearRefreshLockRenewalTimeout(renewTimer)
298
358
  renewTimer = null
299
359
  }
300
- try {
301
- const owner = await readOwner()
302
- if (owner?.ownerId !== ownerId) return
303
- } catch {
304
- return
360
+ await renewalInFlight
361
+ for (let attempt = 0; attempt < MAX_STEAL_ATTEMPTS; attempt++) {
362
+ try {
363
+ const markerAcquired = await withEvictionMarker(async () => {
364
+ const owner = await readOwner()
365
+ if (owner?.ownerId !== ownerId) return
366
+ if (options.onStep) await options.onStep('release-owner-confirmed')
367
+ if (!(await ownsEvictionMarker())) return
368
+ await rm(lockPath, { recursive: true, force: true }).catch(() => {})
369
+ })
370
+ if (markerAcquired) return
371
+ await recoverStaleEvictionMarker()
372
+ } catch {
373
+ return
374
+ }
375
+ await backoff()
305
376
  }
306
- await rm(lockPath, { recursive: true, force: true }).catch(() => {})
377
+ // Do not delete by pathname without the marker: bounded retries leave the
378
+ // lease to expire rather than risking removal of a successor's lock.
307
379
  },
308
380
  }
309
381
  }
@@ -1,6 +0,0 @@
1
- import{randomUUID as L}from"node:crypto";import{mkdir as M,readFile as C,rename as u,rm as $,stat as O,writeFile as S}from"node:fs/promises";import{homedir as E}from"node:os";import{dirname as v,join as x}from"node:path";var m=globalThis.setTimeout.bind(globalThis),P=globalThis.clearTimeout.bind(globalThis),k="openai-auth.json";function p(){if(process.env.OPENCODE_CONFIG_DIR?.trim())return process.env.OPENCODE_CONFIG_DIR.trim();return x(process.env.XDG_CONFIG_HOME||x(E(),".config"),"opencode")}function h(z){let b=z?.code;return b==="ENOENT"||b==="EINVAL"||b==="ENOTDIR"}function l(){return process.env.OPENCODE_OPENAI_AUTH_FILE?.trim()||x(p(),k)}async function i(z){let G=`${z.path??l()}.${z.name}.lock`,T=x(G,"owner.json"),X=L(),K=z.now??Date.now,W=null,j=!1;async function N(){try{return JSON.parse(await C(G,"utf8"))}catch(B){if(B.code!=="EISDIR")throw B;return JSON.parse(await C(T,"utf8"))}}async function I(){await S(G,`${JSON.stringify({ownerId:X,expiresAt:K()+z.ttlMs})}
2
- `,{encoding:"utf8",mode:384})}async function R(){try{return await S(G,`${JSON.stringify({ownerId:X,expiresAt:K()+z.ttlMs})}
3
- `,{encoding:"utf8",mode:384,flag:"wx"}),!0}catch(B){let J=B.code;if(J==="EEXIST"||J==="EISDIR")return!1;if(J==="ENOENT"){await M(v(G),{recursive:!0});try{return await S(G,`${JSON.stringify({ownerId:X,expiresAt:K()+z.ttlMs})}
4
- `,{encoding:"utf8",mode:384,flag:"wx"}),!0}catch(V){let _=V.code;if(_==="EEXIST"||_==="EISDIR")return!1;throw V}}throw B}}function U(){if(!z.renew||j)return;let B=z.renewIntervalMs??Math.max(1000,Math.floor(z.ttlMs/3));if(W=m(()=>{(async()=>{try{let J=await N(),V=K();if(j||J?.ownerId!==X||Number(J?.expiresAt)<=V)return;await I(),U()}catch{}})()},B),"unref"in W)W.unref()}let Q=await R();if(!Q){let B=`${G}.evicting`,J=x(B,"owner.json"),V=L(),_=5000,w=8;async function D(){await new Promise((H)=>setTimeout(H,Math.floor(Math.random()*4)))}async function y(){try{let H=await N();return Number(H?.expiresAt)>K()}catch{try{return(await O(G)).mtimeMs+z.ttlMs>K()}catch{return!1}}}async function Y(){try{return JSON.parse(await C(J,"utf8"))?.ownerId===V}catch{return!1}}async function f(){await M(B);try{await S(J,`${JSON.stringify({ownerId:V,createdAt:K()})}
5
- `,{encoding:"utf8",mode:384,flag:"wx"})}catch(H){if(h(H))return!1;throw await F(),H}return await z.onStep?.("eviction-marker-acquired"),!0}async function F(){if(await Y())await $(B,{recursive:!0,force:!0}).catch(()=>{})}for(let H=0;H<8;H++){if(Q=await R(),Q)break;if(await y())return null;try{if(!await f()){await D();continue}}catch(q){if(q.code!=="EEXIST")throw q;let g;try{g=await O(B)}catch(Z){if(Z.code==="ENOENT"){await D();continue}throw Z}if(g.mtimeMs+5000>K())return null;await z.onStep?.("stale-marker-stat");let A=`${B}.${L()}`;try{await u(B,A)}catch(Z){if(Z.code==="ENOENT"){await D();continue}throw Z}await z.onStep?.("stale-marker-claimed"),await $(A,{recursive:!0,force:!0}).catch(()=>{}),await D();continue}try{if(await y())return null;if(!await Y())return null;if(await z.onStep?.("stale-lock-confirmed"),!await Y())return null;if(await $(G,{recursive:!0,force:!0}).catch(()=>{}),!await Y())return null;if(Q=await R(),!Q)return null;if(!await Y())return await $(G,{recursive:!0,force:!0}).catch(()=>{}),Q=!1,null;break}finally{await F()}}}if(!Q)return null;return U(),{release:async()=>{if(j=!0,W)P(W),W=null;try{if((await N())?.ownerId!==X)return}catch{return}await $(G,{recursive:!0,force:!0}).catch(()=>{})}}}
6
- export{i as ua};