| 1 | import * as Arr from "effect/Array"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; |
| 4 | import * as Chat from "effect/ai/Chat"; |
| 5 | import { |
| 6 | BackingPersistence, |
| 7 | PersistenceError, |
| 8 | type BackingPersistenceStore, |
| 9 | } from "effect/persistence/Persistence"; |
| 10 | import { RuntimeContext } from "../../RuntimeContext.ts"; |
| 11 | import { DurableObjectState } from "../Workers/DurableObjectState.ts"; |
| 12 | |
| 13 | export 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 | |
| 86 | export const layerChatDurableObject = Chat.layerPersisted({ |
| 87 | storeId: "alchemy.chat", |
| 88 | }).pipe(Layer.provide(DurableObjectChatPersistence)); |