Vlad
·
14 Nov 24
thunk.ts
1import { ActionContext, API_ACTION_PREFIX, takeEvery } from "../action.ts";
2import { compose } from "../compose.ts";
3import { Callable, ensure, Ok, Operation, Signal } from "../deps.ts";
4import { keepAlive, supervise } from "../fx/mod.ts";
5import { IdContext } from "../store/store.ts";
6import { createKey } from "./create-key.ts";
7import { isFn, isObject } from "./util.ts";
8
9import type { ActionWithPayload, AnyAction, Next, Payload } from "../types.ts";
10import type {
11 CreateAction,
12 CreateActionPayload,
13 CreateActionWithPayload,
14 Middleware,
15 MiddlewareCo,
16 Supervisor,
17 ThunkCtx,
18} from "./types.ts";
19export interface ThunksApi<Ctx extends ThunkCtx> {
20 use: (fn: Middleware<Ctx>) => void;
21 routes: () => Middleware<Ctx>;
22 bootup: Callable<void>;
23 register: Callable<void>;
24 reset: () => void;
25
26 /**
27 * Name only
28 */
29 create(name: string): CreateAction<Ctx>;
30 create<P>(
31 name: string,
32 ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
33
34 /**
35 * Name and options
36 */
37 create(name: string, req: { supervisor?: Supervisor }): CreateAction<Ctx>;
38 create<P>(
39 name: string,
40 req: { supervisor?: Supervisor },
41 ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
42
43 /**
44 * Name and middleware
45 */
46 create(name: string, fn: MiddlewareCo<Ctx>): CreateAction<Ctx>;
47 create<Gtx extends Ctx = Ctx>(
48 name: string,
49 fn: MiddlewareCo<Gtx>,
50 ): CreateAction<Gtx>;
51 create<P>(
52 name: string,
53 fn: MiddlewareCo<Omit<Ctx, "payload"> & Payload<P>>,
54 ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
55 create<P, Gtx extends Ctx = Ctx>(
56 name: string,
57 fn: MiddlewareCo<Gtx>,
58 ): CreateActionWithPayload<Gtx, P>;
59
60 /*
61 * Name, options, and middleware
62 */
63 create(
64 name: string,
65 req: { supervisor?: Supervisor },
66 fn: MiddlewareCo<Ctx>,
67 ): CreateAction<Ctx>;
68 create<Gtx extends Ctx = Ctx>(
69 name: string,
70 req: { supervisor?: Supervisor },
71 fn: MiddlewareCo<Gtx>,
72 ): CreateAction<Gtx>;
73 create<P>(
74 name: string,
75 req: { supervisor?: Supervisor },
76 fn: MiddlewareCo<Omit<Ctx, "payload"> & Payload<P>>,
77 ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
78 create<P, Gtx extends Ctx = Ctx>(
79 name: string,
80 req: { supervisor?: Supervisor },
81 fn: MiddlewareCo<Gtx>,
82 ): CreateActionWithPayload<Gtx, P>;
83}
84
85let id = 0;
86
87/**
88 * Creates a middleware pipeline.
89 *
90 * @remarks
91 * This middleware pipeline is almost exactly like koa's middleware system.
92 * See {@link https://koajs.com}
93 *
94 * @example
95 * ```ts
96 * import { createThunks } from 'starfx';
97 *
98 * const thunks = createThunks();
99 * thunks.use(function* (ctx, next) {
100 * console.log('beginning');
101 * yield* next();
102 * console.log('end');
103 * });
104 * thunks.use(thunks.routes());
105 *
106 * const doit = thunks.create('do-something', function*(ctx, next) {
107 * console.log('middle');
108 * yield* next();
109 * console.log('middle end');
110 * });
111 *
112 * // ...
113 *
114 * store.dispatch(doit());
115 * // beginning
116 * // middle
117 * // middle end
118 * // end
119 * ```
120 */
121export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
122 {
123 supervisor = takeEvery,
124 }: {
125 supervisor?: Supervisor;
126 } = { supervisor: takeEvery },
127): ThunksApi<Ctx> {
128 let signal: Signal<AnyAction, void> | undefined = undefined;
129 let storeId: number | undefined = undefined;
130 const middleware: Middleware<Ctx>[] = [];
131 const visors: { [key: string]: Callable<unknown> } = {};
132 const middlewareMap: { [key: string]: Middleware<Ctx> } = {};
133 let dynamicMiddlewareMap: { [key: string]: Middleware<Ctx> } = {};
134 const actionMap: {
135 [key: string]: CreateActionWithPayload<Ctx, any>;
136 } = {};
137 const thunkId = id++;
138
139 const storeMap = new Map<number, Signal<AnyAction, void>>();
140
141 function* defaultMiddleware(_: Ctx, next: Next) {
142 yield* next();
143 }
144
145 const createType = (post: string) => `${API_ACTION_PREFIX}${post}`;
146
147 function* onApi<P extends CreateActionPayload>(
148 action: ActionWithPayload<P>,
149 ): Operation<Ctx> {
150 const { name, key, options } = action.payload;
151 const actionFn = actionMap[name];
152 const ctx = {
153 action,
154 name,
155 key,
156 payload: options,
157 actionFn,
158 result: Ok(undefined),
159 } as unknown as Ctx;
160 const fn = compose(middleware);
161 yield* fn(ctx);
162 return ctx;
163 }
164
165 function create(name: string, ...args: any[]) {
166 if (Object.hasOwn(visors, name)) {
167 const msg =
168 `[${name}] already exists, do you have two thunks with the same name?`;
169 console.warn(msg);
170 }
171
172 const type = createType(name);
173 const action = (payload?: any) => {
174 return { type, payload };
175 };
176 let req = null;
177 let fn = null;
178 if (args.length === 2) {
179 req = args[0];
180 fn = args[1];
181 }
182
183 if (args.length === 1) {
184 if (isFn(args[0]) || Array.isArray(args[0])) {
185 fn = args[0];
186 } else {
187 req = args[0];
188 }
189 }
190
191 if (req && !isObject(req)) {
192 throw new Error("Options must be an object");
193 }
194
195 if (fn && Array.isArray(fn)) {
196 fn = compose(fn);
197 }
198
199 if (fn && !isFn(fn)) {
200 throw new Error("Middleware must be a function");
201 }
202
203 middlewareMap[name] = fn || defaultMiddleware;
204
205 const tt = req ? (req as any).supervisor : supervisor;
206 function* curVisor() {
207 yield* tt(type, onApi);
208 }
209
210 visors[name] = curVisor;
211
212 // If signal is already referenced, register immediately, otherwise defer
213 for (const [storeId, storeSignal] of storeMap.entries()) {
214 storeSignal.send({
215 type: `${API_ACTION_PREFIX}REGISTER_THUNK_${storeId}_${thunkId}`,
216 payload: curVisor,
217 });
218 }
219
220 const errMsg =
221 `[${name}] is being called before its thunk has been registered. ` +
222 "Run `store.run(thunks.register)` where `thunks` is the name of your `createThunks` or `createApi` variable.";
223
224 const actionFn = (options?: Ctx["payload"]) => {
225 if (!signal) {
226 console.warn(errMsg);
227 }
228 const key = createKey(name, options);
229 return action({ name, key, options });
230 };
231 actionFn.run = (action?: unknown): Operation<Ctx> => {
232 if (action && Object.hasOwn(action, "type")) {
233 return onApi(action as ActionWithPayload<CreateActionPayload>);
234 }
235 return onApi(actionFn(action));
236 };
237 actionFn.use = (fn: Middleware<Ctx>) => {
238 const cur = middlewareMap[name];
239 if (cur) {
240 dynamicMiddlewareMap[name] = compose([cur, fn]);
241 } else {
242 dynamicMiddlewareMap[name] = fn;
243 }
244 };
245 actionFn.toString = () => type;
246 actionFn._success = {};
247 actionFn._error = {};
248 actionMap[name] = actionFn;
249
250 return actionFn;
251 }
252
253 function* watcher(action: ActionWithPayload<Callable<unknown>>) {
254 yield* supervise(action.payload)();
255 }
256
257 function* register() {
258 storeId = yield* IdContext;
259 if (storeId && storeMap.has(storeId)) {
260 console.warn("This thunk instance is already registered.");
261 return;
262 }
263
264 signal = yield* ActionContext;
265 storeMap.set(storeId, signal);
266
267 yield* ensure(function* () {
268 if (storeId) {
269 storeMap.delete(storeId);
270 }
271 });
272
273 // Register any thunks created after signal is available
274 yield* keepAlive(Object.values(visors));
275
276 // Spawn a watcher for further thunk matchingPairs
277 yield* takeEvery(
278 `${API_ACTION_PREFIX}REGISTER_THUNK_${storeId}_${thunkId}`,
279 watcher as any,
280 );
281 }
282
283 function routes() {
284 function* router(ctx: Ctx, next: Next) {
285 const match = dynamicMiddlewareMap[ctx.name] || middlewareMap[ctx.name];
286 if (!match) {
287 yield* next();
288 return;
289 }
290
291 const result = yield* match(ctx, next);
292 return result;
293 }
294
295 return router;
296 }
297
298 function resetMdw() {
299 dynamicMiddlewareMap = {};
300 }
301
302 return {
303 use: (fn: Middleware<Ctx>) => {
304 middleware.push(fn);
305 },
306 create,
307 routes,
308 /**
309 * @deprecated use `register()` instead
310 */
311 bootup: register,
312 reset: resetMdw,
313 register,
314 };
315}