1import { ThreadId } from "@t3tools/contracts";
2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Schema from "effect/Schema";
7
8import { resolveAttachmentPathById } from "../attachmentStore.ts";
9import * as ServerConfig from "../config.ts";
10import * as PreviewManager from "../preview/Manager.ts";
11import * as TerminalManager from "../terminal/Manager.ts";
12
13export class ResourceCleanupError extends Schema.TaggedError<ResourceCleanupError>()(
14 "ResourceCleanupError",
15 {
16 operation: Schema.Literals(["terminal", "preview", "attachment"]),
17 threadId: Schema.optional(Schema.String),
18 attachmentId: Schema.optional(Schema.String),
19 cause: Schema.Defect(),
20 },
21) {}
22
23export class ResourceCleanupService extends Context.Reference<{
24 readonly cleanupTerminals: (threadId: string) => Effect.Effect<void, ResourceCleanupError>;
25 /** Closes every preview session of the thread; server browser tabs end with them. */
26 readonly cleanupPreviews: (threadId: string) => Effect.Effect<void, ResourceCleanupError>;
27 readonly cleanupAttachments: (
28 attachmentIds: ReadonlyArray<string>,
29 ) => Effect.Effect<void, ResourceCleanupError>;
30}>("t3/orchestration-v2/ResourceCleanupService", {
31 defaultValue: () => ({
32 cleanupTerminals: () => Effect.void,
33 cleanupPreviews: () => Effect.void,
34 cleanupAttachments: () => Effect.void,
35 }),
36}) {}
37
38export const layer = Layer.effect(
39 ResourceCleanupService,
40 Effect.gen(function* () {
41 const terminals = yield* TerminalManager.TerminalManager;
42 const previews = yield* PreviewManager.PreviewManager;
43 const fileSystem = yield* FileSystem.FileSystem;
44 const config = yield* ServerConfig.ServerConfig;
45 return {
46 cleanupTerminals: (threadId: string) =>
47 terminals
48 .close({ threadId, deleteHistory: true })
49 .pipe(
50 Effect.mapError(
51 (cause) => new ResourceCleanupError({ operation: "terminal", threadId, cause }),
52 ),
53 ),
54 cleanupPreviews: (threadId: string) =>
55 previews
56 .close({ threadId: ThreadId.make(threadId) })
57 .pipe(
58 Effect.mapError(
59 (cause) => new ResourceCleanupError({ operation: "preview", threadId, cause }),
60 ),
61 ),
62 cleanupAttachments: (attachmentIds: ReadonlyArray<string>) =>
63 Effect.forEach(
64 attachmentIds,
65 (attachmentId) => {
66 const path = resolveAttachmentPathById({
67 attachmentsDir: config.attachmentsDir,
68 attachmentId,
69 });
70 return path === null
71 ? Effect.void
72 : fileSystem
73 .remove(path, { force: true })
74 .pipe(
75 Effect.mapError(
76 (cause) =>
77 new ResourceCleanupError({ operation: "attachment", attachmentId, cause }),
78 ),
79 );
80 },
81 { discard: true, concurrency: 4 },
82 ),
83 };
84 }),
85);