Skip to content
Source
Sign inSign up

ProviderLatestVersions.ts

92 lines3.6 KB

1import * as Context from "effect/Context";
2import * as DateTime from "effect/DateTime";
3import * as Deferred from "effect/Deferred";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";
9import * as Ref from "effect/Ref";
10​
11const FOUND_TTL = Duration.hours(1);
12const MISSING_TTL = Duration.minutes(1);
13​
14interface Entry {
15 readonly expiresAt: number;
16 readonly version: string | null;
17 readonly inFlight?: Deferred.Deferred<Option.Option<string | null>>;
19​
20export class ProviderLatestVersions extends Context.Service<
21 ProviderLatestVersions,
22 {
23 readonly cached: <E, R>(
24 key: string,
25 lookup: Effect.Effect<string | null, E, R>,
26 options?: { readonly fresh?: boolean },
27 ) => Effect.Effect<string | null, E, R>;
28 readonly invalidate: (key: string) => Effect.Effect<void>;
29 readonly invalidateAll: Effect.Effect<void>;
30 }
31>()("@t3tools/provider-core/server/ProviderLatestVersions") {}
32​
33export const make = Effect.fn("ProviderLatestVersions.make")(function* (
34 seed: Iterable<readonly [key: string, version: string | null]> = [],
35) {
36 const entries = yield* Ref.make(
37 new Map<string, Entry>(
38 Array.from(seed, ([key, version]) => [key, { version, expiresAt: Number.MAX_SAFE_INTEGER }]),
39 ),
40 );
41 const cached: ProviderLatestVersions["Service"]["cached"] = Effect.fn(
42 "ProviderLatestVersions.cached",
43 )(function* (key, lookup, options) {
44 const now = DateTime.toEpochMillis(yield* DateTime.now);
45 const inFlight = yield* Deferred.make<Option.Option<string | null>>();
46 // Replacing or invalidating this entry retires any lookup still in flight.
47 const pending: Entry = { version: null, expiresAt: 0, inFlight };
48 // Claim and cleanup registration must be atomic with respect to interruption.
49 return yield* Effect.acquireUseRelease(
50 Ref.modify(entries, (current) => {
51 const entry = current.get(key);
52 return !options?.fresh && entry && (entry.expiresAt > now || entry.inFlight)
53 ? [entry, current]
54 : [undefined, new Map(current).set(key, pending)];
55 }),
56 (existing) =>
57 Effect.gen(function* () {
58 if (!existing) return yield* lookup;
59 if (!existing.inFlight) return existing.version;
60 const shared = yield* Deferred.await(existing.inFlight);
61 return Option.isSome(shared) ? shared.value : yield* cached(key, lookup, options);
62 }),
63 (existing, exit) => {
64 if (existing) return Effect.void;
65 const version = Exit.isSuccess(exit) ? Option.some(exit.value) : Option.none();
66 return Ref.update(entries, (current) => {
67 if (current.get(key) !== pending) return current;
68 const next = new Map(current);
69 if (Option.isNone(version)) {
70 next.delete(key);
71 return next;
72 }
73 const ttl = version.value === null ? MISSING_TTL : FOUND_TTL;
74 return next.set(key, { version: version.value, expiresAt: now + Duration.toMillis(ttl) });
75 }).pipe(Effect.andThen(Deferred.succeed(inFlight, version)), Effect.asVoid);
76 },
77 );
78 });
79 return ProviderLatestVersions.of({
80 cached,
81 invalidate: Effect.fn("ProviderLatestVersions.invalidate")(function (key) {
82 return Ref.update(entries, (current) => {
83 const next = new Map(current);
84 next.delete(key);
85 return next;
86 });
87 }),
88 invalidateAll: Ref.set(entries, new Map()),
89 });
90});
91​
92export const layer = Layer.effect(ProviderLatestVersions, make());