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;