cloudaxe-opencode/packages/opencode/src/file/time.ts

72 lines
2.4 KiB
TypeScript
Raw Normal View History

import { Instance } from "../project/instance"
2025-07-04 21:57:48 +00:00
import { Log } from "../util/log"
import { Flag } from "../flag/flag"
import { Filesystem } from "../util/filesystem"
2025-05-19 23:29:38 +00:00
2025-06-27 15:29:20 +00:00
export namespace FileTime {
2025-07-04 21:57:48 +00:00
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.
2025-09-10 03:43:37 +00:00
export const state = Instance.state(() => {
const read: {
[sessionID: string]: {
[path: string]: Date | undefined
2025-06-03 00:24:32 +00:00
}
2025-09-10 03:43:37 +00:00
} = {}
const locks = new Map<string, Promise<void>>()
2025-09-10 03:43:37 +00:00
return {
read,
locks,
2025-09-10 03:43:37 +00:00
}
})
2025-05-19 23:29:38 +00:00
2025-06-03 00:24:32 +00:00
export function read(sessionID: string, file: string) {
2025-07-04 21:57:48 +00:00
log.info("read", { sessionID, file })
2025-06-03 00:24:32 +00:00
const { read } = state()
read[sessionID] = read[sessionID] || {}
read[sessionID][file] = new Date()
2025-05-19 23:29:38 +00:00
}
2025-06-03 00:24:32 +00:00
export function get(sessionID: string, file: string) {
return state().read[sessionID]?.[file]
2025-05-19 23:29:38 +00:00
}
2025-06-04 17:33:25 +00:00
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)
}
}
}
2025-06-04 17:33:25 +00:00
export async function assert(sessionID: string, filepath: string) {
if (Flag.OPENCODE_DISABLE_FILETIME_CHECK === true) {
return
}
2025-06-04 17:33:25 +00:00
const time = get(sessionID, filepath)
if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
const mtime = Filesystem.stat(filepath)?.mtime
// Allow a 50ms tolerance for Windows NTFS timestamp fuzziness / async flushing
if (mtime && mtime.getTime() > time.getTime() + 50) {
2025-06-04 17:33:25 +00:00
throw new Error(
`File ${filepath} has been modified since it was last read.\nLast modification: ${mtime.toISOString()}\nLast read: ${time.toISOString()}\n\nPlease read the file again before modifying it.`,
2025-06-04 17:33:25 +00:00
)
}
}
2025-05-19 23:29:38 +00:00
}