Reimplement cache more lol
This commit is contained in:
@@ -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 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> {
|
export interface Pending<A, E> {
|
||||||
readonly _tag: "Pending";
|
readonly _tag: "Pending";
|
||||||
@@ -17,6 +21,24 @@ export type State<A, E> =
|
|||||||
| Pending<A, E>
|
| Pending<A, E>
|
||||||
| Fulfilled<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>>({
|
export const Pending = <A, E>(deferred: Deferred.Deferred<A, E>): Pending<A, E> => Object.freeze<Pending<A, E>>({
|
||||||
_tag: "Pending",
|
_tag: "Pending",
|
||||||
deferred,
|
deferred,
|
||||||
@@ -39,109 +61,158 @@ export interface Cache<K, A, E> extends CacheInterface<K, A, E> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface 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;
|
* Save value for a new key to the cache. Running this effect while the key
|
||||||
readonly setFulfilledSucceed: (key: K, value: A) => void;
|
* already exists in the cache is not allowed and results in a
|
||||||
readonly unset: (key: K) => void;
|
* `InvalidModificationError<K>`.
|
||||||
readonly update: (key: K, action: (prev: A) => A) => void;
|
*/
|
||||||
|
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>;
|
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>;
|
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 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 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 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 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> => {
|
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);
|
const pending = Pending(deferred);
|
||||||
map = HashMap.set(map, key, pending);
|
stateMap.set(key, pending);
|
||||||
listeners.forEach((callback) => callback(map));
|
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);
|
const fulfilled = Fulfilled(exit);
|
||||||
map = HashMap.set(map, key, fulfilled);
|
stateMap.set(key, fulfilled);
|
||||||
listeners.forEach((callback) => callback(map));
|
listenersMap.get(key)?.forEach((callback) => callback(Option.some(fulfilled)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const setFulfilledSucceed = (key: K, value: A) => {
|
const state_delete = (key: K) => {
|
||||||
setFulfilled(key, Exit.succeed(value));
|
stateMap.delete(key);
|
||||||
|
listenersMap.get(key)?.forEach((callback) => callback(Option.none()));
|
||||||
};
|
};
|
||||||
|
|
||||||
const unset = (key: K) => {
|
// --- INTERFACE -----------------------------------------------------------
|
||||||
map = HashMap.remove(map, key);
|
|
||||||
listeners.forEach((callback) => callback(map));
|
|
||||||
};
|
|
||||||
|
|
||||||
const update = (key: K, update: (prev: A) => A) => {
|
const create = (key: K, value: A) => Effect.suspend(() => {
|
||||||
map = HashMap.modify(map, key, (state) => {
|
if (stateMap.has(key)) {
|
||||||
switch (state._tag) {
|
return Effect.fail(new InvalidModificationError({ key }));
|
||||||
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* () {
|
state_setFulfilled(key, Exit.succeed(value));
|
||||||
|
return Effect.succeed(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const refresh = (key: K) => Effect.gen(function* () {
|
||||||
const deferred = yield* Deferred.make<A, E>();
|
const deferred = yield* Deferred.make<A, E>();
|
||||||
setPending(key, deferred);
|
state_setPending(key, deferred);
|
||||||
|
|
||||||
const exit = yield* Effect.exit(fetchFn(key));
|
const exit = yield* Effect.exit(fetchFn(key));
|
||||||
setFulfilled(key, exit);
|
state_setFulfilled(key, exit);
|
||||||
|
|
||||||
yield* Deferred.done(deferred, exit);
|
yield* Deferred.done(deferred, exit);
|
||||||
return yield* exit;
|
return yield* exit;
|
||||||
}).pipe(Effect.uninterruptible);
|
}).pipe(Effect.uninterruptible);
|
||||||
|
|
||||||
const get = (key: K): Effect.Effect<A, E> => Effect.gen(function* () {
|
const get = (key: K) => Effect.suspend(() => {
|
||||||
const state = Option.getOrNull(HashMap.get(map, key));
|
const state = stateMap.get(key);
|
||||||
|
if (state === undefined) {
|
||||||
if (state === null) {
|
return refresh(key);
|
||||||
return yield* refresh(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (state._tag) {
|
return Match(state, {
|
||||||
case "Pending":
|
onPending: Deferred.await,
|
||||||
return yield* Deferred.await(state.deferred);
|
onFulfilled: identity,
|
||||||
case "Fulfilled":
|
});
|
||||||
return yield* state.exit;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
listeners.add(callback);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
listeners.delete(callback);
|
listenersMap.get(key)?.delete(callback);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheInterface = Object.freeze<CacheInterface<K, A, E>>({
|
const cacheInterface = Object.freeze<CacheInterface<K, A, E>>({
|
||||||
setPending,
|
create,
|
||||||
setFulfilled,
|
|
||||||
setFulfilledSucceed,
|
|
||||||
unset,
|
|
||||||
update,
|
|
||||||
refresh,
|
refresh,
|
||||||
get,
|
get,
|
||||||
getCurrent,
|
getCurrent,
|
||||||
|
update,
|
||||||
|
delete: _delete,
|
||||||
subscribe,
|
subscribe,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
export const isUint8ArrayArrayBuffer = (input: unknown): input is Uint8Array<ArrayBuffer> => input instanceof Uint8Array && input.buffer instanceof ArrayBuffer;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as Cache from "common/Cache";
|
import * as Cache from "common/Cache";
|
||||||
import { Cause, Effect, Either, Exit, flow, Option } from "effect";
|
import { Cause, Effect, Either, Exit, flow, Option } from "effect";
|
||||||
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
import { constant } from "effect/Function";
|
||||||
|
import { useCallback, useEffect as useLayoutEffect, useMemo, useSyncExternalStore } from "react";
|
||||||
|
|
||||||
export namespace Loading {
|
export namespace Loading {
|
||||||
export interface Pending {
|
export interface Pending {
|
||||||
@@ -34,39 +35,43 @@ const PENDING: Loading.Pending = Object.freeze<Loading.Pending>({
|
|||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
function resolveState<A, E>(maybeState: Option.Option<Cache.State<A, E>>): Loading<A, E> {
|
const pending = constant(PENDING);
|
||||||
const state = Option.getOrNull(maybeState);
|
|
||||||
|
|
||||||
if (state === null) {
|
const success = <A>(data: A): Loading.Success<A> => Object.freeze<Loading.Success<A>>({
|
||||||
return PENDING;
|
isLoading: false,
|
||||||
}
|
data,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
switch (state._tag) {
|
const error = <E>(error: E): Loading.Error<E> => Object.freeze<Loading.Error<E>>({
|
||||||
case "Pending":
|
isLoading: false,
|
||||||
return PENDING;
|
data: null,
|
||||||
case "Fulfilled":
|
error,
|
||||||
return Exit.match(state.exit, {
|
});
|
||||||
|
|
||||||
|
export function useCache<K, A, E>(cache: Cache.Cache<K, A, E>, key: NoInfer<K>): Loading<A, E> {
|
||||||
|
|
||||||
|
const subscribe = useMemo(() => cache.subscribe.bind(undefined, key), [cache.subscribe, key]);
|
||||||
|
const selector = useCallback(() => cache.getCurrent(key), [cache.getCurrent, key]);
|
||||||
|
|
||||||
|
const state = useSyncExternalStore(subscribe, selector);
|
||||||
|
|
||||||
|
useLayoutEffect(() => { Effect.runFork(cache.get(key)); }, [cache.get, key]);
|
||||||
|
|
||||||
|
return Option.match(state, {
|
||||||
|
onNone: pending,
|
||||||
|
onSome: Cache.Match({
|
||||||
|
onPending: pending,
|
||||||
|
onFulfilled: Exit.match({
|
||||||
onFailure: flow(
|
onFailure: flow(
|
||||||
Cause.failureOrCause,
|
Cause.failureOrCause,
|
||||||
Either.match({
|
Either.match({
|
||||||
onLeft: (error) => Object.freeze<Loading.Error<E>>({ isLoading: false, data: null, error }),
|
onLeft: error,
|
||||||
onRight: (cause) => Object.freeze<Loading.Error<E>>({ isLoading: false, data: null, error: cause as any }),
|
onRight: (cause) => error(cause as any),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
onSuccess: (data) => Object.freeze<Loading.Success<A>>({ isLoading: false, data, error: null }),
|
onSuccess: success,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export function useCache<K, A, E>(cache: Cache.Cache<K, A, E>, key: NoInfer<K>, refresh: boolean = false) {
|
|
||||||
|
|
||||||
const selector = useCallback(() => cache.getCurrent(key), [cache.getCurrent, key]);
|
|
||||||
|
|
||||||
const state = useSyncExternalStore(cache.subscribe, selector);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Effect.runFork(refresh ? cache.refresh(key) : cache.get(key));
|
|
||||||
}, [cache.get, key, refresh]);
|
|
||||||
|
|
||||||
return resolveState(state);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { type User } from "common/the_api";
|
import { Update } from "common";
|
||||||
|
import { User } from "common/the_api";
|
||||||
import { identity } from "effect";
|
import { identity } from "effect";
|
||||||
import { useLayoutEffect, useState } from "react";
|
import { useLayoutEffect, useState } from "react";
|
||||||
|
|
||||||
export type Update<T> = T | ((prev: T) => T);
|
|
||||||
export type Updater<T> = (action: Update<T>) => void;
|
|
||||||
|
|
||||||
export const mapProp = <const K extends string, T>(prop: K, action: Update<T>) => <O extends { readonly [_ in K]: T }>(object: O): O => {
|
export const mapProp = <const K extends string, T>(prop: K, action: Update<T>) => <O extends { readonly [_ in K]: T }>(object: O): O => {
|
||||||
return Object.freeze({ ...object, [prop]: typeof action === "function" ? (action as (prev: T) => T)(object[prop]) : action });
|
return Object.freeze({ ...object, [prop]: typeof action === "function" ? (action as (prev: T) => T)(object[prop]) : action });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Attachment, denormalizeSystemInformation, pieceCache, type Piece } from "@/cache";
|
import { Attachment, denormalizePiece, denormalizeSystemInformation, pieceCache, type Piece } from "@/cache";
|
||||||
import { API_URL_PREFIX, client } from "@/client";
|
import { API_URL_PREFIX, client } from "@/client";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -78,13 +78,18 @@ function PieceForm(props: PieceForm.Props) {
|
|||||||
try {
|
try {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
|
||||||
await client.updatePiece({
|
await pipe(
|
||||||
|
client.updatePiece({
|
||||||
pieceId: props.piece.pieceId,
|
pieceId: props.piece.pieceId,
|
||||||
name,
|
name,
|
||||||
composer: composer.length > 0 ? Option.some(composer) : Option.none(),
|
composer: composer.length > 0 ? Option.some(composer) : Option.none(),
|
||||||
lyricist: lyricist.length > 0 ? Option.some(lyricist) : Option.none(),
|
lyricist: lyricist.length > 0 ? Option.some(lyricist) : Option.none(),
|
||||||
arranger: arranger.length > 0 ? Option.some(arranger) : Option.none(),
|
arranger: arranger.length > 0 ? Option.some(arranger) : Option.none(),
|
||||||
}).pipe(Effect.runPromise);
|
}),
|
||||||
|
Effect.flatMap(denormalizePiece),
|
||||||
|
Effect.tap((piece) => pieceCache.update(piece.pieceId, piece)),
|
||||||
|
Effect.runPromise,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -97,8 +102,11 @@ function PieceForm(props: PieceForm.Props) {
|
|||||||
try {
|
try {
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
|
|
||||||
await Effect.runPromise(client.deletePiece(props.piece.pieceId));
|
await pipe(
|
||||||
pieceCache.unset(props.piece.pieceId);
|
client.deletePiece(props.piece.pieceId),
|
||||||
|
Effect.andThen(pieceCache.delete(props.piece.pieceId)),
|
||||||
|
Effect.runPromise,
|
||||||
|
);
|
||||||
|
|
||||||
navigate("..");
|
navigate("..");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -200,8 +208,6 @@ namespace AttachmentRow {
|
|||||||
|
|
||||||
function AttachmentRow(props: AttachmentRow.Props) {
|
function AttachmentRow(props: AttachmentRow.Props) {
|
||||||
|
|
||||||
const url = `${API_URL_PREFIX}/api/piece/${props.attachment.pieceId}/attachment/${props.attachment.attachmentId}`;
|
|
||||||
|
|
||||||
const download = () => Effect.gen(function* () {
|
const download = () => Effect.gen(function* () {
|
||||||
const { data, mediaType, filename } = yield* client.getAttachment(props.attachment.attachmentId);
|
const { data, mediaType, filename } = yield* client.getAttachment(props.attachment.attachmentId);
|
||||||
|
|
||||||
@@ -239,7 +245,7 @@ function AttachmentRow(props: AttachmentRow.Props) {
|
|||||||
|
|
||||||
const doDelete = () => Effect.gen(function* () {
|
const doDelete = () => Effect.gen(function* () {
|
||||||
yield* client.deleteAttachment(props.attachment.attachmentId);
|
yield* client.deleteAttachment(props.attachment.attachmentId);
|
||||||
pieceCache.update(props.attachment.pieceId, mapProp("attachments", Array.filter<Attachment>((a) => a.attachmentId !== props.attachment.attachmentId)));
|
yield* pieceCache.update(props.attachment.pieceId, mapProp("attachments", Array.filter<Attachment>((a) => a.attachmentId !== props.attachment.attachmentId)));
|
||||||
}).pipe(Effect.runPromise);
|
}).pipe(Effect.runPromise);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -332,7 +338,7 @@ function AttachmentForm(props: AttachmentForm.Props) {
|
|||||||
|
|
||||||
const attachment = yield* denormalizeSystemInformation(exit.value);
|
const attachment = yield* denormalizeSystemInformation(exit.value);
|
||||||
|
|
||||||
pieceCache.update(props.pieceId, mapProp("attachments", (attachments: readonly Attachment[]) => {
|
yield* pieceCache.update(props.pieceId, mapProp("attachments", (attachments: readonly Attachment[]) => {
|
||||||
const next = [...attachments, attachment];
|
const next = [...attachments, attachment];
|
||||||
next.sort((a, b) => a.filename.localeCompare(b.filename));
|
next.sort((a, b) => a.filename.localeCompare(b.filename));
|
||||||
return next;
|
return next;
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { useCache } from "@/hooks/useCache";
|
import { useCache } from "@/hooks/useCache";
|
||||||
import { useLoading } from "@/hooks/useLoading";
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
import { Updater } from "@/hooks/useStore";
|
|
||||||
import { authors, created, DEBOUNCE, modified, SAVE_DELAY } from "@/snippets";
|
import { authors, created, DEBOUNCE, modified, SAVE_DELAY } from "@/snippets";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { PieceId } from "common";
|
import { PieceId, Updater } from "common";
|
||||||
import * as Body from "common/Body";
|
import * as Body from "common/Body";
|
||||||
import { getMediaTypeForFilename } from "common/MediaType";
|
import { getMediaTypeForFilename } from "common/MediaType";
|
||||||
import { Array, Cause, Effect, Fiber, Iterable, Match, Option, Order, pipe, Predicate, Scope, SortedMap } from "effect";
|
import { Array, Cause, Effect, Fiber, Iterable, Match, Option, Order, pipe, Predicate, Scope, SortedMap } from "effect";
|
||||||
@@ -27,7 +26,7 @@ export function Pieces() {
|
|||||||
|
|
||||||
const debounce = useRef(Effect.void);
|
const debounce = useRef(Effect.void);
|
||||||
|
|
||||||
const { isLoading, error, data: pieceIds } = useLoading(Effect.gen(function* () {
|
const { isLoading, error, data: pieceIds, refresh } = useLoading(Effect.gen(function* () {
|
||||||
yield* debounce.current;
|
yield* debounce.current;
|
||||||
const data = yield* client.queryPieces({
|
const data = yield* client.queryPieces({
|
||||||
name: name !== "" ? Option.some(name) : Option.none(),
|
name: name !== "" ? Option.some(name) : Option.none(),
|
||||||
@@ -55,7 +54,7 @@ export function Pieces() {
|
|||||||
<Import />Importuj utwory
|
<Import />Importuj utwory
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<ImportPiecesDialogContent setDialogOpen={setImportDialogOpen} />
|
<ImportPiecesDialogContent setDialogOpen={setImportDialogOpen} refresh={refresh} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<Input
|
<Input
|
||||||
className="w-[32ch]"
|
className="w-[32ch]"
|
||||||
@@ -197,7 +196,7 @@ function AddPieceDialogContent() {
|
|||||||
arranger: arranger.length > 0 ? Option.some(arranger) : Option.none(),
|
arranger: arranger.length > 0 ? Option.some(arranger) : Option.none(),
|
||||||
}),
|
}),
|
||||||
Effect.flatMap(denormalizePiece),
|
Effect.flatMap(denormalizePiece),
|
||||||
Effect.tap((piece) => Effect.sync(() => pieceCache.setFulfilledSucceed(piece.pieceId, piece))),
|
Effect.tap((piece) => pieceCache.create(piece.pieceId, piece)),
|
||||||
);
|
);
|
||||||
|
|
||||||
navigate(pieceId);
|
navigate(pieceId);
|
||||||
@@ -260,6 +259,7 @@ function AddPieceDialogContent() {
|
|||||||
namespace ImportPiecesDialogContent {
|
namespace ImportPiecesDialogContent {
|
||||||
export interface Props {
|
export interface Props {
|
||||||
readonly setDialogOpen: Updater<boolean>;
|
readonly setDialogOpen: Updater<boolean>;
|
||||||
|
readonly refresh: Effect.Effect<void>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,12 +358,16 @@ function ImportPiecesDialogContent(props: ImportPiecesDialogContent.Props) {
|
|||||||
yield* pipe(
|
yield* pipe(
|
||||||
{ ...piece, attachments },
|
{ ...piece, attachments },
|
||||||
denormalizePiece,
|
denormalizePiece,
|
||||||
Effect.tap((piece) => Effect.sync(() => pieceCache.setFulfilledSucceed(piece.pieceId, piece))),
|
Effect.tap((piece) => pieceCache.create(piece.pieceId, piece)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setFiles(SortedMap.remove(name));
|
||||||
})),
|
})),
|
||||||
Effect.allWith({ concurrency: "unbounded" }),
|
Effect.allWith({ concurrency: "unbounded" }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
yield* props.refresh;
|
||||||
|
|
||||||
props.setDialogOpen(false);
|
props.setDialogOpen(false);
|
||||||
}));
|
}));
|
||||||
}).pipe(Effect.runPromise);
|
}).pipe(Effect.runPromise);
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { useCache } from "@/hooks/useCache";
|
import { useCache } from "@/hooks/useCache";
|
||||||
import { useLoading } from "@/hooks/useLoading";
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
import { Updater } from "@/hooks/useStore";
|
|
||||||
import { authors, DEBOUNCE, SAVE_DELAY } from "@/snippets";
|
import { authors, DEBOUNCE, SAVE_DELAY } from "@/snippets";
|
||||||
import { PieceId, RepertoireId } from "common";
|
import { PieceId, RepertoireId, Updater } from "common";
|
||||||
import { Array, Cause, Effect, Fiber, Match, Option, pipe, Scope } from "effect";
|
import { Array, Cause, Effect, Fiber, Match, Option, pipe, Scope } from "effect";
|
||||||
import { ChevronDown, ChevronUp, CircleMinus, Loader2, Plus } from "lucide-react";
|
import { ChevronDown, ChevronUp, CircleMinus, Loader2, Plus } from "lucide-react";
|
||||||
import { FormEventHandler, useId, useRef, useState } from "react";
|
import { FormEventHandler, useId, useRef, useState } from "react";
|
||||||
@@ -75,11 +74,15 @@ function RepertoireForm(props: RepertoireForm.Props) {
|
|||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
|
||||||
yield* client.updateRepertoire({
|
yield* pipe(
|
||||||
|
client.updateRepertoire({
|
||||||
repertoireId: props.repertoire.repertoireId,
|
repertoireId: props.repertoire.repertoireId,
|
||||||
name,
|
name,
|
||||||
entries: props.repertoire.entries.map(({ pieceId }) => pieceId),
|
entries: props.repertoire.entries.map(({ pieceId }) => pieceId),
|
||||||
});
|
}),
|
||||||
|
Effect.flatMap(denormalizeRepertoire),
|
||||||
|
Effect.tap((repertoire) => repertoireCache.update(repertoire.repertoireId, repertoire)),
|
||||||
|
);
|
||||||
}));
|
}));
|
||||||
}).pipe(Effect.runPromise);
|
}).pipe(Effect.runPromise);
|
||||||
|
|
||||||
@@ -90,7 +93,7 @@ function RepertoireForm(props: RepertoireForm.Props) {
|
|||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
|
|
||||||
yield* client.deleteRepertoire(props.repertoire.repertoireId);
|
yield* client.deleteRepertoire(props.repertoire.repertoireId);
|
||||||
repertoireCache.unset(props.repertoire.repertoireId);
|
yield* repertoireCache.delete(props.repertoire.repertoireId);
|
||||||
|
|
||||||
navigate("..");
|
navigate("..");
|
||||||
})).pipe(Effect.runPromise);
|
})).pipe(Effect.runPromise);
|
||||||
@@ -210,7 +213,7 @@ function EntryRow({
|
|||||||
entries: pipe(repertoire.entries, action, mapToId),
|
entries: pipe(repertoire.entries, action, mapToId),
|
||||||
}),
|
}),
|
||||||
Effect.flatMap(denormalizeRepertoire),
|
Effect.flatMap(denormalizeRepertoire),
|
||||||
Effect.tap((repertoire) => Effect.sync(() => repertoireCache.setFulfilledSucceed(repertoire.repertoireId, repertoire))),
|
Effect.tap((repertoire) => repertoireCache.update(repertoire.repertoireId, repertoire)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -360,7 +363,7 @@ function EntryDialogPieceRow(props: EntryDialogPieceRow.Props) {
|
|||||||
entries: pipe(props.repertoire.entries, action, mapToId),
|
entries: pipe(props.repertoire.entries, action, mapToId),
|
||||||
}),
|
}),
|
||||||
Effect.flatMap(denormalizeRepertoire),
|
Effect.flatMap(denormalizeRepertoire),
|
||||||
Effect.tap((repertoire) => Effect.sync(() => repertoireCache.setFulfilledSucceed(repertoire.repertoireId, repertoire))),
|
Effect.tap((repertoire) => repertoireCache.update(repertoire.repertoireId, repertoire)),
|
||||||
);
|
);
|
||||||
|
|
||||||
props.setDialogOpen(false);
|
props.setDialogOpen(false);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { repertoireCache } from "@/cache";
|
import { denormalizeRepertoire, repertoireCache } from "@/cache";
|
||||||
import { client } from "@/client";
|
import { client } from "@/client";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
@@ -9,7 +9,7 @@ import { useCache } from "@/hooks/useCache";
|
|||||||
import { useLoading } from "@/hooks/useLoading";
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
import { created, DEBOUNCE, modified } from "@/snippets";
|
import { created, DEBOUNCE, modified } from "@/snippets";
|
||||||
import { RepertoireId } from "common";
|
import { RepertoireId } from "common";
|
||||||
import { Cause, Effect, Match, Option, Scope } from "effect";
|
import { Cause, Effect, Match, Option, pipe, Scope } from "effect";
|
||||||
import { Loader2, Plus } from "lucide-react";
|
import { Loader2, Plus } from "lucide-react";
|
||||||
import { FormEventHandler, ReactNode, useId, useRef, useState } from "react";
|
import { FormEventHandler, ReactNode, useId, useRef, useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
@@ -171,10 +171,14 @@ function AddRepertoireDialogContent() {
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const { repertoireId } = yield* client.createRepertoire({
|
const { repertoireId } = yield* pipe(
|
||||||
|
client.createRepertoire({
|
||||||
name,
|
name,
|
||||||
entries: [],
|
entries: [],
|
||||||
});
|
}),
|
||||||
|
Effect.flatMap(denormalizeRepertoire),
|
||||||
|
Effect.tap((repertoire) => repertoireCache.create(repertoire.repertoireId, repertoire)),
|
||||||
|
);
|
||||||
|
|
||||||
navigate(repertoireId);
|
navigate(repertoireId);
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user