- 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`
+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,
+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 {
+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,
+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 */
+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 };
+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
+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 });
+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+});