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;