instance.ts 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import { Log } from "@/util/log"
  2. import { Context } from "../util/context"
  3. import { Project } from "./project"
  4. import { State } from "./state"
  5. import { iife } from "@/util/iife"
  6. import { GlobalBus } from "@/bus/global"
  7. import { Filesystem } from "@/util/filesystem"
  8. interface Context {
  9. directory: string
  10. worktree: string
  11. project: Project.Info
  12. }
  13. const context = Context.create<Context>("instance")
  14. const cache = new Map<string, Promise<Context>>()
  15. export const Instance = {
  16. async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
  17. let existing = cache.get(input.directory)
  18. if (!existing) {
  19. Log.Default.info("creating instance", { directory: input.directory })
  20. existing = iife(async () => {
  21. const { project, sandbox } = await Project.fromDirectory(input.directory)
  22. const ctx = {
  23. directory: input.directory,
  24. worktree: sandbox,
  25. project,
  26. }
  27. await context.provide(ctx, async () => {
  28. await input.init?.()
  29. })
  30. return ctx
  31. })
  32. cache.set(input.directory, existing)
  33. }
  34. const ctx = await existing
  35. return context.provide(ctx, async () => {
  36. return input.fn()
  37. })
  38. },
  39. get directory() {
  40. return context.use().directory
  41. },
  42. get worktree() {
  43. return context.use().worktree
  44. },
  45. get project() {
  46. return context.use().project
  47. },
  48. /**
  49. * Check if a path is within the project boundary.
  50. * Returns true if path is inside Instance.directory OR Instance.worktree.
  51. * Paths within the worktree but outside the working directory should not trigger external_directory permission.
  52. */
  53. containsPath(filepath: string) {
  54. if (Filesystem.contains(Instance.directory, filepath)) return true
  55. // Non-git projects set worktree to "/" which would match ANY absolute path.
  56. // Skip worktree check in this case to preserve external_directory permissions.
  57. if (Instance.worktree === "/") return false
  58. return Filesystem.contains(Instance.worktree, filepath)
  59. },
  60. state<S>(init: () => S, dispose?: (state: Awaited<S>) => Promise<void>): () => S {
  61. return State.create(() => Instance.directory, init, dispose)
  62. },
  63. async dispose() {
  64. Log.Default.info("disposing instance", { directory: Instance.directory })
  65. await State.dispose(Instance.directory)
  66. cache.delete(Instance.directory)
  67. GlobalBus.emit("event", {
  68. directory: Instance.directory,
  69. payload: {
  70. type: "server.instance.disposed",
  71. properties: {
  72. directory: Instance.directory,
  73. },
  74. },
  75. })
  76. },
  77. async disposeAll() {
  78. Log.Default.info("disposing all instances")
  79. for (const [_key, value] of cache) {
  80. const awaited = await value.catch(() => {})
  81. if (awaited) {
  82. await context.provide(await value, async () => {
  83. await Instance.dispose()
  84. })
  85. }
  86. }
  87. cache.clear()
  88. },
  89. }