@felan-ai/ext-memory 0.8.0 → 0.10.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.
Files changed (78) hide show
  1. package/README.md +41 -30
  2. package/dist/data.d.ts +3 -0
  3. package/dist/data.d.ts.map +1 -0
  4. package/dist/data.js +2 -0
  5. package/dist/data.js.map +1 -0
  6. package/dist/diagnostics.d.ts +3 -0
  7. package/dist/diagnostics.d.ts.map +1 -0
  8. package/dist/diagnostics.js +14 -0
  9. package/dist/diagnostics.js.map +1 -0
  10. package/dist/extension.d.ts +5 -0
  11. package/dist/extension.d.ts.map +1 -0
  12. package/dist/extension.js +37 -0
  13. package/dist/extension.js.map +1 -0
  14. package/dist/hydration.d.ts +0 -3
  15. package/dist/hydration.d.ts.map +1 -1
  16. package/dist/hydration.js +0 -78
  17. package/dist/hydration.js.map +1 -1
  18. package/dist/index.d.ts +18 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +31 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/local/context.d.ts +10 -0
  23. package/dist/local/context.d.ts.map +1 -0
  24. package/dist/local/context.js +10 -0
  25. package/dist/local/context.js.map +1 -0
  26. package/dist/local/control.d.ts +13 -0
  27. package/dist/local/control.d.ts.map +1 -0
  28. package/dist/local/control.js +183 -0
  29. package/dist/local/control.js.map +1 -0
  30. package/dist/local/coordinator.d.ts +93 -0
  31. package/dist/local/coordinator.d.ts.map +1 -0
  32. package/dist/local/coordinator.js +859 -0
  33. package/dist/local/coordinator.js.map +1 -0
  34. package/dist/local/dreamer.d.ts +53 -0
  35. package/dist/local/dreamer.d.ts.map +1 -0
  36. package/dist/local/dreamer.js +490 -0
  37. package/dist/local/dreamer.js.map +1 -0
  38. package/dist/local/history.d.ts +33 -0
  39. package/dist/local/history.d.ts.map +1 -0
  40. package/dist/local/history.js +300 -0
  41. package/dist/local/history.js.map +1 -0
  42. package/dist/local/hydration.d.ts +6 -0
  43. package/dist/local/hydration.d.ts.map +1 -0
  44. package/dist/local/hydration.js +84 -0
  45. package/dist/local/hydration.js.map +1 -0
  46. package/dist/local/lease.d.ts +14 -0
  47. package/dist/local/lease.d.ts.map +1 -0
  48. package/dist/local/lease.js +184 -0
  49. package/dist/local/lease.js.map +1 -0
  50. package/dist/local/lock.d.ts +28 -0
  51. package/dist/local/lock.d.ts.map +1 -0
  52. package/dist/local/lock.js +431 -0
  53. package/dist/local/lock.js.map +1 -0
  54. package/dist/local/project.d.ts +8 -0
  55. package/dist/local/project.d.ts.map +1 -0
  56. package/dist/local/project.js +33 -0
  57. package/dist/local/project.js.map +1 -0
  58. package/dist/local/run-retention.d.ts +13 -0
  59. package/dist/local/run-retention.d.ts.map +1 -0
  60. package/dist/local/run-retention.js +272 -0
  61. package/dist/local/run-retention.js.map +1 -0
  62. package/dist/local/run.d.ts +84 -0
  63. package/dist/local/run.d.ts.map +1 -0
  64. package/dist/local/run.js +361 -0
  65. package/dist/local/run.js.map +1 -0
  66. package/dist/local/store.d.ts +117 -0
  67. package/dist/local/store.d.ts.map +1 -0
  68. package/dist/local/store.js +735 -0
  69. package/dist/local/store.js.map +1 -0
  70. package/dist/local/ui.d.ts +9 -0
  71. package/dist/local/ui.d.ts.map +1 -0
  72. package/dist/local/ui.js +166 -0
  73. package/dist/local/ui.js.map +1 -0
  74. package/dist/session-history.d.ts +18 -0
  75. package/dist/session-history.d.ts.map +1 -0
  76. package/dist/session-history.js +91 -0
  77. package/dist/session-history.js.map +1 -0
  78. package/package.json +9 -5
