Skip to content
Source
Sign inSign up

staleWhileRevalidate.test.ts

177 lines6.1 KB

1import { assert, it } from "@effect/vitest";
2import * as Deferred from "effect/Deferred";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
5import * as Fiber from "effect/Fiber";
6import * as Ref from "effect/Ref";
7import * as Scope from "effect/Scope";
8import * as TestClock from "effect/testing/TestClock";
9​
10import { makeStaleWhileRevalidate } from "./staleWhileRevalidate.ts";
11​
12it.effect("computes once, then answers from memory until the ttl lapses", () =>
13 Effect.gen(function* () {
14 const calls = yield* Ref.make(0);
15 const read = yield* makeStaleWhileRevalidate(
16 Ref.updateAndGet(calls, (count) => count + 1),
17 "60 seconds",
18 );
19​
20 assert.equal(yield* read, 1);
21 yield* TestClock.adjust("59 seconds");
22 assert.equal(yield* read, 1);
23 assert.equal(yield* Ref.get(calls), 1);
24 }),
25);
26​
27it.effect("serves the stale value while one background refresh replaces it", () =>
28 Effect.gen(function* () {
29 const calls = yield* Ref.make(0);
30 const release = yield* Deferred.make<void>();
31 const started = yield* Deferred.make<void>();
32 const refreshed = yield* Deferred.make<Fiber.Fiber<unknown, unknown>>();
33 const read = yield* makeStaleWhileRevalidate(
34 Effect.gen(function* () {
35 const call = yield* Ref.updateAndGet(calls, (count) => count + 1);
36 if (call > 1) {
37 yield* Deferred.succeed(started, undefined);
38 yield* Deferred.await(release);
39 yield* Deferred.succeed(refreshed, yield* Effect.fiber);
40 }
41 return call;
42 }),
43 "60 seconds",
44 );
45​
46 assert.equal(yield* read, 1);
47 yield* TestClock.adjust("61 seconds");
48 // The refresh is blocked, yet callers are answered immediately and only
49 // one refresh starts however many callers arrive.
50 assert.equal(yield* read, 1);
51 assert.equal(yield* read, 1);
52 yield* Deferred.await(started);
53 assert.equal(yield* Ref.get(calls), 2);
54​
55 yield* Deferred.succeed(release, undefined);
56 yield* Fiber.await(yield* Deferred.await(refreshed));
57 assert.equal(yield* read, 2);
58 }),
59);
60​
61it.effect("shares one first scan that an interrupted caller does not cancel", () =>
62 Effect.gen(function* () {
63 const calls = yield* Ref.make(0);
64 const release = yield* Deferred.make<void>();
65 const started = yield* Deferred.make<void>();
66 const read = yield* makeStaleWhileRevalidate(
67 Ref.updateAndGet(calls, (count) => count + 1).pipe(
68 Effect.tap(() => Deferred.succeed(started, undefined)),
69 Effect.tap(() => Deferred.await(release)),
70 ),
71 "60 seconds",
72 );
73​
74 // A connect that times out or disconnects mid-scan leaves the scan running.
75 const interrupted = yield* read.pipe(Effect.forkChild);
76 yield* Deferred.await(started);
77 yield* Fiber.interrupt(interrupted);
78​
79 const next = yield* read.pipe(Effect.forkChild);
80 yield* Deferred.succeed(release, undefined);
81 assert.equal(yield* Fiber.join(next), 1);
82 assert.equal(yield* Ref.get(calls), 1);
83 }),
84);
85​
86it.effect("starts over after a failed first scan", () =>
87 Effect.gen(function* () {
88 const calls = yield* Ref.make(0);
89 const read = yield* makeStaleWhileRevalidate(
90 Ref.updateAndGet(calls, (count) => count + 1).pipe(
91 Effect.filterOrElse(
92 (call) => call !== 1,
93 () => Effect.die("probe crashed"),
94 ),
95 ),
96 "60 seconds",
97 );
98​
99 assert.isTrue(Exit.isFailure(yield* Effect.exit(read)));
100 assert.equal(yield* read, 2);
101 }),
102);
103​
104it.effect("abandons a hung refresh so a later call can start another", () =>
105 Effect.gen(function* () {
106 const calls = yield* Ref.make(0);
107 const started = yield* Deferred.make<Fiber.Fiber<unknown, unknown>>();
108 const interrupted = yield* Deferred.make<void>();
109 const refreshed = yield* Deferred.make<Fiber.Fiber<unknown, unknown>>();
110 const read = yield* makeStaleWhileRevalidate(
111 Ref.updateAndGet(calls, (count) => count + 1).pipe(
112 Effect.flatMap((call) =>
113 call === 2
114 ? Effect.fiber.pipe(
115 Effect.flatMap((fiber) => Deferred.succeed(started, fiber)),
116 Effect.andThen(Effect.never),
117 Effect.onInterrupt(() => Deferred.succeed(interrupted, undefined)),
118 )
119 : Effect.succeed(call).pipe(
120 Effect.tap(() =>
121 call === 3
122 ? Effect.fiber.pipe(
123 Effect.flatMap((fiber) => Deferred.succeed(refreshed, fiber)),
124 )
125 : Effect.void,
126 ),
127 ),
128 ),
129 ),
130 "60 seconds",
131 );
132​
133 assert.equal(yield* read, 1);
134 yield* TestClock.adjust("61 seconds");
135 assert.equal(yield* read, 1);
136 yield* Deferred.await(started);
137 assert.equal(yield* Ref.get(calls), 2);
138​
139 // The hung refresh times out, releasing the claim for the next caller.
140 yield* TestClock.adjust("30 seconds");
141 yield* Deferred.await(interrupted);
142 yield* Fiber.await(yield* Deferred.await(started));
143 assert.equal(yield* read, 1);
144 yield* Fiber.await(yield* Deferred.await(refreshed));
145 assert.equal(yield* Ref.get(calls), 3);
146 assert.equal(yield* read, 3);
147 }),
148);
149​
150it.effect("interrupts a background refresh when the owning scope closes", () =>
151 Effect.gen(function* () {
152 const interrupted = yield* Deferred.make<void>();
153 const started = yield* Deferred.make<void>();
154 const scope = yield* Scope.make();
155 const calls = yield* Ref.make(0);
156 const read = yield* makeStaleWhileRevalidate(
157 Ref.updateAndGet(calls, (count) => count + 1).pipe(
158 Effect.filterOrElse(
159 (call) => call === 1,
160 () =>
161 Deferred.succeed(started, undefined).pipe(
162 Effect.andThen(Effect.never),
163 Effect.onInterrupt(() => Deferred.succeed(interrupted, undefined)),
164 ),
165 ),
166 ),
167 "60 seconds",
168 ).pipe(Scope.provide(scope));
169​
170 yield* read;
171 yield* TestClock.adjust("61 seconds");
172 yield* read;
173 yield* Deferred.await(started);
174 yield* Scope.close(scope, Exit.void);
175 yield* Deferred.await(interrupted);
176 }),
177);