- 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
+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 }
+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;
+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+);