repos / starfx

supercharged async flow control library.
git clone https://github.com/neurosnap/starfx.git

commit
d0a0d68
parent
ed75824
author
Eric Bower
date
2024-02-23 20:28:16 +0000 UTC
feat: clear timers (#41)

feat: use `race` from `effection`

BREAKING CHANGE: replace `race` with `raceMap`
11 files changed,  +132, -40
M mod.ts
M action.ts
+17, -6
 1@@ -129,9 +129,20 @@ export function getIdFromAction(
 2 }
 3 
 4 export const API_ACTION_PREFIX = "";
 5-export const createAction = (type: string) => {
 6-  if (!type) throw new Error("createAction requires non-empty string");
 7-  const action = () => ({ type });
 8-  action.toString = () => type;
 9-  return action;
10-};
11+
12+export function createAction(actionType: string): () => Action;
13+export function createAction<P>(
14+  actionType: string,
15+): (p: P) => ActionWithPayload<P>;
16+export function createAction(actionType: string) {
17+  if (!actionType) {
18+    throw new Error("createAction requires non-empty string");
19+  }
20+  const fn = (payload?: unknown) => ({
21+    type: actionType,
22+    payload,
23+  });
24+  fn.toString = () => actionType;
25+
26+  return fn;
27+}
M deps.ts
+1, -0
1@@ -26,6 +26,7 @@ export {
2   ensure,
3   Err,
4   Ok,
5+  race,
6   resource,
7   run,
8   SignalQueueFactory,
M fx/race.ts
+1, -1
1@@ -5,7 +5,7 @@ interface OpMap<T = unknown> {
2   [key: string]: Callable<T>;
3 }
4 
5-export function race<T>(
6+export function raceMap<T>(
7   opMap: OpMap,
8 ): Operation<
9   {
M matcher.ts
+8, -3
 1@@ -2,17 +2,22 @@ import type { AnyAction } from "./types.ts";
 2 
 3 type ActionType = string;
 4 type GuardPredicate<G extends T, T = unknown> = (arg: T) => arg is G;
 5-type Predicate = (action: AnyAction) => boolean;
 6+type Predicate<Guard extends AnyAction = AnyAction> = (
 7+  action: Guard,
 8+) => boolean;
 9 type StringableActionCreator<A extends AnyAction = AnyAction> = {
10   (...args: unknown[]): A;
11   toString(): string;
12 };
13-type SubPattern = Predicate | StringableActionCreator | ActionType;
14+type SubPattern<Guard extends AnyAction = AnyAction> =
15+  | Predicate<Guard>
16+  | StringableActionCreator
17+  | ActionType;
18 export type Pattern = SubPattern | SubPattern[];
19 type ActionSubPattern<Guard extends AnyAction = AnyAction> =
20   | GuardPredicate<Guard, AnyAction>
21   | StringableActionCreator<Guard>
22-  | Predicate
23+  | Predicate<Guard>
24   | ActionType;
25 export type ActionPattern<Guard extends AnyAction = AnyAction> =
26   | ActionSubPattern<Guard>
M mod.ts
+1, -0
1@@ -16,6 +16,7 @@ export {
2   ensure,
3   Err,
4   Ok,
5+  race,
6   resource,
7   run,
8   sleep,
M query/mdw.ts
+17, -1
 1@@ -9,7 +9,7 @@ import type {
 2   RequiredApiRequest,
 3   ThunkCtx,
 4 } from "./types.ts";
 5-import type { Next } from "../types.ts";
 6+import type { AnyAction, Next } from "../types.ts";
 7 import { mergeRequest } from "./util.ts";
 8 import * as fetchMdw from "./fetch.ts";
 9 import { call, Callable } from "../deps.ts";
10@@ -93,6 +93,21 @@ export function* queryCtx<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
11   yield* next();
12 }
13 
14+/**
15+ * This middleware will take the result of `ctx.actions` and dispatch them
16+ * as a single batch.
17+ *
18+ * @remarks This is useful because sometimes there are a lot of actions that need dispatched
19+ * within the pipeline of the middleware and instead of dispatching them serially this
20+ * improves performance by only hitting the reducers once.
21+ */
22+export function* actions(ctx: { actions: AnyAction[] }, next: Next) {
23+  if (!ctx.actions) ctx.actions = [];
24+  yield* next();
25+  if (ctx.actions.length === 0) return;
26+  yield* put(ctx.actions);
27+}
28+
29 /**
30  * This middleware is a composition of many middleware used to faciliate
31  * the {@link createApi}.
32@@ -102,6 +117,7 @@ export function* queryCtx<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
33 export function api<Ctx extends ApiCtx = ApiCtx>() {
34   return compose<Ctx>([
35     err,
36+    actions,
37     queryCtx,
38     customKey,
39     fetchMdw.nameParser,
R store/query.ts => store/mdw.ts
+1, -18
 1@@ -1,7 +1,6 @@
 2 import type { ApiCtx, ThunkCtx } from "../query/mod.ts";
 3 import { compose } from "../compose.ts";
 4-import type { AnyAction, AnyState, Next } from "../types.ts";
 5-import { put } from "../action.ts";
 6+import type { AnyState, Next } from "../types.ts";
 7 import { select, updateStore } from "./fx.ts";
 8 import { LoaderOutput } from "./slice/loaders.ts";
 9 import { TableOutput } from "./slice/table.ts";
10@@ -15,7 +14,6 @@ export function store<
11   errorFn?: (ctx: Ctx) => string;
12 }) {
13   return compose<Ctx>([
14-    actions,
15     loaderApi(props.loaders, props.errorFn),
16     cache(props.cache),
17   ]);
18@@ -46,21 +44,6 @@ export function cache<Ctx extends ApiCtx = ApiCtx>(
19   };
20 }
21 
22-/**
23- * This middleware will take the result of `ctx.actions` and dispatch them
24- * as a single batch.
25- *
26- * @remarks This is useful because sometimes there are a lot of actions that need dispatched
27- * within the pipeline of the middleware and instead of dispatching them serially this
28- * improves performance by only hitting the reducers once.
29- */
30-export function* actions(ctx: { actions: AnyAction[] }, next: Next) {
31-  if (!ctx.actions) ctx.actions = [];
32-  yield* next();
33-  if (ctx.actions.length === 0) return;
34-  yield* put(ctx.actions);
35-}
36-
37 /**
38  * This middleware will track the status of a middleware fn
39  */
M store/mod.ts
+1, -1
1@@ -7,5 +7,5 @@ export * from "./slice/mod.ts";
2 export * from "./schema.ts";
3 export * from "./batch.ts";
4 export * from "./persist.ts";
5-import * as storeMdw from "./query.ts";
6+import * as storeMdw from "./mdw.ts";
7 export { storeMdw };
M supervisor.ts
+23, -9
 1@@ -1,8 +1,8 @@
 2-import { race } from "./fx/mod.ts";
 3-import { take } from "./action.ts";
 4-import { call, Callable, Operation, sleep, spawn, Task } from "./deps.ts";
 5+import { createAction, take } from "./action.ts";
 6+import { call, Callable, Operation, race, sleep, spawn, Task } from "./deps.ts";
 7 import type { ActionWithPayload, AnyAction } from "./types.ts";
 8 import type { CreateActionPayload } from "./query/mod.ts";
 9+import { getIdFromAction } from "./action.ts";
10 
11 const MS = 1000;
12 const SECONDS = 1 * MS;
13@@ -16,7 +16,7 @@ export function poll(parentTimer: number = 5 * SECONDS, cancelType?: string) {
14     const cancel = cancelType || actionType;
15     function* fire(action: { type: string }, timer: number) {
16       while (true) {
17-        yield* call(() => op(action));
18+        yield* op(action);
19         yield* sleep(timer);
20       }
21     }
22@@ -24,14 +24,16 @@ export function poll(parentTimer: number = 5 * SECONDS, cancelType?: string) {
23     while (true) {
24       const action = yield* take<{ timer?: number }>(actionType);
25       const timer = action.payload?.timer || parentTimer;
26-      yield* race({
27-        fire: () => call(() => fire(action, timer)),
28-        cancel: () => take(`${cancel}`),
29-      });
30+      yield* race([
31+        fire(action, timer),
32+        take(`${cancel}`) as Operation<void>,
33+      ]);
34     }
35   };
36 }
37 
38+export const clearTimers = createAction<string[]>("clear-timers");
39+
40 /**
41  * timer() will create a cache timer for each `key` inside
42  * of a starfx api endpoint.  `key` is a hash of the action type and payload.
43@@ -50,7 +52,19 @@ export function timer(timer: number = 5 * MINUTES) {
44 
45     function* activate(action: ActionWithPayload<CreateActionPayload>) {
46       yield* call(() => op(action));
47-      yield* sleep(timer);
48+      const idA = getIdFromAction(action);
49+
50+      const matchFn = (act: AnyAction) => {
51+        if (act.type !== `${clearTimers}`) return false;
52+        if (!act.payload) return false;
53+        const ids: string[] = act.payload || [];
54+        return ids.some((id) => idA === id || id === "*");
55+      };
56+      yield* race([
57+        sleep(timer),
58+        take(matchFn) as Operation<void>,
59+      ]);
60+
61       delete map[action.payload.key];
62     }
63 
M test/action.test.ts
+1, -1
1@@ -10,5 +10,5 @@ it(tests, "should return action type when stringified", () => {
2 
3 it(tests, "return object with type", () => {
4   const undo = createAction("UNDO");
5-  expect(undo()).toEqual({ type: `UNDO` });
6+  expect(undo()).toEqual({ type: `UNDO`, payload: undefined });
7 });
A test/timer.test.ts
+61, -0
 1@@ -0,0 +1,61 @@
 2+import { describe, expect, it } from "../test.ts";
 3+import { clearTimers, put, run, sleep, spawn, timer } from "../mod.ts";
 4+
 5+const tests = describe("timer()");
 6+
 7+it(tests, "should call thunk at most once every timer", async () => {
 8+  let called = 0;
 9+  await run(function* () {
10+    yield* spawn(function* () {
11+      yield* timer(10)("ACTION", function* () {
12+        called += 1;
13+      });
14+    });
15+    yield* put({ type: "ACTION", payload: { key: "my-key" } });
16+    yield* sleep(1);
17+    yield* put({ type: "ACTION", payload: { key: "my-key" } });
18+    yield* sleep(20);
19+    yield* put({ type: "ACTION", payload: { key: "my-key" } });
20+    yield* sleep(50);
21+  });
22+  expect(called).toBe(2);
23+});
24+
25+it(tests, "should let user cancel timer", async () => {
26+  let called = 0;
27+  await run(function* () {
28+    yield* spawn(function* () {
29+      yield* timer(10_000)("ACTION", function* () {
30+        called += 1;
31+      });
32+    });
33+    yield* put({ type: "ACTION", payload: { key: "my-key" } });
34+    yield* sleep(1);
35+    yield* put(clearTimers(["my-key"]));
36+    yield* put({ type: "ACTION", payload: { key: "my-key" } });
37+  });
38+  expect(called).toBe(2);
39+});
40+
41+it(tests, "should let user cancel timer with wildcard", async () => {
42+  let called = 0;
43+  await run(function* () {
44+    yield* spawn(function* () {
45+      yield* timer(10_000)("ACTION", function* () {
46+        called += 1;
47+      });
48+    });
49+    yield* spawn(function* () {
50+      yield* timer(10_000)("WOW", function* () {
51+        called += 1;
52+      });
53+    });
54+    yield* put({ type: "ACTION", payload: { key: "my-key" } });
55+    yield* put({ type: "WOW", payload: { key: "my-key" } });
56+    yield* sleep(1);
57+    yield* put(clearTimers(["*"]));
58+    yield* put({ type: "ACTION", payload: { key: "my-key" } });
59+    yield* put({ type: "WOW", payload: { key: "my-key" } });
60+  });
61+  expect(called).toBe(4);
62+});