Skip to content

Commit 297fca0

Browse files
committed
integrate plugin logic from @wq/app and @wq/react
1 parent 72009ad commit 297fca0

File tree

10 files changed

+675
-1088
lines changed

10 files changed

+675
-1088
lines changed

.github/workflows/test.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ jobs:
3030
with:
3131
python-version: ${{ matrix.python-version }}
3232
- name: Install dependencies
33-
run: npm ci && npm run build
33+
run:
34+
echo "registry=https://npm.pkg.github.com/wq" > .npmrc
35+
echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" >> .npmrc
36+
npm ci && npm run build
3437
- name: Install native dependencies
3538
if: matrix.package == 'material-native' || matrix.package == 'map-gl-native'
3639
run: cd packages/$PACKAGE && npm ci

package-lock.json

+438-1,085
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/store/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,15 @@
2727
},
2828
"homepage": "https://wq.io/@wq/store",
2929
"dependencies": {
30+
"@wq/react": "^3.0.0-a1-dev1.g760eb56",
3031
"localforage": "^1.10.0",
3132
"redux": "^4.2.1",
3233
"redux-logger": "^3.0.6",
3334
"redux-persist": "^6.0.0"
35+
},
36+
"devDependencies": {
37+
"@testing-library/react": "^16.2.0",
38+
"react": "^19.0.0",
39+
"react-redux": "^9.2.0"
3440
}
3541
}

packages/store/src/Root.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React, { useEffect, useState } from "react";
2+
import { useConfig, withWQ } from "@wq/react";
3+
import { Provider as ReduxProvider } from "react-redux";
4+
import store from "./store.js";
5+
import { StoreContext } from "./hooks.js";
6+
7+
const defaultConfig = {
8+
store: {
9+
service: "",
10+
defaults: { format: "json" },
11+
},
12+
};
13+
14+
function Root({ store: instance = store, children }) {
15+
const config = useConfig("store"),
16+
[ready, setReady] = useState(Boolean(instance._store));
17+
18+
useEffect(() => {
19+
if (instance._store) {
20+
return;
21+
}
22+
init();
23+
async function init() {
24+
await instance.init(config);
25+
setReady(true);
26+
}
27+
}, [instance, config]);
28+
29+
if (!ready) {
30+
return null;
31+
}
32+
33+
return (
34+
<StoreContext.Provider value={instance}>
35+
<ReduxProvider store={instance._store}>{children}</ReduxProvider>
36+
</StoreContext.Provider>
37+
);
38+
}
39+
40+
export default withWQ(Root, {
41+
defaults: { config: defaultConfig },
42+
});

packages/store/src/__tests__/react.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { render, waitFor } from "@testing-library/react";
2+
import React, { useEffect } from "react";
3+
import ds, { Root, usePluginReducer } from "../index.js";
4+
5+
ds.use({
6+
name: "myPlugin",
7+
async init() {
8+
await new Promise((resolve) => setTimeout(resolve, 10));
9+
this.someInit = true;
10+
},
11+
actions: {
12+
setName(name) {
13+
return { type: "SET_NAME", name };
14+
},
15+
},
16+
reducer(state, action) {
17+
if (action.type === "SET_NAME") {
18+
return { ...state, name: action.name };
19+
}
20+
return state || { name: null };
21+
},
22+
});
23+
24+
function TestComponent() {
25+
const [{ name }, { setName }] = usePluginReducer("myPlugin");
26+
useEffect(() => {
27+
setName("Test");
28+
}, []);
29+
return <div>{name}</div>;
30+
}
31+
32+
test("usePluginReducer", async () => {
33+
expect(() => ds.use({ name: "myPlugin" })).toThrow(
34+
"main store already has a plugin named myPlugin!"
35+
);
36+
37+
render(
38+
<Root>
39+
<TestComponent />
40+
</Root>
41+
);
42+
43+
await waitFor(() => expect(ds.getState()["myPlugin"].name).toBe("Test"));
44+
45+
expect(ds.plugins.myPlugin.someInit).toBe(true);
46+
47+
expect(() => ds.use({ name: "anotherPlugin" })).toThrow(
48+
"Store already initialized!"
49+
);
50+
});

packages/store/src/hooks.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { createContext, useContext } from "react";
2+
import { useSelector } from "react-redux";
3+
4+
// Adapted from @wq/react
5+
6+
const selectors = {};
7+
8+
function getSelector(name) {
9+
if (!selectors[name]) {
10+
selectors[name] = (state) => state[name];
11+
}
12+
return selectors[name];
13+
}
14+
15+
export const StoreContext = createContext(null);
16+
17+
export function useStore() {
18+
return useContext(StoreContext);
19+
}
20+
21+
export function usePluginActions(name) {
22+
const { plugins } = useStore();
23+
return plugins[name];
24+
}
25+
26+
export function usePluginState(name) {
27+
const plugin = usePluginActions(name),
28+
pluginState = useSelector(getSelector(name));
29+
30+
if (plugin) {
31+
return pluginState;
32+
} else {
33+
return null;
34+
}
35+
}
36+
37+
export function usePluginReducer(name) {
38+
const plugin = usePluginActions(name),
39+
pluginState = useSelector(getSelector(name));
40+
41+
if (plugin) {
42+
return [pluginState, plugin];
43+
} else {
44+
return [null, null];
45+
}
46+
}

