repos / starfx

a micro-mvc framework for react apps
git clone https://github.com/neurosnap/starfx.git

starfx / src / store
Eric Bower  ·  2025-06-06

store.ts

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