| 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 | */ |
| 12 | import * as Cache from "../Cache.ts" |
| 13 | import * as Duration from "../Duration.ts" |
| 14 | import * as Effect from "../Effect.ts" |
| 15 | import type { Exit } from "../Exit.ts" |
| 16 | import { constant, identity } from "../Function.ts" |
| 17 | import type * as Schema from "../Schema.ts" |
| 18 | import type * as Scope from "../Scope.ts" |
| 19 | import type * as Persistable from "./Persistable.ts" |
| 20 | import * as Persistence from "./Persistence.ts" |
| 21 | |
| 22 | const 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 | */ |
| 31 | export 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 | */ |
| 60 | export 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 | }) |