packages/store/src/index.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import ds, { Store, getStore } from "./store.js";
22
import * as storage from "./storage.js";
3+
import Root from "./Root.js";
34

45
ds.setEngine(storage);
56

67
export default ds;
7-
export { Store, getStore };
8+
export { Store, getStore, Root };
9+
export * from "./hooks.js";

packages/store/src/index.native.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import ds, { Store, getStore } from "./store.js";
22
import * as storage from "./storage.native.js";
3+
import Root from "./Root.js";
34

45
ds.setEngine(storage);
56

67
export default ds;
7-
export { Store, getStore };
8+
export { Store, getStore, Root };
9+
export * from "./hooks.js";

packages/store/src/plugins.js

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Adapted from @wq/app
2+
3+
let pcount = 0;
4+
5+
export function registerPlugin(store, plugin) {
6+
if (store._store) {
7+
throw new Error("Store already initialized!");
8+
}
9+
if (Array.isArray(plugin)) {
10+
plugin.forEach((p) => registerPlugin(store, p));
11+
return;
12+
}
13+
if (plugin.dependencies) {
14+
registerPlugin(store, plugin.dependencies);
15+
}
16+
if (store.plugins[plugin.name]) {
17+
if (store.plugins[plugin.name] === plugin) {
18+
return;
19+
} else {
20+
throw new Error(
21+
`${store.name} store already has a plugin named ${plugin.name}!`
22+
);
23+
}
24+
}
25+
pcount++;
26+
if (!plugin.name) {
27+
plugin.name = "plugin" + pcount;
28+
}
29+
store.plugins[plugin.name] = plugin;
30+
plugin.store = store;
31+
}
32+
33+
export function applyPlugins(store, config) {
34+
for (const [name, plugin] of Object.entries(store.plugins)) {
35+
if (plugin.ajax) {
36+
config.ajax = plugin.ajax.bind(plugin);
37+
}
38+
if (plugin.reducer) {
39+
let persist = false,
40+
restore = null;
41+
if (typeof plugin.persist === "function") {
42+
persist = plugin.persist.bind(plugin);
43+
restore = plugin.restore
44+
? plugin.restore.bind(plugin)
45+
: (state) => state;
46+
} else if (plugin.persist) {
47+
persist = true;
48+
}
49+
store.addReducer(
50+
name,
51+
(state, action) => plugin.reducer(state, action),
52+
persist,
53+
restore
54+
);
55+
}
56+
if (plugin.actions) {
57+
Object.assign(plugin, store.bindActionCreators(plugin.actions));
58+
}
59+
if (plugin.subscriber) {
60+
store.subscribe(() => plugin.subscriber(store.getState()));
61+
}
62+
}
63+
}
64+
65+
export async function initPlugins(store, config) {
66+
for (const plugin of Object.values(store.plugins)) {
67+
if (plugin.init) {
68+
await plugin.init(config && config[plugin.name]);
69+
}
70+
}
71+
}

packages/store/src/store.js

+12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import logger from "redux-logger";
99
import { persistStore, persistReducer, createTransform } from "redux-persist";
1010
import autoMergeLevel2 from "redux-persist/lib/stateReconciler/autoMergeLevel2.js";
11+
import { registerPlugin, applyPlugins, initPlugins } from "./plugins.js";
1112

1213
const REMOVE = "@@KVP_REMOVE";
1314
const SET = "@@KVP_SET";
@@ -26,6 +27,8 @@ class Store {
2627
throw name + " store already exists!";
2728
}
2829
this.name = name;
30+
this.plugins = {};
31+
2932
_stores[name] = this;
3033

3134
this.debug = false;
@@ -62,11 +65,16 @@ class Store {
6265
);
6366
}
6467

68+
use(plugin) {
69+
registerPlugin(this, plugin);
70+
}
71+
6572
setEngine({ createStorage, serialize, deserialize }) {
6673
this.engine = { createStorage, serialize, deserialize };
6774
}
6875

6976
init(opts = {}) {
77+
applyPlugins(this, opts);
7078
var self = this;
7179
var optlist = [
7280
"debug",
@@ -132,6 +140,10 @@ class Store {
132140
this._store.subscribe(fn)
133141
);
134142
this._deferActions.forEach(this._store.dispatch);
143+
144+
return this.ready.then(() => {
145+
return initPlugins(this, opts);
146+
});
135147
}
136148

137149
dispatch(action) {

0 commit comments

Comments
 (0)