react-native-hox 0.0.1-beta → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,69 +1,30 @@
1
- import { useRef, useSyncExternalStore } from 'react';
1
+ import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector';
2
+ import AsyncStorage from '@react-native-async-storage/async-storage';
2
3
 
3
4
  // src/createModel.ts
4
5
 
5
- // src/config.ts
6
- function getDefaultWebStorage() {
7
- try {
8
- const anyGlobal = globalThis;
9
- const storage = anyGlobal == null ? void 0 : anyGlobal.localStorage;
10
- if (!storage) {
11
- return void 0;
12
- }
13
- const testKey = "__react_native_hox_test__";
14
- storage.setItem(testKey, "1");
15
- storage.removeItem(testKey);
16
- return {
17
- getItem: (key) => storage.getItem(key),
18
- setItem: (key, value) => storage.setItem(key, value),
19
- removeItem: (key) => storage.removeItem(key)
20
- };
21
- } catch {
22
- return void 0;
6
+ // src/vanilla.ts
7
+ function isPlainObject(value) {
8
+ if (Object.prototype.toString.call(value) !== "[object Object]") {
9
+ return false;
23
10
  }
11
+ const proto = Object.getPrototypeOf(value);
12
+ return proto === Object.prototype || proto === null;
24
13
  }
25
- var config = {
26
- storage: getDefaultWebStorage()
27
- };
28
- var setup = (options) => {
29
- Object.assign(config, options);
30
- };
31
- var getConfig = () => config;
32
-
33
- // src/vanilla.ts
34
14
  var createStore = (initialState) => {
35
15
  let state = initialState;
36
16
  const listeners = /* @__PURE__ */ new Set();
37
- const getState = () => state;
38
- const setState = (partial, replace = false) => {
17
+ const setState = (partial, replace) => {
39
18
  const nextState = typeof partial === "function" ? partial(state) : partial;
40
- if (Object.is(nextState, state)) {
41
- return;
42
- }
43
- const previousState = state;
44
- const isNextObject = typeof nextState === "object" && nextState !== null;
45
- const isPrevObject = typeof previousState === "object" && previousState !== null;
46
- const shouldReplace = replace || !isNextObject || !isPrevObject;
47
- if (shouldReplace) {
48
- state = nextState;
49
- } else {
50
- const patch = nextState;
51
- const prevObj = previousState;
52
- let changed = false;
53
- for (const [key, value] of Object.entries(patch)) {
54
- if (!Object.is(prevObj[key], value)) {
55
- changed = true;
56
- break;
57
- }
58
- }
59
- if (!changed) {
60
- return;
61
- }
62
- state = Object.assign({}, previousState, nextState);
19
+ if (!Object.is(nextState, state)) {
20
+ const previousState = state;
21
+ const shouldReplace = replace != null ? replace : !isPlainObject(state) || !isPlainObject(nextState);
22
+ state = shouldReplace ? nextState : Object.assign({}, state, nextState);
23
+ listeners.forEach((listener) => listener(state, previousState));
63
24
  }
64
- const currentListeners = new Set(listeners);
65
- currentListeners.forEach((listener) => listener(state, previousState));
66
25
  };
26
+ const getState = () => state;
27
+ const getInitialState = () => initialState;
67
28
  const subscribe = (listener) => {
68
29
  listeners.add(listener);
69
30
  return () => listeners.delete(listener);
@@ -71,11 +32,11 @@ var createStore = (initialState) => {
71
32
  const destroy = () => {
72
33
  listeners.clear();
73
34
  };
74
- return { getState, setState, subscribe, destroy };
35
+ return { getState, getInitialState, setState, subscribe, destroy };
75
36
  };
76
37
 
77
38
  // src/utils.ts
78
- var shallow = (objA, objB) => {
39
+ function shallow(objA, objB) {
79
40
  if (Object.is(objA, objB)) {
80
41
  return true;
81
42
  }
@@ -93,7 +54,7 @@ var shallow = (objA, objB) => {
93
54
  }
94
55
  }
95
56
  return true;
96
- };
57
+ }
97
58
  function debounce(func, wait) {
98
59
  let timeout;
99
60
  let lastArgs;
@@ -105,6 +66,7 @@ function debounce(func, wait) {
105
66
  timeout = setTimeout(() => {
106
67
  func.apply(context, args);
107
68
  lastArgs = void 0;
69
+ timeout = void 0;
108
70
  }, wait);
109
71
  };
110
72
  debounced.flush = () => {
@@ -112,162 +74,172 @@ function debounce(func, wait) {
112
74
  clearTimeout(timeout);
113
75
  func.apply(context, lastArgs);
114
76
  lastArgs = void 0;
77
+ timeout = void 0;
115
78
  }
116
79
  };
117
80
  debounced.cancel = () => {
118
81
  clearTimeout(timeout);
119
82
  lastArgs = void 0;
83
+ timeout = void 0;
120
84
  };
121
85
  return debounced;
122
86
  }
123
87
 
124
88
  // src/createModel.ts
89
+ var { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports;
90
+ var identity = (v) => v;
91
+ function isInvalidHookCallError(error) {
92
+ return error instanceof Error && /Invalid hook call|Hooks can only be called/i.test(error.message);
93
+ }
125
94
  function createModel(initialState, options) {
95
+ var _a;
126
96
  const initialValue = typeof initialState === "function" ? initialState() : initialState;
127
97
  const store = createStore(initialValue);
128
- setupModelSideEffects(store, options);
129
- const model = bindStore(store);
130
- model.setState = store.setState;
131
- model.getState = store.getState;
132
- model.destroy = store.destroy;
133
- model.reset = () => store.setState(initialValue, true);
134
- return model;
135
- }
136
- function bindStore(store) {
137
- const subscribe = (onStoreChange) => store.subscribe(() => onStoreChange());
138
- const useModel = ((selector, equalityFn = shallow) => {
139
- const selectorRef = useRef(selector);
140
- selectorRef.current = selector;
141
- const equalityRef = useRef(equalityFn);
142
- equalityRef.current = equalityFn;
143
- const hasSelectionRef = useRef(false);
144
- const lastStateRef = useRef(null);
145
- const lastSelectionRef = useRef(null);
146
- const getSelection = () => {
147
- const state = store.getState();
148
- const selectorFn = selectorRef.current;
149
- const nextSelection = selectorFn ? selectorFn(state) : state;
150
- if (hasSelectionRef.current) {
151
- if (Object.is(state, lastStateRef.current)) {
152
- return lastSelectionRef.current;
153
- }
154
- if (equalityRef.current(lastSelectionRef.current, nextSelection)) {
155
- lastStateRef.current = state;
156
- return lastSelectionRef.current;
157
- }
158
- }
159
- hasSelectionRef.current = true;
160
- lastStateRef.current = state;
161
- lastSelectionRef.current = nextSelection;
162
- return nextSelection;
163
- };
164
- return useSyncExternalStore(subscribe, getSelection, getSelection);
165
- });
166
- Object.defineProperty(useModel, "data", {
98
+ const logger = (_a = options == null ? void 0 : options.logger) != null ? _a : console;
99
+ setupModelSideEffects(store, options, logger);
100
+ let isDestroyed = false;
101
+ const setState = (partial, replace) => {
102
+ var _a2;
103
+ if (isDestroyed && ((_a2 = options == null ? void 0 : options.strict) == null ? void 0 : _a2.forbidSetStateAfterDestroy)) {
104
+ const error = new Error("[react-native-hox] model \u5DF2\u9500\u6BC1\uFF0C\u65E0\u6CD5\u518D\u8C03\u7528 setState\u3002");
105
+ logger.error(error);
106
+ throw error;
107
+ }
108
+ store.setState(partial, replace);
109
+ };
110
+ const destroy = () => {
111
+ isDestroyed = true;
112
+ store.destroy();
113
+ };
114
+ const reset = () => store.setState(initialValue, true);
115
+ const subscribe = createSubscribe(store);
116
+ const data = {
117
+ getState: store.getState,
118
+ setState,
119
+ subscribe,
120
+ destroy,
121
+ reset
122
+ };
123
+ Object.defineProperty(data, "state", {
167
124
  get: () => store.getState()
168
125
  });
169
- useModel.getState = store.getState;
170
- useModel.setState = store.setState;
171
- useModel.destroy = store.destroy;
172
- useModel.reset = () => {
126
+ const hook = createModelHook(store);
127
+ const model = {
128
+ getState: hook,
129
+ useState: hook,
130
+ use: hook,
131
+ // Add 'use' alias
132
+ setState,
133
+ reset,
134
+ destroy,
135
+ data
136
+ };
137
+ const modelFn = function(selector, equalityFn) {
138
+ return hook(selector, equalityFn);
173
139
  };
174
- useModel.useState = useModel;
175
- useModel.use = useModel;
176
- const baseSubscribe = store.subscribe;
177
- useModel.subscribe = (arg1, arg2, arg3) => {
140
+ Object.assign(modelFn, model);
141
+ return modelFn;
142
+ }
143
+ function createSubscribe(store) {
144
+ return ((arg1, arg2, arg3) => {
178
145
  var _a;
179
146
  if (typeof arg1 === "function" && typeof arg2 !== "function") {
180
- return baseSubscribe(arg1);
147
+ return store.subscribe(arg1);
181
148
  }
182
149
  const selector = arg1;
183
150
  const listener = arg2;
184
151
  const options = arg3 != null ? arg3 : {};
185
152
  const equalityFn = (_a = options.equalityFn) != null ? _a : Object.is;
186
- let prevSelected = selector(store.getState());
153
+ let lastSelected = selector(store.getState());
187
154
  if (options.fireImmediately) {
188
- listener(prevSelected, prevSelected);
155
+ listener(lastSelected, lastSelected);
189
156
  }
190
- return baseSubscribe((nextState, prevState) => {
157
+ return store.subscribe((nextState) => {
191
158
  const nextSelected = selector(nextState);
192
- const prevSelectedFromPrevState = selector(prevState);
193
- if (equalityFn(prevSelectedFromPrevState, nextSelected)) {
159
+ if (equalityFn(lastSelected, nextSelected)) {
194
160
  return;
195
161
  }
196
- prevSelected = nextSelected;
197
- listener(nextSelected, prevSelectedFromPrevState);
162
+ const prevSelected = lastSelected;
163
+ lastSelected = nextSelected;
164
+ listener(nextSelected, prevSelected);
198
165
  });
199
- };
166
+ });
167
+ }
168
+ function createModelHook(store) {
169
+ const useModel = ((selector, equalityFn = shallow) => {
170
+ try {
171
+ return useSyncExternalStoreWithSelector(
172
+ store.subscribe,
173
+ store.getState,
174
+ store.getInitialState,
175
+ selector != null ? selector : identity,
176
+ equalityFn
177
+ );
178
+ } catch (error) {
179
+ if (isInvalidHookCallError(error)) {
180
+ throw new Error(
181
+ "[react-native-hox] \u4F60\u6B63\u5728\u7EC4\u4EF6\u5916\u8C03\u7528 Hook \u8BFB\u53D6\u72B6\u6001\uFF1B\u8BF7\u5728\u7EC4\u4EF6\u5185\u4F7F\u7528 `model()` / `model.use()` / `model.getState()` / `model.useState()`\uFF0C\u7EC4\u4EF6\u5916\u8BF7\u4F7F\u7528 `model.data.getState()` / `model.data.state`\u3002"
182
+ );
183
+ }
184
+ throw error;
185
+ }
186
+ });
200
187
  return useModel;
201
188
  }
202
- function setupModelSideEffects(store, options) {
203
- var _a, _b, _c;
204
- const { storage } = getConfig();
189
+ function setupModelSideEffects(store, options, logger) {
190
+ var _a, _b, _c, _d;
205
191
  const persist = typeof (options == null ? void 0 : options.persist) === "string" ? { key: options.persist } : options == null ? void 0 : options.persist;
206
192
  const persistKey = persist == null ? void 0 : persist.key;
207
- const syncEnabled = !!(options == null ? void 0 : options.sync);
193
+ const storage = (_a = persist == null ? void 0 : persist.storage) != null ? _a : AsyncStorage;
208
194
  if (!persistKey) {
209
- if (syncEnabled) {
210
- console.warn(
211
- "[react-native-hox] `sync` \u9700\u8981 `persist` \u63D0\u4F9B key \u624D\u80FD\u5EFA\u7ACB\u901A\u9053"
212
- );
213
- }
214
195
  return;
215
196
  }
216
- const beforePersist = (_a = persist == null ? void 0 : persist.beforePersist) != null ? _a : ((s) => s);
217
- const beforeRecover = (_b = persist == null ? void 0 : persist.beforeRecover) != null ? _b : ((v) => v);
218
- const debounceMs = (_c = persist == null ? void 0 : persist.debounce) != null ? _c : 100;
197
+ const beforePersist = (_b = persist == null ? void 0 : persist.beforePersist) != null ? _b : ((s) => s);
198
+ const beforeRecover = (_c = persist == null ? void 0 : persist.beforeRecover) != null ? _c : ((v) => v);
199
+ const debounceMs = (_d = persist == null ? void 0 : persist.debounce) != null ? _d : 100;
219
200
  if (!storage) {
220
- console.warn(
221
- "[react-native-hox] \u672A\u914D\u7F6E storage\uFF0Cpersist \u5C06\u4E0D\u4F1A\u751F\u6548\uFF1B\u8BF7\u5148\u8C03\u7528 setup({ storage })"
201
+ logger.warn(
202
+ "[react-native-hox] persist \u5DF2\u5F00\u542F\u4F46 storage \u4E0D\u53EF\u7528\uFF1A\u8BF7\u5728 persist.storage \u4F20\u5165 AsyncStorage/MMKV \u7B49\u3002"
222
203
  );
204
+ return;
223
205
  }
224
- let channel;
225
- const instanceId = Math.random().toString(36).slice(2);
226
- let suppressBroadcast = false;
227
- if (storage) {
228
- Promise.resolve(storage.getItem(persistKey)).then((val) => {
229
- if (!val) return;
230
- try {
231
- const parsed = JSON.parse(val);
232
- const recovered = beforeRecover(parsed);
233
- suppressBroadcast = true;
234
- store.setState(recovered, true);
235
- } catch (e) {
236
- console.error("[react-native-hox] Failed to parse stored value:", e);
237
- } finally {
238
- suppressBroadcast = false;
239
- }
240
- });
241
- }
242
- if (syncEnabled && typeof BroadcastChannel !== "undefined") {
243
- channel = new BroadcastChannel(`hox-sync-${persistKey}`);
244
- channel.onmessage = (event) => {
245
- const payload = event.data;
246
- const nextState = (payload == null ? void 0 : payload.__hox) ? payload.state : payload;
247
- const source = (payload == null ? void 0 : payload.__hox) ? payload.source : void 0;
248
- if (source && source === instanceId) {
249
- return;
250
- }
251
- suppressBroadcast = true;
252
- store.setState(nextState, true);
253
- suppressBroadcast = false;
254
- };
255
- }
256
- const save = debounce((state, s) => {
257
- if (!s) return;
206
+ let isDestroyed = false;
207
+ Promise.resolve().then(() => storage.getItem(persistKey)).then((val) => {
208
+ if (isDestroyed) return;
209
+ if (!val) return;
210
+ try {
211
+ const parsed = JSON.parse(val);
212
+ const recovered = beforeRecover(parsed);
213
+ store.setState(recovered, false);
214
+ } catch (e) {
215
+ logger.error("[react-native-hox] Failed to parse stored value:", e);
216
+ }
217
+ }).catch((e) => {
218
+ logger.error("[react-native-hox] Recover failed:", e);
219
+ });
220
+ const save = debounce((state) => {
221
+ if (isDestroyed) return;
258
222
  try {
259
223
  const prepared = beforePersist(state);
260
- s.setItem(persistKey, JSON.stringify(prepared));
224
+ const result = storage.setItem(persistKey, JSON.stringify(prepared));
225
+ Promise.resolve(result).catch((e) => {
226
+ logger.error("[react-native-hox] Save failed:", e);
227
+ });
261
228
  } catch (e) {
262
- console.error("[react-native-hox] Save failed:", e);
229
+ logger.error("[react-native-hox] Save failed:", e);
263
230
  }
264
231
  }, debounceMs);
265
- store.subscribe((state) => {
266
- save(state, storage);
267
- if (channel && !suppressBroadcast) {
268
- channel.postMessage({ __hox: true, source: instanceId, state });
269
- }
232
+ const unsubscribe = store.subscribe((state) => {
233
+ save(state);
270
234
  });
235
+ const originalDestroy = store.destroy;
236
+ store.destroy = () => {
237
+ unsubscribe();
238
+ save.flush();
239
+ isDestroyed = true;
240
+ save.cancel();
241
+ originalDestroy();
242
+ };
271
243
  }
272
244
 
273
- export { createModel, setup, shallow };
245
+ export { createModel, shallow };
package/package.json CHANGED
@@ -1,11 +1,24 @@
1
1
  {
2
2
  "name": "react-native-hox",
3
- "version": "0.0.1-beta",
3
+ "version": "0.0.2",
4
4
  "description": "一个轻量级、类型安全、零心智负担的 React Native 状态管理解决方案。",
5
+ "keywords": [
6
+ "react-native",
7
+ "state-management",
8
+ "store",
9
+ "global-state",
10
+ "typescript",
11
+ "hooks",
12
+ "hox"
13
+ ],
5
14
  "repository": {
6
15
  "type": "git",
7
16
  "url": "https://gitee.com/ws18250840411/react-native-hox.git"
8
17
  },
18
+ "bugs": {
19
+ "url": "https://gitee.com/ws18250840411/react-native-hox/issues"
20
+ },
21
+ "homepage": "https://gitee.com/ws18250840411/react-native-hox#readme",
9
22
  "license": "ISC",
10
23
  "author": "wangws",
11
24
  "sideEffects": false,
@@ -26,23 +39,25 @@
26
39
  "dist",
27
40
  "README.md"
28
41
  ],
29
- "publishConfig": {
30
- "tag": "beta"
31
- },
32
42
  "scripts": {
33
43
  "test": "jest",
34
44
  "build": "tsup",
35
45
  "prepack": "npm run build"
36
46
  },
47
+ "dependencies": {
48
+ "@react-native-async-storage/async-storage": "^2.2.0",
49
+ "use-sync-external-store": "^1.6.0"
50
+ },
37
51
  "peerDependencies": {
38
52
  "react": ">=16.8",
39
- "react-native": ">=0.64"
53
+ "react-native": ">=0.65"
40
54
  },
41
55
  "devDependencies": {
42
56
  "@types/jest": "^30.0.0",
43
57
  "@types/react": "^19.2.8",
44
58
  "@types/react-native": "^0.72.8",
45
59
  "@types/react-test-renderer": "^19.1.0",
60
+ "@types/use-sync-external-store": "^1.5.0",
46
61
  "jest": "^30.2.0",
47
62
  "react": "^19.2.0",
48
63
  "react-test-renderer": "^19.2.3",
@@ -50,4 +65,4 @@
50
65
  "tsup": "^8.5.0",
51
66
  "typescript": "^5.9.3"
52
67
  }
53
- }
68
+ }
package/README.en.md DELETED
@@ -1,36 +0,0 @@
1
- # react-native-hox
2
-
3
- #### Description
4
- {**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
5
-
6
- #### Software Architecture
7
- Software architecture description
8
-
9
- #### Installation
10
-
11
- 1. xxxx
12
- 2. xxxx
13
- 3. xxxx
14
-
15
- #### Instructions
16
-
17
- 1. xxxx
18
- 2. xxxx
19
- 3. xxxx
20
-
21
- #### Contribution
22
-
23
- 1. Fork the repository
24
- 2. Create Feat_xxx branch
25
- 3. Commit your code
26
- 4. Create Pull Request
27
-
28
-
29
- #### Gitee Feature
30
-
31
- 1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
32
- 2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
33
- 3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
34
- 4. The most valuable open source project [GVP](https://gitee.com/gvp)
35
- 5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
36
- 6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)