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;