Skip to content

Commit d0a8540

Browse files
authored
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
1 parent 794602b commit d0a8540

File tree

3 files changed

+55
-18
lines changed

3 files changed

+55
-18
lines changed

query/thunk.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ActionContext, API_ACTION_PREFIX, takeEvery } from "../action.ts";
22
import { compose } from "../compose.ts";
33
import { Callable, ensure, Ok, Operation, Signal } from "../deps.ts";
44
import { keepAlive, supervise } from "../fx/mod.ts";
5+
import { IdContext } from "../store/store.ts";
56
import { createKey } from "./create-key.ts";
67
import { isFn, isObject } from "./util.ts";
78

@@ -15,7 +16,6 @@ import type {
1516
Supervisor,
1617
ThunkCtx,
1718
} from "./types.ts";
18-
1919
export interface ThunksApi<Ctx extends ThunkCtx> {
2020
use: (fn: Middleware<Ctx>) => void;
2121
routes: () => Middleware<Ctx>;
@@ -82,6 +82,8 @@ export interface ThunksApi<Ctx extends ThunkCtx> {
8282
): CreateActionWithPayload<Gtx, P>;
8383
}
8484

85+
let id = 0;
86+
8587
/**
8688
* Creates a middleware pipeline.
8789
*
@@ -124,17 +126,17 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
124126
} = { supervisor: takeEvery },
125127
): ThunksApi<Ctx> {
126128
let signal: Signal<AnyAction, void> | undefined = undefined;
129+
let storeId: number | undefined = undefined;
127130
const middleware: Middleware<Ctx>[] = [];
128131
const visors: { [key: string]: Callable<unknown> } = {};
129132
const middlewareMap: { [key: string]: Middleware<Ctx> } = {};
130133
let dynamicMiddlewareMap: { [key: string]: Middleware<Ctx> } = {};
131134
const actionMap: {
132135
[key: string]: CreateActionWithPayload<Ctx, any>;
133136
} = {};
134-
const thunkId = `${Date.now().toString(36)}-${
135-
Math.random().toString(36).substring(2, 11)
136-
}`;
137-
let hasRegistered = false;
137+
const thunkId = id++;
138+
139+
const storeMap = new Map<number, Signal<AnyAction, void>>();
138140

139141
function* defaultMiddleware(_: Ctx, next: Next) {
140142
yield* next();
@@ -207,10 +209,10 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
207209

208210
visors[name] = curVisor;
209211

210-
// If signal is available, register immediately, otherwise defer
211-
if (signal) {
212-
signal.send({
213-
type: `${API_ACTION_PREFIX}REGISTER_THUNK_${thunkId}`,
212+
// If signal is already referenced, register immediately, otherwise defer
213+
for (const [storeId, storeSignal] of storeMap.entries()) {
214+
storeSignal.send({
215+
type: `${API_ACTION_PREFIX}REGISTER_THUNK_${storeId}_${thunkId}`,
214216
payload: curVisor,
215217
});
216218
}
@@ -253,23 +255,27 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
253255
}
254256

255257
function* register() {
256-
if (hasRegistered) {
258+
storeId = yield* IdContext;
259+
if (storeId && storeMap.has(storeId)) {
257260
console.warn("This thunk instance is already registered.");
258261
return;
259262
}
260-
hasRegistered = true;
263+
261264
signal = yield* ActionContext;
265+
storeMap.set(storeId, signal);
262266

263267
yield* ensure(function* () {
264-
hasRegistered = false;
268+
if (storeId) {
269+
storeMap.delete(storeId);
270+
}
265271
});
266272

267273
// Register any thunks created after signal is available
268274
yield* keepAlive(Object.values(visors));
269275

270276
// Spawn a watcher for further thunk matchingPairs
271277
yield* takeEvery(
272-
`${API_ACTION_PREFIX}REGISTER_THUNK_${thunkId}`,
278+
`${API_ACTION_PREFIX}REGISTER_THUNK_${storeId}_${thunkId}`,
273279
watcher as any,
274280
);
275281
}

store/store.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1+
import { ActionContext, API_ACTION_PREFIX, emit } from "../action.ts";
2+
import { BaseMiddleware, compose } from "../compose.ts";
13
import {
4+
createContext,
25
createScope,
36
createSignal,
47
enablePatches,
58
Ok,
69
produceWithPatches,
710
Scope,
811
} from "../deps.ts";
9-
import { BaseMiddleware, compose } from "../compose.ts";
10-
import type { AnyAction, AnyState, Next } from "../types.ts";
11-
import type { FxStore, Listener, StoreUpdater, UpdaterCtx } from "./types.ts";
1212
import { StoreContext, StoreUpdateContext } from "./context.ts";
13-
import { ActionContext, emit } from "../action.ts";
14-
import { API_ACTION_PREFIX } from "../action.ts";
1513
import { createRun } from "./run.ts";
1614

15+
import type { AnyAction, AnyState, Next } from "../types.ts";
16+
import type { FxStore, Listener, StoreUpdater, UpdaterCtx } from "./types.ts";
1717
const stubMsg = "This is merely a stub, not implemented";
1818

19+
let id = 0;
20+
1921
// https://github.com/reduxjs/redux/blob/4a6d2fb227ba119d3498a43fab8f53fe008be64c/src/createStore.ts#L344
2022
function observable() {
2123
return {
@@ -34,6 +36,8 @@ export interface CreateStore<S extends AnyState> {
3436
middleware?: BaseMiddleware<UpdaterCtx<S>>[];
3537
}
3638

39+
export const IdContext = createContext("starfx:id", 0);
40+
3741
export function createStore<S extends AnyState>({
3842
initialState,
3943
scope: initScope,
@@ -53,6 +57,7 @@ export function createStore<S extends AnyState>({
5357

5458
const signal = createSignal<AnyAction, void>();
5559
scope.set(ActionContext, signal);
60+
scope.set(IdContext, id++);
5661

5762
function getScope() {
5863
return scope;

test/thunk.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "../mod.ts";
99
import { createStore, updateStore } from "../store/mod.ts";
1010
import { assertLike, asserts, describe, it } from "../test.ts";
11+
1112
import type { Next, ThunkCtx } from "../mod.ts";
1213

1314
// deno-lint-ignore no-explicit-any
@@ -625,3 +626,28 @@ it(
625626
);
626627
},
627628
);
629+
630+
it(
631+
tests,
632+
"should allow multiple stores to register a thunk",
633+
() => {
634+
const api1 = createThunks<RoboCtx>();
635+
api1.use(api1.routes());
636+
const storeA = createStore({ initialState: {} });
637+
const storeB = createStore({ initialState: {} });
638+
storeA.run(api1.register);
639+
storeB.run(api1.register);
640+
let acc = "";
641+
const action = api1.create("/users", function* () {
642+
acc += "b";
643+
});
644+
storeA.dispatch(action());
645+
storeB.dispatch(action());
646+
647+
asserts.assertEquals(
648+
acc,
649+
"bb",
650+
"Expected 'bb' after first API call, but got: " + acc,
651+
);
652+
},
653+
);

0 commit comments

Comments
 (0)