repos / starfx

supercharged async flow control library.
git clone https://github.com/neurosnap/starfx.git

starfx / store
Eric Bower · 04 Mar 24

store.ts

  1import {
  2  createScope,
  3  createSignal,
  4  enablePatches,
  5  Ok,
  6  produceWithPatches,
  7  Scope,
  8} from "../deps.ts";
  9import { BaseMiddleware, compose } from "../compose.ts";
 10import type { AnyAction, AnyState, Next } from "../types.ts";
 11import type { FxStore, Listener, StoreUpdater, UpdaterCtx } from "./types.ts";
 12import { StoreContext, StoreUpdateContext } from "./context.ts";
 13import { ActionContext, emit } from "../action.ts";
 14import { API_ACTION_PREFIX } from "../action.ts";
 15import { createRun } from "./run.ts";
 16
 17const stubMsg = "This is merely a stub, not implemented";
 18
 19// https://github.com/reduxjs/redux/blob/4a6d2fb227ba119d3498a43fab8f53fe008be64c/src/createStore.ts#L344
 20function observable() {
 21  return {
 22    subscribe: (_observer: unknown) => {
 23      throw new Error(stubMsg);
 24    },
 25    [Symbol.observable]() {
 26      return this;
 27    },
 28  };
 29}
 30
 31export interface CreateStore<S extends AnyState> {
 32  scope?: Scope;
 33  initialState: S;
 34  middleware?: BaseMiddleware<UpdaterCtx<S>>[];
 35}
 36
 37export function createStore<S extends AnyState>({
 38  initialState,
 39  scope: initScope,
 40  middleware = [],
 41}: CreateStore<S>): FxStore<S> {
 42  let scope: Scope;
 43  if (initScope) {
 44    scope = initScope;
 45  } else {
 46    const tuple = createScope();
 47    scope = tuple[0];
 48  }
 49
 50  let state = initialState;
 51  const listeners = new Set<Listener>();
 52  enablePatches();
 53
 54  const signal = createSignal<AnyAction, void>();
 55  scope.set(ActionContext, signal);
 56
 57  function getScope() {
 58    return scope;
 59  }
 60
 61  function getState() {
 62    return state;
 63  }
 64
 65  function subscribe(fn: Listener) {
 66    listeners.add(fn);
 67    return () => listeners.delete(fn);
 68  }
 69
 70  function* updateMdw(ctx: UpdaterCtx<S>, next: Next) {
 71    const upds: StoreUpdater<S>[] = [];
 72
 73    if (Array.isArray(ctx.updater)) {
 74      upds.push(...ctx.updater);
 75    } else {
 76      upds.push(ctx.updater);
 77    }
 78
 79    const [nextState, patches, _] = produceWithPatches(getState(), (draft) => {
 80      // TODO: check for return value inside updater
 81      // deno-lint-ignore no-explicit-any
 82      upds.forEach((updater) => updater(draft as any));
 83    });
 84    ctx.patches = patches;
 85
 86    // set the state!
 87    state = nextState;
 88
 89    yield* next();
 90  }
 91
 92  function* logMdw(ctx: UpdaterCtx<S>, next: Next) {
 93    dispatch({
 94      type: `${API_ACTION_PREFIX}store`,
 95      payload: ctx,
 96    });
 97    yield* next();
 98  }
 99
100  function* notifyChannelMdw(_: UpdaterCtx<S>, next: Next) {
101    const chan = yield* StoreUpdateContext;
102    yield* chan.send();
103    yield* next();
104  }
105
106  function* notifyListenersMdw(_: UpdaterCtx<S>, next: Next) {
107    listeners.forEach((f) => f());
108    yield* next();
109  }
110
111  function createUpdater() {
112    const fn = compose<UpdaterCtx<S>>([
113      updateMdw,
114      ...middleware,
115      logMdw,
116      notifyChannelMdw,
117      notifyListenersMdw,
118    ]);
119
120    return fn;
121  }
122
123  const mdw = createUpdater();
124  function* update(updater: StoreUpdater<S> | StoreUpdater<S>[]) {
125    const ctx = {
126      updater,
127      patches: [],
128      result: Ok(undefined),
129    };
130
131    yield* mdw(ctx);
132
133    if (!ctx.result.ok) {
134      dispatch({
135        type: `${API_ACTION_PREFIX}store`,
136        payload: ctx.result.error,
137      });
138    }
139
140    return ctx;
141  }
142
143  function dispatch(action: AnyAction | AnyAction[]) {
144    emit({ signal, action });
145  }
146
147  function getInitialState() {
148    return initialState;
149  }
150
151  function* reset(ignoreList: (keyof S)[] = []) {
152    return yield* update((s) => {
153      const keep = ignoreList.reduce<S>((acc, key) => {
154        acc[key] = s[key];
155        return acc;
156      }, { ...initialState });
157
158      Object.keys(s).forEach((key: keyof S) => {
159        s[key] = keep[key];
160      });
161    });
162  }
163
164  const store = {
165    getScope,
166    getState,
167    subscribe,
168    update,
169    reset,
170    run: createRun(scope),
171    // instead of actions relating to store mutation, they
172    // refer to pieces of business logic -- that can also mutate state
173    dispatch,
174    // stubs so `react-redux` is happy
175    // deno-lint-ignore no-explicit-any
176    replaceReducer<S = any>(
177      _nextReducer: (_s: S, _a: AnyAction) => void,
178    ): void {
179      throw new Error(stubMsg);
180    },
181    getInitialState,
182    [Symbol.observable]: observable,
183  };
184
185  // deno-lint-ignore no-explicit-any
186  store.getScope().set(StoreContext, store as any);
187  return store;
188}
189
190/**
191 * @deprecated use {@link createStore}
192 */
193export const configureStore = createStore;