1/**
2 * Combines in-memory caching with durable storage for `Persistable` requests.
3 *
4 * A `PersistedCache` checks a process-local `Cache`, then a named `Persistence`
5 * store, before running the supplied lookup. It stores the lookup `Exit`, so
6 * expensive or idempotent results can be reused across fibers, process restarts,
7 * or workers that share the same backing store.
8 *
9 * @stability unstable
10 * @since 4.0.0
11 */
12import * as Cache from "../Cache.ts"
13import * as Duration from "../Duration.ts"
14import * as Effect from "../Effect.ts"
15import type { Exit } from "../Exit.ts"
16import { constant, identity } from "../Function.ts"
17import type * as Schema from "../Schema.ts"
18import type * as Scope from "../Scope.ts"
19import type * as Persistable from "./Persistable.ts"
20import * as Persistence from "./Persistence.ts"
21
22const TypeId = "~effect/persistence/PersistedCache" as const
23
24/**
25 * Cache that combines an in-memory `Cache` with a persisted backing store.
26 *
27 * @stability unstable
28 * @category models
29 * @since 4.0.0
30 */
31export interface PersistedCache<K extends Persistable.Any, out R = never> {
32 readonly [TypeId]: typeof TypeId
33 readonly inMemory: Cache.Cache<
34 K,
35 Persistable.Success<K>,
36 Persistable.Error<K> | Persistence.PersistenceError | Schema.SchemaError,
37 Persistable.Services<K> | R
38 >
39 readonly get: (key: K) => Effect.Effect<
40 Persistable.Success<K>,
41 Persistable.Error<K> | Persistence.PersistenceError | Schema.SchemaError,
42 Persistable.Services<K> | R
43 >
44 readonly invalidate: (key: K) => Effect.Effect<void, Persistence.PersistenceError>
45}
46
47/**
48 * Creates a persisted cache for `Persistable` request keys.
49 *
50 * **Details**
51 *
52 * The cache reads persisted exits before running the lookup, stores lookup
53 * exits with the configured persistent TTL, and also keeps a scoped in-memory
54 * cache with its own capacity and TTL.
55 *
56 * @stability unstable
57 * @category constructors
58 * @since 4.0.0
59 */
60export const make: <
61 K extends Persistable.Any,
62 R = never,
63 ServiceMode extends "lookup" | "construction" = never
64>(lookup: (key: K) => Effect.Effect<Persistable.Success<K>, Persistable.Error<K>, R>, options: {
65 readonly storeId: string
66 readonly timeToLive: Persistable.TimeToLiveFn<K>
67 readonly inMemoryCapacity?: number | undefined
68 readonly inMemoryTTL?: Persistable.TimeToLiveFn<K> | undefined
69 readonly requireServicesAt?: ServiceMode | undefined
70}) => Effect.Effect<
71 PersistedCache<K, "lookup" extends ServiceMode ? R : never>,
72 never,
73 ("lookup" extends ServiceMode ? never : R) | Persistence.Persistence | Scope.Scope
74> = Effect.fnUntraced(function*<
75 K extends Persistable.Any,
76 R = never,
77 ServiceMode extends "lookup" | "construction" = never
78>(
79 lookup: (key: K) => Effect.Effect<Persistable.Success<K>, Persistable.Error<K>, R>,
80 options: {
81 readonly storeId: string
82 readonly timeToLive: Persistable.TimeToLiveFn<K>
83 readonly inMemoryCapacity?: number | undefined
84 readonly inMemoryTTL?: Persistable.TimeToLiveFn<K> | undefined
85 readonly requireServicesAt?: ServiceMode | undefined
86 }
87) {
88 const store = yield* (yield* Persistence.Persistence).make({
89 storeId: options.storeId,
90 timeToLive: options.timeToLive as any
91 })
92 const inMemory = yield* Cache.makeWith(
93 Effect.fnUntraced(function*(key: K) {
94 const exit = yield* (store.get(key) as Effect.Effect<Exit<Persistable.Success<K>, Persistable.Error<K>>>)
95 if (exit) {
96 return yield* exit
97 }
98 const result = yield* Effect.exit(Effect.suspend(() => lookup(key)))
99 yield* (store.set(key, result) as Effect.Effect<void>)
100 return yield* result
101 }),
102 {
103 timeToLive: options.inMemoryTTL ?? constant(Duration.seconds(10)),
104 capacity: options.inMemoryCapacity ?? 1024,
105 requireServicesAt: options.requireServicesAt
106 }
107 )
108 return identity<PersistedCache<K, "lookup" extends ServiceMode ? R : never>>({
109 [TypeId]: TypeId,
110 inMemory,
111 get: (key) => Cache.get(inMemory, key),
112 invalidate: (key) => Effect.flatMap(store.remove(key), () => Cache.invalidate(inMemory, key))
113 })
114})