repos / starfx

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

starfx / src
Eric Bower  ·  2025-06-06

supervisor.ts

 1import {
 2  type Callable,
 3  type Operation,
 4  type Task,
 5  call,
 6  race,
 7  sleep,
 8  spawn,
 9} from "effection";
10import { createAction, take } from "./action.js";
11import { getIdFromAction } from "./action.js";
12import type { CreateActionPayload } from "./query/index.js";
13import type { ActionWithPayload, AnyAction } from "./types.js";
14
15const MS = 1000;
16const SECONDS = 1 * MS;
17const MINUTES = 60 * SECONDS;
18
19export function poll(parentTimer: number = 5 * SECONDS, cancelType?: string) {
20  return function* poller<T>(
21    actionType: string,
22    op: (action: AnyAction) => Operation<T>,
23  ): Operation<T> {
24    const cancel = cancelType || actionType;
25    function* fire(action: { type: string }, timer: number) {
26      while (true) {
27        yield* op(action);
28        yield* sleep(timer);
29      }
30    }
31
32    while (true) {
33      const action = yield* take<{ timer?: number }>(actionType);
34      const timer = action.payload?.timer || parentTimer;
35      yield* race([fire(action, timer), take(`${cancel}`) as Operation<void>]);
36    }
37  };
38}
39
40type ClearTimerPayload = string | { type: string; payload: { key: string } };
41
42export const clearTimers = createAction<
43  ClearTimerPayload | ClearTimerPayload[]
44>("clear-timers");
45
46/**
47 * timer() will create a cache timer for each `key` inside
48 * of a starfx api endpoint.  `key` is a hash of the action type and payload.
49 *
50 * Why do we want this?  If we have an api endpoint to fetch a single app: `fetchApp({ id: 1 })`
51 * if we don't set a timer per key then all calls to `fetchApp` will be on a timer.
52 * So if we call `fetchApp({ id: 1 })` and then `fetchApp({ id: 2 })` if we use a normal
53 * cache timer then the second call will not send an http request.
54 */
55export function timer(timer: number = 5 * MINUTES) {
56  return function* onTimer(
57    actionType: string,
58    op: (action: AnyAction) => Callable<unknown>,
59  ) {
60    const map: { [key: string]: Task<unknown> } = {};
61
62    function* activate(action: ActionWithPayload<CreateActionPayload>) {
63      yield* call(() => op(action));
64      const idA = getIdFromAction(action);
65
66      const matchFn = (
67        act: ActionWithPayload<ClearTimerPayload | ClearTimerPayload[]>,
68      ) => {
69        if (act.type !== `${clearTimers}`) return false;
70        if (!act.payload) return false;
71        const ids = Array.isArray(act.payload) ? act.payload : [act.payload];
72        return ids.some((id) => {
73          if (id === "*") {
74            return true;
75          }
76          if (typeof id === "string") {
77            return idA === id;
78          }
79          return idA === getIdFromAction(id);
80        });
81      };
82      yield* race([sleep(timer), take(matchFn as any) as Operation<void>]);
83
84      delete map[action.payload.key];
85    }
86
87    while (true) {
88      const action = yield* take<CreateActionPayload>(`${actionType}`);
89      const key = action.payload.key;
90      if (!map[key]) {
91        const task = yield* spawn(function* () {
92          yield* activate(action);
93        });
94        map[key] = task;
95      }
96    }
97  };
98}