time.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import { DateTime, Effect, Layer, Option, Semaphore, Context } from "effect"
  2. import { InstanceState } from "@/effect/instance-state"
  3. import { AppFileSystem } from "@/filesystem"
  4. import { Flag } from "@/flag/flag"
  5. import type { SessionID } from "@/session/schema"
  6. import { Filesystem } from "@/util/filesystem"
  7. import { Log } from "../util/log"
  8. export namespace FileTime {
  9. const log = Log.create({ service: "file.time" })
  10. export type Stamp = {
  11. readonly read: Date
  12. readonly mtime: number | undefined
  13. readonly size: number | undefined
  14. }
  15. const session = (reads: Map<SessionID, Map<string, Stamp>>, sessionID: SessionID) => {
  16. const value = reads.get(sessionID)
  17. if (value) return value
  18. const next = new Map<string, Stamp>()
  19. reads.set(sessionID, next)
  20. return next
  21. }
  22. interface State {
  23. reads: Map<SessionID, Map<string, Stamp>>
  24. locks: Map<string, Semaphore.Semaphore>
  25. }
  26. export interface Interface {
  27. readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void>
  28. readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined>
  29. readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void>
  30. readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T>
  31. }
  32. export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {}
  33. export const layer = Layer.effect(
  34. Service,
  35. Effect.gen(function* () {
  36. const fsys = yield* AppFileSystem.Service
  37. const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
  38. const stamp = Effect.fnUntraced(function* (file: string) {
  39. const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.void))
  40. return {
  41. read: yield* DateTime.nowAsDate,
  42. mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
  43. size: info ? Number(info.size) : undefined,
  44. }
  45. })
  46. const state = yield* InstanceState.make<State>(
  47. Effect.fn("FileTime.state")(() =>
  48. Effect.succeed({
  49. reads: new Map<SessionID, Map<string, Stamp>>(),
  50. locks: new Map<string, Semaphore.Semaphore>(),
  51. }),
  52. ),
  53. )
  54. const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) {
  55. filepath = Filesystem.normalizePath(filepath)
  56. const locks = (yield* InstanceState.get(state)).locks
  57. const lock = locks.get(filepath)
  58. if (lock) return lock
  59. const next = Semaphore.makeUnsafe(1)
  60. locks.set(filepath, next)
  61. return next
  62. })
  63. const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) {
  64. file = Filesystem.normalizePath(file)
  65. const reads = (yield* InstanceState.get(state)).reads
  66. log.info("read", { sessionID, file })
  67. session(reads, sessionID).set(file, yield* stamp(file))
  68. })
  69. const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) {
  70. file = Filesystem.normalizePath(file)
  71. const reads = (yield* InstanceState.get(state)).reads
  72. return reads.get(sessionID)?.get(file)?.read
  73. })
  74. const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) {
  75. if (disableCheck) return
  76. filepath = Filesystem.normalizePath(filepath)
  77. const reads = (yield* InstanceState.get(state)).reads
  78. const time = reads.get(sessionID)?.get(filepath)
  79. if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
  80. const next = yield* stamp(filepath)
  81. const changed = next.mtime !== time.mtime || next.size !== time.size
  82. if (!changed) return
  83. throw new Error(
  84. `File ${filepath} has been modified since it was last read.\nLast modification: ${new Date(next.mtime ?? next.read.getTime()).toISOString()}\nLast read: ${time.read.toISOString()}\n\nPlease read the file again before modifying it.`,
  85. )
  86. })
  87. const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Effect.Effect<T>) {
  88. return yield* fn().pipe((yield* getLock(filepath)).withPermits(1))
  89. })
  90. return Service.of({ read, get, assert, withLock })
  91. }),
  92. ).pipe(Layer.orDie)
  93. export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
  94. }