repos / starfx

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

starfx / store
Vlad · 14 Nov 24

store.ts

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