| 1 | import { assert, it } from "@effect/vitest" |
| 2 | import type { Vitest } from "@effect/vitest" |
| 3 | import type { Duration, Layer } from "effect" |
| 4 | import { Context, Data, Effect, Exit, Schema } from "effect" |
| 5 | import { Persistable, PersistedCache, Persistence } from "effect/persistence" |
| 6 | |
| 7 | class User extends Schema.Class<User>("User")({ |
| 8 | id: Schema.Number, |
| 9 | name: Schema.String |
| 10 | }) {} |
| 11 | |
| 12 | class TTLRequest extends Persistable.Class<{ |
| 13 | payload: { id: number } |
| 14 | }>()("TTLRequest", { |
| 15 | primaryKey: (req) => `TTLRequest:${req.id}`, |
| 16 | success: User, |
| 17 | error: Schema.String |
| 18 | }) {} |
| 19 | |
| 20 | export class TransientError extends Data.TaggedError("TransientError") {} |
| 21 | |
| 22 | class LookupService extends Context.Service<LookupService, { readonly value: string }>()("LookupService") {} |
| 23 | |
| 24 | export const suiteWith = <R>( |
| 25 | storeId: string, |
| 26 | layer: Layer.Layer<Persistence.Persistence, unknown, R>, |
| 27 | testApi: Vitest.MethodsNonLive<R>, |
| 28 | timeout: Duration.Input = "60 seconds" |
| 29 | ) => |
| 30 | testApi.layer(layer, { timeout })(`PersistedCache (${storeId})`, (it) => { |
| 31 | it.effect("smoke test", () => |
| 32 | Effect.gen(function*() { |
| 33 | const persistence = yield* Persistence.Persistence |
| 34 | const store = yield* persistence.make({ storeId: "users" }) |
| 35 | let invocations = 0 |
| 36 | let cache = yield* PersistedCache.make((req: TTLRequest) => |
| 37 | Effect.sync(() => { |
| 38 | invocations++ |
| 39 | return new User({ id: req.id, name: "John" }) |
| 40 | }), { |
| 41 | storeId: "users", |
| 42 | timeToLive: () => 5000 |
| 43 | }) |
| 44 | const user = yield* cache.get(new TTLRequest({ id: 1 })) |
| 45 | assert.deepStrictEqual(user, new User({ id: 1, name: "John" })) |
| 46 | assert.deepStrictEqual( |
| 47 | yield* store.get(new TTLRequest({ id: 1 })), |
| 48 | Exit.succeed(new User({ id: 1, name: "John" })) |
| 49 | ) |
| 50 | assert.strictEqual(invocations, 1) |
| 51 | assert.deepStrictEqual(yield* cache.get(new TTLRequest({ id: 1 })), new User({ id: 1, name: "John" })) |
| 52 | assert.strictEqual(invocations, 1) |
| 53 | |
| 54 | cache = yield* PersistedCache.make((req: TTLRequest) => |
| 55 | Effect.sync(() => { |
| 56 | invocations++ |
| 57 | return new User({ id: req.id, name: "John" }) |
| 58 | }), { |
| 59 | storeId: "users", |
| 60 | timeToLive: (_req, _exit) => 5000 |
| 61 | }) |
| 62 | assert.deepStrictEqual(yield* cache.get(new TTLRequest({ id: 1 })), new User({ id: 1, name: "John" })) |
| 63 | assert.strictEqual(invocations, 1) |
| 64 | |
| 65 | yield* store.setMany([ |
| 66 | [new TTLRequest({ id: 1 }), Exit.fail("Not found")], |
| 67 | [new TTLRequest({ id: 2 }), Exit.succeed(new User({ id: 2, name: "Jane" }))] |
| 68 | ]) |
| 69 | const results = yield* store.getMany([new TTLRequest({ id: 1 }), new TTLRequest({ id: 2 })]) |
| 70 | assert.deepStrictEqual(results, [ |
| 71 | Exit.fail("Not found"), |
| 72 | Exit.succeed(new User({ id: 2, name: "Jane" })) |
| 73 | ]) |
| 74 | }), 30_000) |
| 75 | |
| 76 | it.effect("requireServicesAt: 'lookup' requires lookup services at get-time", () => |
| 77 | Effect.gen(function*() { |
| 78 | const cache = yield* PersistedCache.make( |
| 79 | (req: TTLRequest) => Effect.map(LookupService, (service) => new User({ id: req.id, name: service.value })), |
| 80 | { |
| 81 | storeId: `${storeId}-require-services-at`, |
| 82 | timeToLive: () => 5000, |
| 83 | requireServicesAt: "lookup" |
| 84 | } |
| 85 | ) |
| 86 | |
| 87 | const result1 = yield* cache.get(new TTLRequest({ id: 1 })).pipe( |
| 88 | Effect.provideService(LookupService, LookupService.of({ value: "first" })) |
| 89 | ) |
| 90 | const result2 = yield* cache.get(new TTLRequest({ id: 2 })).pipe( |
| 91 | Effect.provideService(LookupService, LookupService.of({ value: "second" })) |
| 92 | ) |
| 93 | const result3 = yield* cache.get(new TTLRequest({ id: 1 })).pipe( |
| 94 | Effect.provideService(LookupService, LookupService.of({ value: "third" })) |
| 95 | ) |
| 96 | |
| 97 | assert.deepStrictEqual(result1, new User({ id: 1, name: "first" })) |
| 98 | assert.deepStrictEqual(result2, new User({ id: 2, name: "second" })) |
| 99 | assert.deepStrictEqual(result3, new User({ id: 1, name: "first" })) |
| 100 | }), 30_000) |
| 101 | }) |
| 102 | |
| 103 | export const suite = (storeId: string, layer: Layer.Layer<Persistence.Persistence, unknown>) => |
| 104 | suiteWith(storeId, layer, it) |