repos / starfx

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

commit
d0a8540
parent
794602b
author
Vlad
date
2024-11-14 15:11:47 +0000 UTC
feat: support multiple stores registering the same thunk (#51)

* feat(thunk): enable multiple stores to register thunks with unique identifiers

* refactor(store): remove unused getStoreId function
3 files changed,  +55, -18
M query/thunk.ts
+19, -13
 1@@ -2,6 +2,7 @@ import { ActionContext, API_ACTION_PREFIX, takeEvery } from "../action.ts";
 2 import { compose } from "../compose.ts";
 3 import { Callable, ensure, Ok, Operation, Signal } from "../deps.ts";
 4 import { keepAlive, supervise } from "../fx/mod.ts";
 5+import { IdContext } from "../store/store.ts";
 6 import { createKey } from "./create-key.ts";
 7 import { isFn, isObject } from "./util.ts";
 8 
 9@@ -15,7 +16,6 @@ import type {
10   Supervisor,
11   ThunkCtx,
12 } from "./types.ts";
13-
14 export interface ThunksApi<Ctx extends ThunkCtx> {
15   use: (fn: Middleware<Ctx>) => void;
16   routes: () => Middleware<Ctx>;
17@@ -82,6 +82,8 @@ export interface ThunksApi<Ctx extends ThunkCtx> {
18   ): CreateActionWithPayload<Gtx, P>;
19 }
20 
21+let id = 0;
22+
23 /**
24  * Creates a middleware pipeline.
25  *
26@@ -124,6 +126,7 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
27   } = { supervisor: takeEvery },
28 ): ThunksApi<Ctx> {
29   let signal: Signal<AnyAction, void> | undefined = undefined;
30+  let storeId: number | undefined = undefined;
31   const middleware: Middleware<Ctx>[] = [];
32   const visors: { [key: string]: Callable<unknown> } = {};
33   const middlewareMap: { [key: string]: Middleware<Ctx> } = {};
34@@ -131,10 +134,9 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
35   const actionMap: {
36     [key: string]: CreateActionWithPayload<Ctx, any>;
37   } = {};
38-  const thunkId = `${Date.now().toString(36)}-${
39-    Math.random().toString(36).substring(2, 11)
40-  }`;
41-  let hasRegistered = false;
42+  const thunkId = id++;
43+
44+  const storeMap = new Map<number, Signal<AnyAction, void>>();
45 
46   function* defaultMiddleware(_: Ctx, next: Next) {
47     yield* next();
48@@ -207,10 +209,10 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
49 
50     visors[name] = curVisor;
51 
52-    // If signal is available, register immediately, otherwise defer
53-    if (signal) {
54-      signal.send({
55-        type: `${API_ACTION_PREFIX}REGISTER_THUNK_${thunkId}`,
56+    // If signal is already referenced, register immediately, otherwise defer
57+    for (const [storeId, storeSignal] of storeMap.entries()) {
58+      storeSignal.send({
59+        type: `${API_ACTION_PREFIX}REGISTER_THUNK_${storeId}_${thunkId}`,
60         payload: curVisor,
61       });
62     }
63@@ -253,15 +255,19 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
64   }
65 
66   function* register() {
67-    if (hasRegistered) {
68+    storeId = yield* IdContext;
69+    if (storeId && storeMap.has(storeId)) {
70       console.warn("This thunk instance is already registered.");
71       return;
72     }
73-    hasRegistered = true;
74+
75     signal = yield* ActionContext;
76+    storeMap.set(storeId, signal);
77 
78     yield* ensure(function* () {
79-      hasRegistered = false;
80+      if (storeId) {
81+        storeMap.delete(storeId);
82+      }
83     });
84 
85     // Register any thunks created after signal is available
86@@ -269,7 +275,7 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
87 
88     // Spawn a watcher for further thunk matchingPairs
89     yield* takeEvery(
90-      `${API_ACTION_PREFIX}REGISTER_THUNK_${thunkId}`,
91+      `${API_ACTION_PREFIX}REGISTER_THUNK_${storeId}_${thunkId}`,
92       watcher as any,
93     );
94   }
M store/store.ts
+10, -5
 1@@ -1,4 +1,7 @@
 2+import { ActionContext, API_ACTION_PREFIX, emit } from "../action.ts";
 3+import { BaseMiddleware, compose } from "../compose.ts";
 4 import {
 5+  createContext,
 6   createScope,
 7   createSignal,
 8   enablePatches,
 9@@ -6,16 +9,15 @@ import {
10   produceWithPatches,
11   Scope,
12 } from "../deps.ts";
13-import { BaseMiddleware, compose } from "../compose.ts";
14-import type { AnyAction, AnyState, Next } from "../types.ts";
15-import type { FxStore, Listener, StoreUpdater, UpdaterCtx } from "./types.ts";
16 import { StoreContext, StoreUpdateContext } from "./context.ts";
17-import { ActionContext, emit } from "../action.ts";
18-import { API_ACTION_PREFIX } from "../action.ts";
19 import { createRun } from "./run.ts";
20 
21+import type { AnyAction, AnyState, Next } from "../types.ts";
22+import type { FxStore, Listener, StoreUpdater, UpdaterCtx } from "./types.ts";
23 const stubMsg = "This is merely a stub, not implemented";
24 
25+let id = 0;
26+
27 // https://github.com/reduxjs/redux/blob/4a6d2fb227ba119d3498a43fab8f53fe008be64c/src/createStore.ts#L344
28 function observable() {
29   return {
30@@ -34,6 +36,8 @@ export interface CreateStore<S extends AnyState> {
31   middleware?: BaseMiddleware<UpdaterCtx<S>>[];
32 }
33 
34+export const IdContext = createContext("starfx:id", 0);
35+
36 export function createStore<S extends AnyState>({
37   initialState,
38   scope: initScope,
39@@ -53,6 +57,7 @@ export function createStore<S extends AnyState>({
40 
41   const signal = createSignal<AnyAction, void>();
42   scope.set(ActionContext, signal);
43+  scope.set(IdContext, id++);
44 
45   function getScope() {
46     return scope;
M test/thunk.test.ts
+26, -0
 1@@ -8,6 +8,7 @@ import {
 2 } from "../mod.ts";
 3 import { createStore, updateStore } from "../store/mod.ts";
 4 import { assertLike, asserts, describe, it } from "../test.ts";
 5+
 6 import type { Next, ThunkCtx } from "../mod.ts";
 7 
 8 // deno-lint-ignore no-explicit-any
 9@@ -625,3 +626,28 @@ it(
10     );
11   },
12 );
13+
14+it(
15+  tests,
16+  "should allow multiple stores to register a thunk",
17+  () => {
18+    const api1 = createThunks<RoboCtx>();
19+    api1.use(api1.routes());
20+    const storeA = createStore({ initialState: {} });
21+    const storeB = createStore({ initialState: {} });
22+    storeA.run(api1.register);
23+    storeB.run(api1.register);
24+    let acc = "";
25+    const action = api1.create("/users", function* () {
26+      acc += "b";
27+    });
28+    storeA.dispatch(action());
29+    storeB.dispatch(action());
30+
31+    asserts.assertEquals(
32+      acc,
33+      "bb",
34+      "Expected 'bb' after first API call, but got: " + acc,
35+    );
36+  },
37+);