221 lines
6.8 KiB
TypeScript
221 lines
6.8 KiB
TypeScript
import { type 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<A, E> = (state: Option.Option<State<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 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,
|
|
});
|
|
|
|
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> {
|
|
/**
|
|
* 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) => State<A, E> | undefined;
|
|
/**
|
|
* 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 const make = <K, A, E>(fetchFn: FetchFn<K, A, E>): Cache<K, A, E> => {
|
|
|
|
const stateMap = new Map<K, State<A, E>>;
|
|
const listenersMap = new Map<K, Set<ListenFn<A, E>>>();
|
|
|
|
// --- INTERNAL FUNCTIONS --------------------------------------------------
|
|
|
|
const state_setPending = (key: K, deferred: Deferred.Deferred<A, E>) => {
|
|
const pending = Pending(deferred);
|
|
stateMap.set(key, pending);
|
|
listenersMap.get(key)?.forEach((callback) => callback(Option.some(pending)));
|
|
};
|
|
|
|
const state_setFulfilled = (key: K, exit: Exit.Exit<A, E>) => {
|
|
const fulfilled = Fulfilled(exit);
|
|
stateMap.set(key, fulfilled);
|
|
listenersMap.get(key)?.forEach((callback) => callback(Option.some(fulfilled)));
|
|
};
|
|
|
|
const state_delete = (key: K) => {
|
|
stateMap.delete(key);
|
|
listenersMap.get(key)?.forEach((callback) => callback(Option.none()));
|
|
};
|
|
|
|
// --- INTERFACE -----------------------------------------------------------
|
|
|
|
const create = (key: K, value: A) => Effect.suspend(() => {
|
|
if (stateMap.has(key)) {
|
|
return Effect.fail(new InvalidModificationError({ key }));
|
|
}
|
|
|
|
state_setFulfilled(key, Exit.succeed(value));
|
|
return Effect.succeed(value);
|
|
});
|
|
|
|
const refresh = (key: K) => Effect.gen(function* () {
|
|
const deferred = yield* Deferred.make<A, E>();
|
|
state_setPending(key, deferred);
|
|
|
|
const exit = yield* Effect.exit(fetchFn(key));
|
|
state_setFulfilled(key, exit);
|
|
|
|
yield* Deferred.done(deferred, exit);
|
|
return yield* exit;
|
|
}).pipe(Effect.uninterruptible);
|
|
|
|
const get = (key: K) => Effect.suspend(() => {
|
|
const state = stateMap.get(key);
|
|
if (state === undefined) {
|
|
return refresh(key);
|
|
}
|
|
|
|
return Match(state, {
|
|
onPending: Deferred.await,
|
|
onFulfilled: identity,
|
|
});
|
|
});
|
|
|
|
const getCurrent = (key: K) => stateMap.get(key);
|
|
|
|
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 () => {
|
|
listenersMap.get(key)?.delete(callback);
|
|
};
|
|
};
|
|
|
|
const cacheInterface = Object.freeze<CacheInterface<K, A, E>>({
|
|
create,
|
|
refresh,
|
|
get,
|
|
getCurrent,
|
|
update,
|
|
delete: _delete,
|
|
subscribe,
|
|
});
|
|
|
|
return cacheInterface as Cache<K, A, E>;
|
|
};
|