1import { assert, it } from "@effect/vitest"
2import type { Vitest } from "@effect/vitest"
3import type { Duration, Layer } from "effect"
4import { Context, Data, Effect, Exit, Schema } from "effect"
5import { Persistable, PersistedCache, Persistence } from "effect/persistence"
6
7class User extends Schema.Class<User>("User")({
8 id: Schema.Number,
9 name: Schema.String
10}) {}
11
12class 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
20export class TransientError extends Data.TaggedError("TransientError") {}
21
22class LookupService extends Context.Service<LookupService, { readonly value: string }>()("LookupService") {}
23
24export 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
103export const suite = (storeId: string, layer: Layer.Layer<Persistence.Persistence, unknown>) =>
104 suiteWith(storeId, layer, it)