Reimplement cache more lol

This commit is contained in:
2025-10-10 14:54:42 +02:00
parent a199b104ad
commit 6ea21aa0f1
8 changed files with 224 additions and 130 deletions

View File

@@ -1,7 +1,11 @@
import { Deferred, Effect, Exit, flow, HashMap, Option } from "effect";
import { Update } from "common";
import { Data, Deferred, Effect, Exit, Option } from "effect";
import { dual, identity } from "effect/Function";
export class InvalidModificationError<K> extends Data.TaggedError("InvalidModificationError")<{ key: K }> { }
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 type ListenFn<A, E> = (state: Option.Option<State<A, E>>) => void;
export interface Pending<A, E> {
readonly _tag: "Pending";
@@ -17,6 +21,24 @@ export type State<A, E> =
| Pending<A, E>
| Fulfilled<A, E>;
export const Match = dual<
<A, E, Z1, Z2>(options: {
readonly onPending: (deferred: Deferred.Deferred<A, E>) => Z1,
readonly onFulfilled: (exit: Exit.Exit<A, E>) => Z2,
}) => (self: State<A, E>) => Z1 | Z2,
<A, E, Z1, Z2>(self: State<A, E>, options: {
readonly onPending: (deferred: Deferred.Deferred<A, E>) => Z1,
readonly onFulfilled: (exit: Exit.Exit<A, E>) => Z2,
}) => Z1 | Z2
>(2, (self, { onPending, onFulfilled }) => {
switch (self._tag) {
case "Pending":
return onPending(self.deferred);
case "Fulfilled":
return onFulfilled(self.exit);
}
});
export const Pending = <A, E>(deferred: Deferred.Deferred<A, E>): Pending<A, E> => Object.freeze<Pending<A, E>>({
_tag: "Pending",
deferred,
@@ -39,109 +61,158 @@ export interface Cache<K, A, E> extends CacheInterface<K, A, 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;
/**
* Save value for a new key to the cache. Running this effect while the key
* already exists in the cache is not allowed and results in a
* `InvalidModificationError<K>`.
*/
readonly create: (key: K, value: A) => Effect.Effect<A, InvalidModificationError<K>>;
/**
* Call `fetchFn`, save the result to the cache and return the result.
*/
readonly refresh: (key: K) => Effect.Effect<A, E>;
/**
* If the `key` exists in the cache, retrieve the result, otherwise call
* `fetchFn`, save the result to the cache and return the result.
*/
readonly get: (key: K) => Effect.Effect<A, E>;
/**
* Retrieve the state currently stored in the cache for a given `key`.
*/
readonly getCurrent: (key: K) => Option.Option<State<A, E>>;
readonly subscribe: (callback: ListenFn<K, A, E>) => (() => void);
/**
* Set or update the value currently stored in the cache for a given `key`
* and return the updated value. Running this effect while the state is
* pending or the key does not exist in the cache is not allowed and results
* in a `InvalidModificationError<K>`. Running this effect while the state
* is erroneously fulfilled will do nothing and return the error.
*/
readonly update: (key: K, action: Update<A>) => Effect.Effect<A, E | InvalidModificationError<K>>;
/**
* Remove the state currently stored in the cache for a given `key`. Running
* this effect while the key does not exist in the cache is not allowed and
* results in a `InvalidModificationError<K>`.
*/
readonly delete: (key: K) => Effect.Effect<void, InvalidModificationError<K>>;
/**
* Subscribe to any change in the internal cache to a given key.
* @returns Unsubscribe function
*/
readonly subscribe: (key: K, callback: ListenFn<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 stateMap = new Map<K, State<A, E>>;
const listenersMap = new Map<K, Set<ListenFn<A, E>>>();
const listeners = new Set<ListenFn<K, A, E>>();
// --- INTERNAL FUNCTIONS --------------------------------------------------
const setPending = (key: K, deferred: Deferred.Deferred<A, E>) => {
const state_setPending = (key: K, deferred: Deferred.Deferred<A, E>) => {
const pending = Pending(deferred);
map = HashMap.set(map, key, pending);
listeners.forEach((callback) => callback(map));
stateMap.set(key, pending);
listenersMap.get(key)?.forEach((callback) => callback(Option.some(pending)));
};
const setFulfilled = (key: K, exit: Exit.Exit<A, E>) => {
const state_setFulfilled = (key: K, exit: Exit.Exit<A, E>) => {
const fulfilled = Fulfilled(exit);
map = HashMap.set(map, key, fulfilled);
listeners.forEach((callback) => callback(map));
stateMap.set(key, fulfilled);
listenersMap.get(key)?.forEach((callback) => callback(Option.some(fulfilled)));
};
const setFulfilledSucceed = (key: K, value: A) => {
setFulfilled(key, Exit.succeed(value));
const state_delete = (key: K) => {
stateMap.delete(key);
listenersMap.get(key)?.forEach((callback) => callback(Option.none()));
};
const unset = (key: K) => {
map = HashMap.remove(map, key);
listeners.forEach((callback) => callback(map));
};
// --- INTERFACE -----------------------------------------------------------
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 create = (key: K, value: A) => Effect.suspend(() => {
if (stateMap.has(key)) {
return Effect.fail(new InvalidModificationError({ key }));
}
const refresh = (key: K): Effect.Effect<A, E> => Effect.gen(function* () {
state_setFulfilled(key, Exit.succeed(value));
return Effect.succeed(value);
});
const refresh = (key: K) => Effect.gen(function* () {
const deferred = yield* Deferred.make<A, E>();
setPending(key, deferred);
state_setPending(key, deferred);
const exit = yield* Effect.exit(fetchFn(key));
setFulfilled(key, exit);
state_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);
const get = (key: K) => Effect.suspend(() => {
const state = stateMap.get(key);
if (state === undefined) {
return refresh(key);
}
switch (state._tag) {
case "Pending":
return yield* Deferred.await(state.deferred);
case "Fulfilled":
return yield* state.exit;
}
return Match(state, {
onPending: Deferred.await,
onFulfilled: identity,
});
});
const getCurrent = (key: K): Option.Option<State<A, E>> => HashMap.get(map, key);
const getCurrent = (key: K) => Option.fromNullable(stateMap.get(key));
const subscribe = (callback: ListenFn<K, A, E>) => {
const update = (key: K, action: Update<A>) => Effect.suspend(() => {
const state = stateMap.get(key);
if (state === undefined) {
return Effect.fail(new InvalidModificationError({ key }));
}
return Match(state, {
onPending: () => Effect.fail(new InvalidModificationError({ key })),
onFulfilled: Exit.match({
onFailure: () => Effect.fail(new InvalidModificationError({ key })),
onSuccess: (value) => {
const nextValue = typeof action === "function" ? (action as (prev: A) => A)(value) : action;
state_setFulfilled(key, Exit.succeed(nextValue));
return Effect.succeed(nextValue);
},
}),
});
});
const _delete = (key: K) => Effect.suspend(() => {
if (!stateMap.has(key)) {
return Effect.fail(new InvalidModificationError({ key }));
}
state_delete(key);
return Effect.void;
});
const subscribe = (key: K, callback: ListenFn<A, E>) => {
let listeners = listenersMap.get(key);
if (listeners === undefined) {
listeners = new Set();
listenersMap.set(key, listeners);
}
listeners.add(callback);
return () => {
listeners.delete(callback);
listenersMap.get(key)?.delete(callback);
};
};
const cacheInterface = Object.freeze<CacheInterface<K, A, E>>({
setPending,
setFulfilled,
setFulfilledSucceed,
unset,
update,
create,
refresh,
get,
getCurrent,
update,
delete: _delete,
subscribe,
});

View File

@@ -1,4 +1,7 @@
import { Arbitrary, Array, Equal, Equivalence, pipe, Predicate, Pretty, Schema } from "effect";
import { Arbitrary, Array, Equal, Equivalence, pipe, Pretty, Schema } from "effect";
export type Update<T> = T | ((prev: T) => T);
export type Updater<T> = (action: Update<T>) => void;
export const isUint8ArrayArrayBuffer = (input: unknown): input is Uint8Array<ArrayBuffer> => input instanceof Uint8Array && input.buffer instanceof ArrayBuffer;