|
|
@@ -3,14 +3,20 @@ import { Log } from "../util/log"
|
|
|
|
|
|
export namespace FileTime {
|
|
|
const log = Log.create({ service: "file.time" })
|
|
|
+ // Per-session read times plus per-file write locks.
|
|
|
+ // All tools that overwrite existing files should run their
|
|
|
+ // assert/read/write/update sequence inside withLock(filepath, ...)
|
|
|
+ // so concurrent writes to the same file are serialized.
|
|
|
export const state = Instance.state(() => {
|
|
|
const read: {
|
|
|
[sessionID: string]: {
|
|
|
[path: string]: Date | undefined
|
|
|
}
|
|
|
} = {}
|
|
|
+ const locks = new Map<string, Promise<void>>()
|
|
|
return {
|
|
|
read,
|
|
|
+ locks,
|
|
|
}
|
|
|
})
|
|
|
|
|
|
@@ -25,6 +31,26 @@ export namespace FileTime {
|
|
|
return state().read[sessionID]?.[file]
|
|
|
}
|
|
|
|
|
|
+ export async function withLock<T>(filepath: string, fn: () => Promise<T>): Promise<T> {
|
|
|
+ const current = state()
|
|
|
+ const currentLock = current.locks.get(filepath) ?? Promise.resolve()
|
|
|
+ let release: () => void = () => {}
|
|
|
+ const nextLock = new Promise<void>((resolve) => {
|
|
|
+ release = resolve
|
|
|
+ })
|
|
|
+ const chained = currentLock.then(() => nextLock)
|
|
|
+ current.locks.set(filepath, chained)
|
|
|
+ await currentLock
|
|
|
+ try {
|
|
|
+ return await fn()
|
|
|
+ } finally {
|
|
|
+ release()
|
|
|
+ if (current.locks.get(filepath) === chained) {
|
|
|
+ current.locks.delete(filepath)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
export async function assert(sessionID: string, filepath: string) {
|
|
|
const time = get(sessionID, filepath)
|
|
|
if (!time) throw new Error(`You must read the file ${filepath} before overwriting it. Use the Read tool first`)
|