1import * as Arr from "effect/Array";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Chat from "effect/ai/Chat";
5import {
6 BackingPersistence,
7 PersistenceError,
8 type BackingPersistenceStore,
9} from "effect/persistence/Persistence";
10import { RuntimeContext } from "../../RuntimeContext.ts";
11import { DurableObjectState } from "../Workers/DurableObjectState.ts";
12
13export const DurableObjectChatPersistence = Layer.effect(BackingPersistence)(
14 Effect.gen(function* () {
15 const state = yield* DurableObjectState;
16 const storage = state.storage;
17
18 const wrapErr = (op: string, key?: string) => (cause: unknown) =>
19 new PersistenceError({
20 message: `Failed to ${op}${key !== undefined ? ` key ${key}` : ""} in DurableObject storage`,
21 cause,
22 });
23
24 return BackingPersistence.of({
25 make: (storeId) =>
26 Effect.sync(() => {
27 const prefixed = (k: string) => `${storeId}:${k}`;
28 return {
29 get: (key) =>
30 storage
31 .get<object>(prefixed(key))
32 .pipe(
33 Effect.mapError(wrapErr("get", key)),
34 Effect.provide(RuntimeContext.phantom),
35 ),
36 getMany: (keys) =>
37 storage.get<object>(keys.map(prefixed)).pipe(
38 Effect.mapError(wrapErr("getMany")),
39 Effect.map(
40 (map) =>
41 keys.map((k) => map.get(prefixed(k))) as Arr.NonEmptyArray<
42 object | undefined
43 >,
44 ),
45 Effect.provide(RuntimeContext.phantom),
46 ),
47 set: (key, value, _ttl) =>
48 storage
49 .put(prefixed(key), value)
50 .pipe(
51 Effect.mapError(wrapErr("set", key)),
52 Effect.provide(RuntimeContext.phantom),
53 ),
54 setMany: (entries) =>
55 storage
56 .put(
57 Object.fromEntries(entries.map(([k, v]) => [prefixed(k), v])),
58 )
59 .pipe(
60 Effect.mapError(wrapErr("setMany")),
61 Effect.provide(RuntimeContext.phantom),
62 ),
63 remove: (key) =>
64 storage
65 .delete(prefixed(key))
66 .pipe(
67 Effect.asVoid,
68 Effect.mapError(wrapErr("remove", key)),
69 Effect.provide(RuntimeContext.phantom),
70 ),
71 clear: storage.list({ prefix: `${storeId}:` }).pipe(
72 Effect.flatMap((map) => {
73 const ks = [...map.keys()];
74 if (ks.length === 0) return Effect.void;
75 return Effect.asVoid(storage.delete(ks));
76 }),
77 Effect.mapError(wrapErr("clear")),
78 Effect.provide(RuntimeContext.phantom),
79 ),
80 } satisfies BackingPersistenceStore;
81 }),
82 });
83 }),
84);
85
86export const layerChatDurableObject = Chat.layerPersisted({
87 storeId: "alchemy.chat",
88}).pipe(Layer.provide(DurableObjectChatPersistence));