@@ -0,0 +1,184 @@
1
+ import { removeIfPresent } from './context.js';
2
+ import { acquireLocalFileLock } from './lock.js';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { basename, dirname, join } from 'node:path';
5
+ const DEFAULT_STALE_MS = 30_000;
6
+ const DEFAULT_UPDATE_MS = 10_000;
7
+ export async function acquireLocalMemoryLease(environment, projectDirectory, options = {}) {
8
+ const target = join(projectDirectory, 'writer');
9
+ const gateTarget = join(projectDirectory, 'writer.gate');
10
+ await environment.runtime.files.ensurePrivateDirectory(dirname(target), { recursive: true });
11
+ await environment.runtime.files.append(target, new TextEncoder().encode(''));
12
+ await environment.runtime.files.append(gateTarget, new TextEncoder().encode(''));
13
+ const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
14
+ let gateLock;
15
+ try {
16
+ gateLock = await acquireLocalFileLock(environment, gateTarget, {
17
+ realpath: false,
18
+ stale: staleMs,
19
+ update: options.updateMs ?? DEFAULT_UPDATE_MS,
20
+ retries: 0,
21
+ preservePathOnRelease: true,
22
+ });
23
+ }
24
+ catch (error) {
25
+ if (isLocked(error))
26
+ return undefined;
27
+ throw error;
28
+ }
29
+ let fileLock;
30
+ try {
31
+ await recoverStaleLegacyLock(environment, target, staleMs);
32
+ fileLock = await acquireLocalFileLock(environment, target, {
33
+ realpath: false,
34
+ stale: staleMs,
35
+ update: options.updateMs ?? DEFAULT_UPDATE_MS,
36
+ retries: 0,
37
+ preservePathOnRelease: true,
38
+ });
39
+ }
40
+ catch (error) {
41
+ await gateLock.release().catch(() => { });
42
+ await gateLock.retire(staleMs);
43
+ if (isLocked(error))
44
+ return undefined;
45
+ throw error;
46
+ }
47
+ const token = randomUUID();
48
+ const acquiredAt = new Date().toISOString();
49
+ const ownerPath = `${target}.owner.${token}.json`;
50
+ const temporaryOwnerPath = `${ownerPath}.${token}.tmp`;
51
+ const compromiseController = new AbortController();
52
+ const onCompromise = () => {
53
+ compromiseController.abort(new Error('Memory writer lease was lost'));
54
+ };
55
+ gateLock.compromised.addEventListener('abort', onCompromise, { once: true });
56
+ fileLock.compromised.addEventListener('abort', onCompromise, { once: true });
57
+ try {
58
+ await environment.runtime.writeFile(temporaryOwnerPath, new TextEncoder().encode(`${JSON.stringify({ token, acquiredAt, pid: process.pid })}\n`), { noFollow: true, exclusive: true });
59
+ await environment.runtime.files.move(temporaryOwnerPath, ownerPath);
60
+ gateLock.throwIfCompromised('Memory writer lease was lost during acquisition');
61
+ fileLock.throwIfCompromised('Memory writer lease was lost during acquisition');
62
+ if (!(await gateLock.isCurrent()) || !(await fileLock.isCurrent())) {
63
+ throw new Error('Memory writer lease was lost during acquisition');
64
+ }
65
+ await garbageCollectOwnerFiles(environment, target, ownerPath);
66
+ }
67
+ catch (error) {
68
+ await removeIfPresent(environment, temporaryOwnerPath).catch(() => { });
69
+ await removeOwnerIfToken(environment, ownerPath, token);
70
+ await fileLock.release().catch(() => { });
71
+ await fileLock.retire(staleMs);
72
+ gateLock.compromised.removeEventListener('abort', onCompromise);
73
+ fileLock.compromised.removeEventListener('abort', onCompromise);
74
+ await gateLock.release().catch(() => { });
75
+ await gateLock.retire(staleMs);
76
+ throw error;
77
+ }
78
+ let released = false;
79
+ return {
80
+ token,
81
+ acquiredAt,
82
+ compromised: compromiseController.signal,
83
+ async verify() {
84
+ if (released || fileLock.isCompromised())
85
+ return false;
86
+ if (gateLock.isCompromised())
87
+ return false;
88
+ if (!(await gateLock.isCurrent()) || !(await fileLock.isCurrent()))
89
+ return false;
90
+ try {
91
+ const owner = JSON.parse(new TextDecoder().decode(await environment.runtime.readFile(ownerPath)));
92
+ return owner.token === token;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ },
98
+ async release() {
99
+ if (released)
100
+ return;
101
+ released = true;
102
+ let ownsOwnerRecord = false;
103
+ try {
104
+ const owner = JSON.parse(new TextDecoder().decode(await environment.runtime.readFile(ownerPath)));
105
+ ownsOwnerRecord = owner.token === token;
106
+ }
107
+ catch {
108
+ // A missing owner record is fenced and must not release a possible successor lock.
109
+ }
110
+ if (ownsOwnerRecord)
111
+ await removeIfPresent(environment, ownerPath).catch(() => { });
112
+ // LocalFileLock guards the underlying directory identity before rmdir, so
113
+ // releasing a fenced handle stops its heartbeat without removing a successor.
114
+ await fileLock.release().catch(() => { });
115
+ await fileLock.retire(staleMs);
116
+ gateLock.compromised.removeEventListener('abort', onCompromise);
117
+ fileLock.compromised.removeEventListener('abort', onCompromise);
118
+ await gateLock.release().catch(() => { });
119
+ await gateLock.retire(staleMs);
120
+ },
121
+ };
122
+ }
123
+ async function garbageCollectOwnerFiles(environment, target, currentOwnerPath) {
124
+ const directory = dirname(target);
125
+ const prefix = `${basename(target)}.owner.`;
126
+ let entries;
127
+ try {
128
+ entries = await environment.runtime.listFiles(directory, { includeDirectories: true, includeOther: true });
129
+ }
130
+ catch (error) {
131
+ if (isMissing(error))
132
+ return;
133
+ throw error;
134
+ }
135
+ await Promise.all(entries
136
+ .filter((entry) => entry.startsWith(prefix) && join(directory, entry) !== currentOwnerPath)
137
+ .map((entry) => removeIfPresent(environment, join(directory, entry))));
138
+ }
139
+ async function recoverStaleLegacyLock(environment, target, staleMs) {
140
+ const lockPath = `${target}.lock`;
141
+ const ownerPath = join(lockPath, 'owner.json');
142
+ const effectiveStaleMs = Math.max(staleMs, 2_000);
143
+ const initial = await safeLstat(environment, lockPath);
144
+ if (initial?.kind !== 'directory' || initial.modifiedAt >= Date.now() - effectiveStaleMs)
145
+ return;
146
+ const owner = await safeLstat(environment, ownerPath);
147
+ if (owner?.kind !== 'file')
148
+ return;
149
+ const confirmed = await safeLstat(environment, lockPath);
150
+ if (confirmed?.kind !== 'directory'
151
+ || confirmed.modifiedAt !== initial.modifiedAt
152
+ || confirmed.modifiedAt >= Date.now() - effectiveStaleMs)
153
+ return;
154
+ await removeIfPresent(environment, ownerPath);
155
+ const retiredAt = new Date(Date.now() - effectiveStaleMs - 1_000);
156
+ await environment.runtime.files.touch(lockPath, (retiredAt).getTime()).catch(() => { });
157
+ }
158
+ async function removeOwnerIfToken(environment, ownerPath, token) {
159
+ try {
160
+ const owner = JSON.parse(new TextDecoder().decode(await environment.runtime.readFile(ownerPath)));
161
+ if (owner.token === token)
162
+ await removeIfPresent(environment, ownerPath);
163
+ }
164
+ catch { }
165
+ }
166
+ async function safeLstat(environment, path) {
167
+ try {
168
+ return await environment.runtime.files.inspect(path);
169
+ }
170
+ catch (error) {
171
+ if (isMissing(error))
172
+ return undefined;
173
+ throw error;
174
+ }
175
+ }
176
+ function isLocked(error) {
177
+ return typeof error === 'object'
178
+ && error !== null
179
+ && Reflect.get(error, 'code') === 'ELOCKED';
180
+ }
181
+ function isMissing(error) {
182
+ return typeof error === 'object' && error !== null && Reflect.get(error, 'code') === 'ENOENT';
183
+ }
184
+ //# sourceMappingURL=lease.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease.js","sourceRoot":"","sources":["../../src/local/lease.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { MemoryContext } from './context.js';
2
+ interface LocalFileLockRetryOptions {
3
+ readonly retries?: number;
4
+ readonly factor?: number;
5
+ readonly minTimeout?: number;
6
+ readonly maxTimeout?: number;
7
+ readonly randomize?: boolean;
8
+ }
9
+ export interface LocalFileLockOptions {
10
+ readonly stale?: number;
11
+ readonly update?: number;
12
+ readonly retries?: number | LocalFileLockRetryOptions;
13
+ readonly realpath?: boolean;
14
+ readonly lockfilePath?: string;
15
+ readonly preservePathOnRelease?: boolean;
16
+ }
17
+ export interface LocalFileLock {
18
+ readonly compromised: AbortSignal;
19
+ isCompromised(): boolean;
20
+ isCurrent(): Promise<boolean>;
21
+ throwIfCompromised(message?: string): void;
22
+ retire(staleMs: number): Promise<void>;
23
+ release(): Promise<void>;
24
+ }
25
+ export declare function withLocalFileLock<T>(environment: MemoryContext, path: string, options: LocalFileLockOptions, operation: (lock: LocalFileLock) => Promise<T>): Promise<T>;
26
+ export declare function acquireLocalFileLock(environment: MemoryContext, path: string, options?: LocalFileLockOptions): Promise<LocalFileLock>;
27
+ export {};
28
+ //# sourceMappingURL=lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../src/local/lock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAQlD,UAAU,yBAAyB;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAGD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,yBAAyB,CAAC;IACtD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;CAC1C;AAGD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,aAAa,IAAI,OAAO,CAAC;IACzB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,kBAAkB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AA+BD,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,WAAW,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EACjF,OAAO,EAAE,oBAAoB,EAC7B,SAAS,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC7C,OAAO,CAAC,CAAC,CAAC,CAcZ;AAGD,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EACjF,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,aAAa,CAAC,CA6CxB"}
@@ -0,0 +1,431 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { resolve } from 'node:path';
3
+ const DEFAULT_STALE_MS = 10_000;
4
+ const MINIMUM_STALE_MS = 2_000;
5
+ const MINIMUM_UPDATE_MS = 1_000;
6
+ export async function withLocalFileLock(environment, path, options, operation) {
7
+ const lock = await acquireLocalFileLock(environment, path, {
8
+ ...options,
9
+ preservePathOnRelease: true,
10
+ });
11
+ try {
12
+ lock.throwIfCompromised();
13
+ const result = await operation(lock);
14
+ lock.throwIfCompromised();
15
+ return result;
16
+ }
17
+ finally {
18
+ await lock.release();
19
+ await lock.retire(options.stale ?? DEFAULT_STALE_MS);
20
+ }
21
+ }
22
+ export async function acquireLocalFileLock(environment, path, options = {}) {
23
+ const controller = new AbortController();
24
+ let compromiseError;
25
+ const canonicalPath = options.realpath === false ? resolve(path) : await environment.runtime.files.canonicalPath(path);
26
+ const lockPath = options.lockfilePath ?? `${canonicalPath}.lock`;
27
+ const staleMs = Math.max(options.stale ?? DEFAULT_STALE_MS, MINIMUM_STALE_MS);
28
+ const requestedUpdateMs = options.update ?? staleMs / 2;
29
+ const updateMs = Math.max(Math.min(requestedUpdateMs, staleMs / 2), MINIMUM_UPDATE_MS);
30
+ const state = await acquireLockState(environment, canonicalPath, lockPath, staleMs, updateMs, options.retries, (error) => {
31
+ compromiseError ??= error;
32
+ controller.abort(error);
33
+ });
34
+ scheduleHeartbeat(environment, state);
35
+ const isCurrent = async () => {
36
+ if (compromiseError)
37
+ return false;
38
+ const current = await readLockIdentity(environment, lockPath);
39
+ return current !== undefined && sameLockIdentity(current, state.identity);
40
+ };
41
+ let released = false;
42
+ return {
43
+ compromised: controller.signal,
44
+ isCompromised: () => compromiseError !== undefined,
45
+ isCurrent,
46
+ throwIfCompromised(message) {
47
+ if (!compromiseError)
48
+ return;
49
+ if (message === undefined)
50
+ throw compromiseError;
51
+ throw new Error(message, { cause: compromiseError });
52
+ },
53
+ async retire(retireStaleMs) {
54
+ if (!options.preservePathOnRelease || !(await isCurrent()))
55
+ return;
56
+ const retiredAt = new Date(Date.now() - Math.max(retireStaleMs, MINIMUM_STALE_MS) - 1_000);
57
+ await setTimes(environment, lockPath, retiredAt).catch(() => { });
58
+ },
59
+ async release() {
60
+ if (released)
61
+ return;
62
+ released = true;
63
+ const owned = state.status === 'active';
64
+ state.status = 'released';
65
+ clearHeartbeat(state);
66
+ await state.heartbeat;
67
+ if (!owned || options.preservePathOnRelease)
68
+ return;
69
+ await removeLockIfCurrent(environment, lockPath, state.identity);
70
+ },
71
+ };
72
+ }
73
+ async function acquireLockState(environment, targetPath, lockPath, staleMs, updateMs, retries, onCompromised) {
74
+ const retryDelays = createRetryDelays(retries);
75
+ let lastError;
76
+ for (let attempt = 0; attempt <= retryDelays.length; attempt += 1) {
77
+ try {
78
+ await acquireLockDirectory(environment, targetPath, lockPath, staleMs);
79
+ return await initializeLockState(environment, targetPath, lockPath, staleMs, updateMs, onCompromised);
80
+ }
81
+ catch (error) {
82
+ lastError = error;
83
+ const retryDelay = retryDelays[attempt];
84
+ if (retryDelay === undefined)
85
+ throw error;
86
+ await delay(retryDelay);
87
+ }
88
+ }
89
+ throw lastError;
90
+ }
91
+ async function acquireLockDirectory(environment, targetPath, lockPath, staleMs) {
92
+ try {
93
+ await environment.runtime.files.ensurePrivateDirectory(lockPath);
94
+ return;
95
+ }
96
+ catch (error) {
97
+ if (!hasErrorCode(error, 'EEXIST'))
98
+ throw error;
99
+ }
100
+ let stats;
101
+ try {
102
+ stats = await environment.runtime.files.inspect(lockPath);
103
+ }
104
+ catch (error) {
105
+ if (!hasErrorCode(error, 'ENOENT'))
106
+ throw error;
107
+ return makeDirectoryWithoutStaleRecovery(environment, targetPath, lockPath);
108
+ }
109
+ if (Math.round(stats.modifiedAt) >= Date.now() - staleMs)
110
+ throw lockHeldError(targetPath);
111
+ if (!(await reclaimStaleLock(environment, lockPath, stats)))
112
+ throw lockHeldError(targetPath);
113
+ await makeDirectoryWithoutStaleRecovery(environment, targetPath, lockPath);
114
+ }
115
+ async function reclaimStaleLock(environment, lockPath, observed) {
116
+ const claimPath = lockClaimPath(lockPath, 'reclaim');
117
+ try {
118
+ await environment.runtime.files.move(lockPath, claimPath);
119
+ }
120
+ catch (error) {
121
+ if (hasErrorCode(error, 'ENOENT'))
122
+ return true;
123
+ throw error;
124
+ }
125
+ let claimed;
126
+ try {
127
+ claimed = await environment.runtime.files.inspect(claimPath);
128
+ }
129
+ catch (error) {
130
+ if (hasErrorCode(error, 'ENOENT'))
131
+ return true;
132
+ await restoreClaim(environment, claimPath, lockPath);
133
+ throw error;
134
+ }
135
+ if (!sameLockIdentity(claimed, lockIdentity(observed))
136
+ || Math.round(claimed.modifiedAt) !== Math.round(observed.modifiedAt)) {
137
+ await restoreClaim(environment, claimPath, lockPath);
138
+ return false;
139
+ }
140
+ try {
141
+ await environment.runtime.files.removeEmptyDirectory(claimPath);
142
+ return true;
143
+ }
144
+ catch (error) {
145
+ await restoreClaim(environment, claimPath, lockPath);
146
+ if (isDirectoryNotEmpty(error))
147
+ return false;
148
+ if (hasErrorCode(error, 'ENOENT'))
149
+ return true;
150
+ throw error;
151
+ }
152
+ }
153
+ async function makeDirectoryWithoutStaleRecovery(environment, targetPath, lockPath) {
154
+ try {
155
+ await environment.runtime.files.ensurePrivateDirectory(lockPath);
156
+ }
157
+ catch (error) {
158
+ if (hasErrorCode(error, 'EEXIST'))
159
+ throw lockHeldError(targetPath);
160
+ throw error;
161
+ }
162
+ }
163
+ async function initializeLockState(environment, targetPath, lockPath, staleMs, updateMs, onCompromised) {
164
+ let identity;
165
+ try {
166
+ let initialStats;
167
+ try {
168
+ initialStats = await environment.runtime.files.inspect(lockPath);
169
+ }
170
+ catch (error) {
171
+ if (hasErrorCode(error, 'ENOENT'))
172
+ throw lockHeldError(targetPath);
173
+ throw error;
174
+ }
175
+ identity = lockIdentity(initialStats);
176
+ const probeTime = new Date(Math.ceil(Date.now() / 1_000) * 1_000 + 5);
177
+ try {
178
+ await setTimes(environment, lockPath, probeTime);
179
+ }
180
+ catch (error) {
181
+ if (hasErrorCode(error, 'ENOENT'))
182
+ throw lockHeldError(targetPath);
183
+ throw error;
184
+ }
185
+ let stats;
186
+ try {
187
+ stats = await environment.runtime.files.inspect(lockPath);
188
+ }
189
+ catch (error) {
190
+ if (hasErrorCode(error, 'ENOENT'))
191
+ throw lockHeldError(targetPath);
192
+ throw error;
193
+ }
194
+ if (!sameLockIdentity(stats, identity))
195
+ throw lockHeldError(targetPath);
196
+ return {
197
+ lockPath,
198
+ identity,
199
+ staleMs,
200
+ updateMs,
201
+ precision: Math.round(stats.modifiedAt) % 1_000 === 0 ? 'seconds' : 'milliseconds',
202
+ onCompromised,
203
+ status: 'active',
204
+ mtime: new Date(Math.round(stats.modifiedAt)),
205
+ lastUpdateMs: Date.now(),
206
+ updateDelayMs: undefined,
207
+ updateTimer: undefined,
208
+ heartbeat: undefined,
209
+ };
210
+ }
211
+ catch (error) {
212
+ if (identity)
213
+ await removeLockIfCurrent(environment, lockPath, identity).catch(() => { });
214
+ throw error;
215
+ }
216
+ }
217
+ function scheduleHeartbeat(environment, state) {
218
+ if (state.status !== 'active' || state.updateTimer)
219
+ return;
220
+ state.updateTimer = setTimeout(() => {
221
+ state.updateTimer = undefined;
222
+ const heartbeat = updateHeartbeat(environment, state).catch((error) => {
223
+ if (state.status === 'active')
224
+ compromiseLock(state, toCompromisedError(error));
225
+ }).finally(() => {
226
+ if (state.heartbeat === heartbeat)
227
+ state.heartbeat = undefined;
228
+ });
229
+ state.heartbeat = heartbeat;
230
+ }, state.updateDelayMs ?? state.updateMs);
231
+ state.updateTimer.unref();
232
+ }
233
+ async function updateHeartbeat(environment, state) {
234
+ if (state.status !== 'active')
235
+ return;
236
+ let stats;
237
+ try {
238
+ stats = await environment.runtime.files.inspect(state.lockPath);
239
+ }
240
+ catch (error) {
241
+ if (state.status !== 'active')
242
+ return;
243
+ if (hasErrorCode(error, 'ENOENT') || heartbeatOverdue(state)) {
244
+ compromiseLock(state, toCompromisedError(error));
245
+ }
246
+ else {
247
+ state.updateDelayMs = MINIMUM_UPDATE_MS;
248
+ scheduleHeartbeat(environment, state);
249
+ }
250
+ return;
251
+ }
252
+ if (state.status !== 'active')
253
+ return;
254
+ if (!sameLockIdentity(stats, state.identity) || Math.round(stats.modifiedAt) !== state.mtime.getTime()) {
255
+ compromiseLock(state, compromisedError('Unable to update lock within the stale threshold'));
256
+ return;
257
+ }
258
+ const mtime = currentMtime(state.precision);
259
+ try {
260
+ await setTimes(environment, state.lockPath, mtime);
261
+ }
262
+ catch (error) {
263
+ if (state.status !== 'active')
264
+ return;
265
+ if (hasErrorCode(error, 'ENOENT') || heartbeatOverdue(state)) {
266
+ compromiseLock(state, toCompromisedError(error));
267
+ }
268
+ else {
269
+ state.updateDelayMs = MINIMUM_UPDATE_MS;
270
+ scheduleHeartbeat(environment, state);
271
+ }
272
+ return;
273
+ }
274
+ if (state.status !== 'active')
275
+ return;
276
+ let confirmed;
277
+ try {
278
+ confirmed = await environment.runtime.files.inspect(state.lockPath);
279
+ }
280
+ catch (error) {
281
+ if (state.status !== 'active')
282
+ return;
283
+ state.mtime = mtime;
284
+ if (hasErrorCode(error, 'ENOENT') || heartbeatOverdue(state)) {
285
+ compromiseLock(state, toCompromisedError(error));
286
+ }
287
+ else {
288
+ state.updateDelayMs = MINIMUM_UPDATE_MS;
289
+ scheduleHeartbeat(environment, state);
290
+ }
291
+ return;
292
+ }
293
+ if (state.status !== 'active')
294
+ return;
295
+ if (!sameLockIdentity(confirmed, state.identity) || Math.round(confirmed.modifiedAt) !== mtime.getTime()) {
296
+ compromiseLock(state, compromisedError('Unable to update lock within the stale threshold'));
297
+ return;
298
+ }
299
+ state.mtime = mtime;
300
+ state.lastUpdateMs = Date.now();
301
+ state.updateDelayMs = undefined;
302
+ scheduleHeartbeat(environment, state);
303
+ }
304
+ function heartbeatOverdue(state) {
305
+ return state.lastUpdateMs + state.staleMs < Date.now();
306
+ }
307
+ function compromiseLock(state, error) {
308
+ if (state.status !== 'active')
309
+ return;
310
+ state.status = 'compromised';
311
+ clearHeartbeat(state);
312
+ state.onCompromised(error);
313
+ }
314
+ function clearHeartbeat(state) {
315
+ if (!state.updateTimer)
316
+ return;
317
+ clearTimeout(state.updateTimer);
318
+ state.updateTimer = undefined;
319
+ }
320
+ function currentMtime(precision) {
321
+ const now = Date.now();
322
+ return new Date(precision === 'seconds' ? Math.ceil(now / 1_000) * 1_000 : now);
323
+ }
324
+ function createRetryDelays(retries) {
325
+ const options = typeof retries === 'number' ? { retries } : retries;
326
+ const count = Math.max(0, Math.floor(options?.retries ?? (options ? 10 : 0)));
327
+ const factor = options?.factor ?? 2;
328
+ const minTimeout = options?.minTimeout ?? 1_000;
329
+ const maxTimeout = options?.maxTimeout ?? Number.POSITIVE_INFINITY;
330
+ if (minTimeout > maxTimeout)
331
+ throw new Error('minTimeout is greater than maxTimeout');
332
+ return Array.from({ length: count }, (_, attempt) => {
333
+ const random = options?.randomize ? Math.random() + 1 : 1;
334
+ return Math.min(Math.round(random * minTimeout * factor ** attempt), maxTimeout);
335
+ }).sort((left, right) => left - right);
336
+ }
337
+ function lockHeldError(path) {
338
+ return Object.assign(new Error('Lock file is already being held'), { code: 'ELOCKED', file: path });
339
+ }
340
+ function compromisedError(message, cause) {
341
+ return Object.assign(new Error(message, cause === undefined ? undefined : { cause }), { code: 'ECOMPROMISED' });
342
+ }
343
+ function toCompromisedError(error) {
344
+ if (error instanceof Error)
345
+ return compromisedError(error.message, error);
346
+ return compromisedError('Unable to update lock within the stale threshold', error);
347
+ }
348
+ async function removeLockIfCurrent(environment, lockPath, identity) {
349
+ const current = await readLockIdentity(environment, lockPath);
350
+ if (!current || !sameLockIdentity(current, identity))
351
+ return;
352
+ const claimPath = lockClaimPath(lockPath, 'release');
353
+ try {
354
+ await environment.runtime.files.move(lockPath, claimPath);
355
+ }
356
+ catch (error) {
357
+ if (hasErrorCode(error, 'ENOENT'))
358
+ return;
359
+ throw error;
360
+ }
361
+ let claimed;
362
+ try {
363
+ claimed = await readLockIdentity(environment, claimPath);
364
+ }
365
+ catch (error) {
366
+ await restoreClaim(environment, claimPath, lockPath);
367
+ throw error;
368
+ }
369
+ if (!claimed || !sameLockIdentity(claimed, identity)) {
370
+ await restoreClaim(environment, claimPath, lockPath);
371
+ return;
372
+ }
373
+ try {
374
+ await environment.runtime.files.removeEmptyDirectory(claimPath);
375
+ }
376
+ catch (error) {
377
+ await restoreClaim(environment, claimPath, lockPath);
378
+ if (!hasErrorCode(error, 'ENOENT'))
379
+ throw error;
380
+ }
381
+ }
382
+ async function restoreClaim(environment, claimPath, lockPath) {
383
+ try {
384
+ await environment.runtime.files.inspect(lockPath);
385
+ return;
386
+ }
387
+ catch (error) {
388
+ if (!hasErrorCode(error, 'ENOENT'))
389
+ throw error;
390
+ }
391
+ try {
392
+ await environment.runtime.files.move(claimPath, lockPath);
393
+ }
394
+ catch (error) {
395
+ if (hasErrorCode(error, 'ENOENT') || hasErrorCode(error, 'EEXIST') || hasErrorCode(error, 'ENOTEMPTY'))
396
+ return;
397
+ throw error;
398
+ }
399
+ }
400
+ function lockClaimPath(lockPath, operation) {
401
+ return `${lockPath}.${operation}-${randomUUID()}`;
402
+ }
403
+ async function readLockIdentity(environment, path) {
404
+ try {
405
+ return lockIdentity(await environment.runtime.files.inspect(path));
406
+ }
407
+ catch (error) {
408
+ if (hasErrorCode(error, 'ENOENT'))
409
+ return undefined;
410
+ throw error;
411
+ }
412
+ }
413
+ function lockIdentity(stats) {
414
+ return { identity: stats.identity };
415
+ }
416
+ function sameLockIdentity(left, right) {
417
+ return left.identity === right.identity;
418
+ }
419
+ function hasErrorCode(error, code) {
420
+ return typeof error === 'object' && error !== null && Reflect.get(error, 'code') === code;
421
+ }
422
+ function isDirectoryNotEmpty(error) {
423
+ return hasErrorCode(error, 'ENOTEMPTY') || hasErrorCode(error, 'EEXIST');
424
+ }
425
+ function delay(milliseconds) {
426
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, milliseconds));
427
+ }
428
+ function setTimes(environment, path, time) {
429
+ return environment.runtime.files.touch(path, (time).getTime());
430
+ }
431
+ //# sourceMappingURL=lock.js.map