import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import { resolveAttachmentPathById } from "../attachmentStore.ts"; import * as ServerConfig from "../config.ts"; import * as TerminalManager from "../terminal/Manager.ts"; export class ResourceCleanupError extends Schema.TaggedErrorClass()( "ResourceCleanupError", { operation: Schema.Literals(["terminal", "attachment"]), threadId: Schema.optional(Schema.String), attachmentId: Schema.optional(Schema.String), cause: Schema.Defect(), }, ) {} export class ResourceCleanupService extends Context.Reference<{ readonly cleanupTerminals: (threadId: string) => Effect.Effect; readonly cleanupAttachments: ( attachmentIds: ReadonlyArray, ) => Effect.Effect; }>("t3/orchestration-v2/ResourceCleanupService", { defaultValue: () => ({ cleanupTerminals: () => Effect.void, cleanupAttachments: () => Effect.void, }), }) {} export const live = Layer.effect( ResourceCleanupService, Effect.gen(function* () { const terminals = yield* TerminalManager.TerminalManager; const fileSystem = yield* FileSystem.FileSystem; const config = yield* ServerConfig.ServerConfig; return { cleanupTerminals: (threadId: string) => terminals .close({ threadId, deleteHistory: true }) .pipe( Effect.mapError( (cause) => new ResourceCleanupError({ operation: "terminal", threadId, cause }), ), ), cleanupAttachments: (attachmentIds: ReadonlyArray) => Effect.forEach( attachmentIds, (attachmentId) => { const path = resolveAttachmentPathById({ attachmentsDir: config.attachmentsDir, attachmentId, }); return path === null ? Effect.void : fileSystem .remove(path, { force: true }) .pipe( Effect.mapError( (cause) => new ResourceCleanupError({ operation: "attachment", attachmentId, cause }), ), ); }, { discard: true, concurrency: 4 }, ), }; }), );