diff --git a/pkg/interface/src/logic/state/base.ts b/pkg/interface/src/logic/state/base.ts index c987df77a..493eef7c7 100644 --- a/pkg/interface/src/logic/state/base.ts +++ b/pkg/interface/src/logic/state/base.ts @@ -1,24 +1,39 @@ -import produce, { setAutoFreeze } from 'immer'; +import produce, { applyPatches, Patch, produceWithPatches, setAutoFreeze, enablePatches } from 'immer'; import { compose } from 'lodash/fp'; -import create, { State, UseStore } from 'zustand'; +import _ from 'lodash'; +import create, { UseStore } from 'zustand'; import { persist } from 'zustand/middleware'; setAutoFreeze(false); +enablePatches(); -export const stateSetter = ( - fn: (state: StateType) => void, - set +export const stateSetter = ( + fn: (state: Readonly>) => void, + set: (newState: T & BaseState) => void ): void => { - set(produce(fn)); + set(produce(fn) as any); }; +export const optStateSetter = ( + fn: (state: T & BaseState) => void, + set: (newState: T & BaseState) => void, + get: () => T & BaseState +): string => { + const old = get(); + const id = _.uniqueId() + const [state, ,patches] = produceWithPatches(old, fn) as readonly [(T & BaseState), any, Patch[]]; + set({ ...state, patches: { ...state.patches, [id]: patches }}); + return id; +}; + + export const reduceState = < - StateType extends BaseState, - UpdateType + S extends {}, + U >( - state: UseStore, - data: UpdateType, - reducers: ((data: UpdateType, state: StateType) => StateType)[] + state: UseStore>, + data: U, + reducers: ((data: U, state: S & BaseState) => S & BaseState)[] ): void => { const reducer = compose(reducers.map(r => sta => r(data, sta))); state.getState().set((state) => { @@ -26,6 +41,18 @@ export const reduceState = < }); }; +export const optReduceState = ( + state: UseStore>, + data: U, + reducers: ((data: U, state: S & BaseState) => BaseState & S)[] +): string => { + const reducer = compose(reducers.map(r => sta => r(data, sta))); + return state.getState().optSet((state) => { + reducer(state); + }); +}; + + export let stateStorageKeys: string[] = []; export const stateStorageKey = (stateName: string) => { @@ -40,19 +67,56 @@ export const stateStorageKey = (stateName: string) => { }); }; -export interface BaseState extends State { - set: (fn: (state: StateType) => void) => void; +export interface BaseState { + rollback: (id: string) => void; + patches: { + [id: string]: Patch[]; + }; + set: (fn: (state: BaseState) => void) => void; + addPatch: (id: string, ...patch: Patch[]) => void; + removePatch: (id: string) => void; + optSet: (fn: (state: BaseState) => void) => string; } -export const createState = >( +export const createState = ( name: string, - properties: { [K in keyof Omit]: T[K] }, - blacklist: string[] = [] -): UseStore => create(persist((set, get) => ({ + properties: T, + blacklist: (keyof BaseState | keyof T)[] = [] +): UseStore> => create>(persist>((set, get) => ({ set: fn => stateSetter(fn, set), - ...properties as any + optSet: fn => { + return optStateSetter(fn, set, get); + }, + patches: {}, + addPatch: (id: string, ...patch: Patch[]) => { + set(({ patches }) => ({ patches: {...patches, [id]: patch }})); + }, + removePatch: (id: string) => { + set(({ patches }) => ({ patches: _.omit(patches, id)})); + }, + rollback: (id: string) => { + set(state => { + const applying = state.patches[id] + return {...applyPatches(state, applying), patches: _.omit(state.patches, id) } + }); + }, + ...properties }), { blacklist, name: stateStorageKey(name), version: process.env.LANDSCAPE_SHORTHASH as any })); + +export async function doOptimistically(state: UseStore>, action: A, call: (a: A) => Promise, reduce: ((a: A, fn: S & BaseState) => S & BaseState)[]) { + let num: string | undefined = undefined; + try { + num = optReduceState(state, action, reduce); + await call(action); + state.getState().removePatch(num) + } catch (e) { + console.error(e); + if(num) { + state.getState().rollback(num); + } + } +}