-
Notifications
You must be signed in to change notification settings - Fork 212
Expand file tree
/
Copy pathTaskHistoryLock.ts
More file actions
100 lines (87 loc) · 2.88 KB
/
Copy pathTaskHistoryLock.ts
File metadata and controls
100 lines (87 loc) · 2.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import * as fs from "fs/promises"
import * as path from "path"
import * as lockfile from "proper-lockfile"
import { GlobalFileNames } from "../../shared/globalFileNames"
import { getStorageBasePath } from "../../utils/storage"
/**
* Cross-process lock for task history mutations.
*
* Multiple `ClineProvider` instances may live in separate extension-host
* processes (for example VS Code windows) while sharing the same task history
* storage. Each process has its own `TaskHistoryStore`, so an in-memory mutex is
* not sufficient. This lock serializes mutations by taking an exclusive advisory
* lock on the shared `tasks/_history.lock` file.
*/
export class TaskHistoryLock {
private queue: Promise<unknown> = Promise.resolve()
/**
* Acquires the shared task-history lock and executes `fn` while holding it.
*
* The lock file is scoped to the effective storage root (including custom
* storage path resolution) so all windows/processes targeting the same history
* store contend on the same file.
*/
async withLock<T>(globalStoragePath: string, fn: () => Promise<T>): Promise<T> {
const result = this.queue.then(
async () => {
const lockFilePath = await this.getLockFilePath(globalStoragePath)
return this.runWithFileLock(lockFilePath, fn)
},
async () => {
const lockFilePath = await this.getLockFilePath(globalStoragePath)
return this.runWithFileLock(lockFilePath, fn)
},
)
this.queue = result.then(
() => undefined,
() => undefined,
)
return result
}
/**
* Clears in-process queues. File locks held by other processes are not affected.
*/
reset(): void {
this.queue = Promise.resolve()
}
async getLockFilePath(globalStoragePath: string): Promise<string> {
const basePath = await getStorageBasePath(globalStoragePath)
const tasksDir = path.join(basePath, "tasks")
await fs.mkdir(tasksDir, { recursive: true })
const lockFilePath = path.join(tasksDir, GlobalFileNames.historyLock)
try {
await fs.open(lockFilePath, "a").then((handle) => handle.close())
} catch (error) {
console.error(`[TaskHistoryLock] Failed to create lock file at ${lockFilePath}:`, error)
throw error
}
return lockFilePath
}
private async runWithFileLock<T>(lockFilePath: string, fn: () => Promise<T>): Promise<T> {
let releaseLock: (() => Promise<void>) | undefined
try {
releaseLock = await lockfile.lock(lockFilePath, {
stale: 31000,
update: 10000,
realpath: false,
retries: {
retries: 10,
factor: 2,
minTimeout: 50,
maxTimeout: 1000,
},
onCompromised: (err) => {
console.error(`[TaskHistoryLock] Lock at ${lockFilePath} was compromised:`, err)
throw err
},
})
return await fn()
} finally {
if (releaseLock) {
await releaseLock()
}
}
}
}
// Singleton instance shared across all ClineProvider instances in this process.
export const taskHistoryLock = new TaskHistoryLock()