Cache reimplementation

This commit is contained in:
2025-10-10 13:09:11 +02:00
parent 6717e6b0de
commit a199b104ad
8 changed files with 356 additions and 155 deletions

View File

@@ -0,0 +1,149 @@
import { Deferred, Effect, Exit, flow, HashMap, Option } from "effect";
export type FetchFn<K, A, E> = (key: K) => Effect.Effect<A, E>;
export type ListenFn<K, A, E> = (state: Map<K, A, E>) => void;
export interface Pending<A, E> {
readonly _tag: "Pending";
readonly deferred: Deferred.Deferred<A, E>;
}
export interface Fulfilled<A, E> {
readonly _tag: "Fulfilled";
readonly exit: Exit.Exit<A, E>;
}
export type State<A, E> =
| Pending<A, E>
| Fulfilled<A, E>;
export const Pending = <A, E>(deferred: Deferred.Deferred<A, E>): Pending<A, E> => Object.freeze<Pending<A, E>>({
_tag: "Pending",
deferred,
});
export const Fulfilled = <A, E>(exit: Exit.Exit<A, E>): Fulfilled<A, E> => Object.freeze<Fulfilled<A, E>>({
_tag: "Fulfilled",
exit,
});
declare const CacheTypeId: unique symbol;
type CacheTypeId = typeof CacheTypeId;
export interface Cache<K, A, E> extends CacheInterface<K, A, E> {
readonly [CacheTypeId]: {
readonly _K: (_: K) => K,
readonly _A: (_: A) => A,
readonly _E: (_: E) => E,
};
}
interface CacheInterface<K, A, E> {
readonly setPending: (key: K, deferred: Deferred.Deferred<A, E>) => void;
readonly setFulfilled: (key: K, exit: Exit.Exit<A, E>) => void;
readonly setFulfilledSucceed: (key: K, value: A) => void;
readonly unset: (key: K) => void;
readonly update: (key: K, action: (prev: A) => A) => void;
readonly refresh: (key: K) => Effect.Effect<A, E>;
readonly get: (key: K) => Effect.Effect<A, E>;
readonly getCurrent: (key: K) => Option.Option<State<A, E>>;
readonly subscribe: (callback: ListenFn<K, A, E>) => (() => void);
}
export type Key<T extends Cache<any, any, any>> = T extends Cache<infer K, any, any> ? K : never;
export type Value<T extends Cache<any, any, any>> = T extends Cache<any, infer A, any> ? A : never;
export type Error<T extends Cache<any, any, any>> = T extends Cache<any, any, infer E> ? E : never;
export type Map<K, A, E> = HashMap.HashMap<K, State<A, E>>;
export const make = <K, A, E>(fetchFn: FetchFn<K, A, E>): Cache<K, A, E> => {
let map: Map<K, A, E> = HashMap.empty();
const listeners = new Set<ListenFn<K, A, E>>();
const setPending = (key: K, deferred: Deferred.Deferred<A, E>) => {
const pending = Pending(deferred);
map = HashMap.set(map, key, pending);
listeners.forEach((callback) => callback(map));
};
const setFulfilled = (key: K, exit: Exit.Exit<A, E>) => {
const fulfilled = Fulfilled(exit);
map = HashMap.set(map, key, fulfilled);
listeners.forEach((callback) => callback(map));
};
const setFulfilledSucceed = (key: K, value: A) => {
setFulfilled(key, Exit.succeed(value));
};
const unset = (key: K) => {
map = HashMap.remove(map, key);
listeners.forEach((callback) => callback(map));
};
const update = (key: K, update: (prev: A) => A) => {
map = HashMap.modify(map, key, (state) => {
switch (state._tag) {
case "Pending":
return state;
case "Fulfilled":
return Exit.match(state.exit, {
onFailure: () => state,
onSuccess: flow(update, Exit.succeed, Fulfilled),
});
}
});
listeners.forEach((callback) => callback(map));
}
const refresh = (key: K): Effect.Effect<A, E> => Effect.gen(function* () {
const deferred = yield* Deferred.make<A, E>();
setPending(key, deferred);
const exit = yield* Effect.exit(fetchFn(key));
setFulfilled(key, exit);
yield* Deferred.done(deferred, exit);
return yield* exit;
}).pipe(Effect.uninterruptible);
const get = (key: K): Effect.Effect<A, E> => Effect.gen(function* () {
const state = Option.getOrNull(HashMap.get(map, key));
if (state === null) {
return yield* refresh(key);
}
switch (state._tag) {
case "Pending":
return yield* Deferred.await(state.deferred);
case "Fulfilled":
return yield* state.exit;
}
});
const getCurrent = (key: K): Option.Option<State<A, E>> => HashMap.get(map, key);
const subscribe = (callback: ListenFn<K, A, E>) => {
listeners.add(callback);
return () => {
listeners.delete(callback);
};
};
const cacheInterface = Object.freeze<CacheInterface<K, A, E>>({
setPending,
setFulfilled,
setFulfilledSucceed,
unset,
update,
refresh,
get,
getCurrent,
subscribe,
});
return cacheInterface as Cache<K, A, E>;
};