react-native-hox 0.0.1-beta → 0.0.1

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,9 +1,15 @@
1
- import { useRef, useSyncExternalStore } from 'react';
1
+ import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector';
2
2
 
3
3
  // src/createModel.ts
4
4
 
5
5
  // src/config.ts
6
+ var cachedDefaultWebStorage;
7
+ var hasCachedDefaultWebStorage = false;
6
8
  function getDefaultWebStorage() {
9
+ if (hasCachedDefaultWebStorage) {
10
+ return cachedDefaultWebStorage;
11
+ }
12
+ hasCachedDefaultWebStorage = true;
7
13
  try {
8
14
  const anyGlobal = globalThis;
9
15
  const storage = anyGlobal == null ? void 0 : anyGlobal.localStorage;
@@ -13,57 +19,32 @@ function getDefaultWebStorage() {
13
19
  const testKey = "__react_native_hox_test__";
14
20
  storage.setItem(testKey, "1");
15
21
  storage.removeItem(testKey);
16
- return {
22
+ cachedDefaultWebStorage = {
17
23
  getItem: (key) => storage.getItem(key),
18
24
  setItem: (key, value) => storage.setItem(key, value),
19
25
  removeItem: (key) => storage.removeItem(key)
20
26
  };
27
+ return cachedDefaultWebStorage;
21
28
  } catch {
29
+ cachedDefaultWebStorage = void 0;
22
30
  return void 0;
23
31
  }
24
32
  }
25
- var config = {
26
- storage: getDefaultWebStorage()
27
- };
28
- var setup = (options) => {
29
- Object.assign(config, options);
30
- };
31
- var getConfig = () => config;
32
33
 
33
34
  // src/vanilla.ts
34
35
  var createStore = (initialState) => {
35
36
  let state = initialState;
36
37
  const listeners = /* @__PURE__ */ new Set();
37
- const getState = () => state;
38
- const setState = (partial, replace = false) => {
38
+ const setState = (partial, replace) => {
39
39
  const nextState = typeof partial === "function" ? partial(state) : partial;
40
- if (Object.is(nextState, state)) {
41
- return;
40
+ if (!Object.is(nextState, state)) {
41
+ const previousState = state;
42
+ state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
43
+ listeners.forEach((listener) => listener(state, previousState));
42
44
  }
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);
63
- }
64
- const currentListeners = new Set(listeners);
65
- currentListeners.forEach((listener) => listener(state, previousState));
66
45
  };
46
+ const getState = () => state;
47
+ const getInitialState = () => initialState;
67
48
  const subscribe = (listener) => {
68
49
  listeners.add(listener);
69
50
  return () => listeners.delete(listener);
@@ -71,11 +52,11 @@ var createStore = (initialState) => {
71
52
  const destroy = () => {
72
53
  listeners.clear();
73
54
  };
74
- return { getState, setState, subscribe, destroy };
55
+ return { getState, getInitialState, setState, subscribe, destroy };
75
56
  };
76
57
 
77
58
  // src/utils.ts
78
- var shallow = (objA, objB) => {
59
+ function shallow(objA, objB) {
79
60
  if (Object.is(objA, objB)) {
80
61
  return true;
81
62
  }
@@ -93,7 +74,7 @@ var shallow = (objA, objB) => {
93
74
  }
94
75
  }
95
76
  return true;
96
- };
77
+ }
97
78
  function debounce(func, wait) {
98
79
  let timeout;
99
80
  let lastArgs;
@@ -105,6 +86,7 @@ function debounce(func, wait) {
105
86
  timeout = setTimeout(() => {
106
87
  func.apply(context, args);
107
88
  lastArgs = void 0;
89
+ timeout = void 0;
108
90
  }, wait);
109
91
  };
110
92
  debounced.flush = () => {
@@ -112,162 +94,172 @@ function debounce(func, wait) {
112
94
  clearTimeout(timeout);
113
95
  func.apply(context, lastArgs);
114
96
  lastArgs = void 0;
97
+ timeout = void 0;
115
98
  }
116
99
  };
117
100
  debounced.cancel = () => {
118
101
  clearTimeout(timeout);
119
102
  lastArgs = void 0;
103
+ timeout = void 0;
120
104
  };
121
105
  return debounced;
122
106
  }
123
107
 
124
108
  // src/createModel.ts
109
+ var { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports;
110
+ var identity = (v) => v;
111
+ function isInvalidHookCallError(error) {
112
+ return error instanceof Error && /Invalid hook call|Hooks can only be called/i.test(error.message);
113
+ }
125
114
  function createModel(initialState, options) {
115
+ var _a;
126
116
  const initialValue = typeof initialState === "function" ? initialState() : initialState;
127
117
  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", {
118
+ const logger = (_a = options == null ? void 0 : options.logger) != null ? _a : console;
119
+ setupModelSideEffects(store, options, logger);
120
+ let isDestroyed = false;
121
+ const setState = (partial, replace) => {
122
+ var _a2;
123
+ if (isDestroyed && ((_a2 = options == null ? void 0 : options.strict) == null ? void 0 : _a2.forbidSetStateAfterDestroy)) {
124
+ const error = new Error("[react-native-hox] model \u5DF2\u9500\u6BC1\uFF0C\u65E0\u6CD5\u518D\u8C03\u7528 setState\u3002");
125
+ logger.error(error);
126
+ throw error;
127
+ }
128
+ store.setState(partial, replace);
129
+ };
130
+ const destroy = () => {
131
+ isDestroyed = true;
132
+ store.destroy();
133
+ };
134
+ const reset = () => store.setState(initialValue, true);
135
+ const subscribe = createSubscribe(store);
136
+ const data = {
137
+ getState: store.getState,
138
+ setState,
139
+ subscribe,
140
+ destroy,
141
+ reset
142
+ };
143
+ Object.defineProperty(data, "state", {
167
144
  get: () => store.getState()
168
145
  });
169
- useModel.getState = store.getState;
170
- useModel.setState = store.setState;
171
- useModel.destroy = store.destroy;
172
- useModel.reset = () => {
146
+ const hook = createModelHook(store);
147
+ const model = {
148
+ getState: hook,
149
+ useState: hook,
150
+ use: hook,
151
+ // Add 'use' alias
152
+ setState,
153
+ reset,
154
+ destroy,
155
+ data
173
156
  };
174
- useModel.useState = useModel;
175
- useModel.use = useModel;
176
- const baseSubscribe = store.subscribe;
177
- useModel.subscribe = (arg1, arg2, arg3) => {
157
+ const modelFn = function(selector, equalityFn) {
158
+ return hook(selector, equalityFn);
159
+ };
160
+ Object.assign(modelFn, model);
161
+ return modelFn;
162
+ }
163
+ function createSubscribe(store) {
164
+ return ((arg1, arg2, arg3) => {
178
165
  var _a;
179
166
  if (typeof arg1 === "function" && typeof arg2 !== "function") {
180
- return baseSubscribe(arg1);
167
+ return store.subscribe(arg1);
181
168
  }
182
169
  const selector = arg1;
183
170
  const listener = arg2;
184
171
  const options = arg3 != null ? arg3 : {};
185
172
  const equalityFn = (_a = options.equalityFn) != null ? _a : Object.is;
186
- let prevSelected = selector(store.getState());
173
+ let lastSelected = selector(store.getState());
187
174
  if (options.fireImmediately) {
188
- listener(prevSelected, prevSelected);
175
+ listener(lastSelected, lastSelected);
189
176
  }
190
- return baseSubscribe((nextState, prevState) => {
177
+ return store.subscribe((nextState) => {
191
178
  const nextSelected = selector(nextState);
192
- const prevSelectedFromPrevState = selector(prevState);
193
- if (equalityFn(prevSelectedFromPrevState, nextSelected)) {
179
+ if (equalityFn(lastSelected, nextSelected)) {
194
180
  return;
195
181
  }
196
- prevSelected = nextSelected;
197
- listener(nextSelected, prevSelectedFromPrevState);
182
+ const prevSelected = lastSelected;
183
+ lastSelected = nextSelected;
184
+ listener(nextSelected, prevSelected);
198
185
  });
199
- };
186
+ });
187
+ }
188
+ function createModelHook(store) {
189
+ const useModel = ((selector, equalityFn = shallow) => {
190
+ try {
191
+ return useSyncExternalStoreWithSelector(
192
+ store.subscribe,
193
+ store.getState,
194
+ store.getInitialState,
195
+ selector != null ? selector : identity,
196
+ equalityFn
197
+ );
198
+ } catch (error) {
199
+ if (isInvalidHookCallError(error)) {
200
+ throw new Error(
201
+ "[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"
202
+ );
203
+ }
204
+ throw error;
205
+ }
206
+ });
200
207
  return useModel;
201
208
  }
202
- function setupModelSideEffects(store, options) {
203
- var _a, _b, _c;
204
- const { storage } = getConfig();
209
+ function setupModelSideEffects(store, options, logger) {
210
+ var _a, _b, _c, _d;
205
211
  const persist = typeof (options == null ? void 0 : options.persist) === "string" ? { key: options.persist } : options == null ? void 0 : options.persist;
206
212
  const persistKey = persist == null ? void 0 : persist.key;
207
- const syncEnabled = !!(options == null ? void 0 : options.sync);
213
+ const storage = (_a = persist == null ? void 0 : persist.storage) != null ? _a : getDefaultWebStorage();
208
214
  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
215
  return;
215
216
  }
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;
217
+ const beforePersist = (_b = persist == null ? void 0 : persist.beforePersist) != null ? _b : ((s) => s);
218
+ const beforeRecover = (_c = persist == null ? void 0 : persist.beforeRecover) != null ? _c : ((v) => v);
219
+ const debounceMs = (_d = persist == null ? void 0 : persist.debounce) != null ? _d : 100;
219
220
  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 })"
221
+ logger.warn(
222
+ "[react-native-hox] persist \u5DF2\u5F00\u542F\u4F46 storage \u4E0D\u53EF\u7528\uFF1AWeb \u9700\u8981 localStorage \u53EF\u7528\uFF1BReact Native/Node \u8BF7\u5728 persist.storage \u4F20\u5165 AsyncStorage/MMKV \u7B49\u3002"
222
223
  );
224
+ return;
223
225
  }
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;
226
+ let isDestroyed = false;
227
+ Promise.resolve().then(() => storage.getItem(persistKey)).then((val) => {
228
+ if (isDestroyed) return;
229
+ if (!val) return;
230
+ try {
231
+ const parsed = JSON.parse(val);
232
+ const recovered = beforeRecover(parsed);
233
+ store.setState(recovered, false);
234
+ } catch (e) {
235
+ logger.error("[react-native-hox] Failed to parse stored value:", e);
236
+ }
237
+ }).catch((e) => {
238
+ logger.error("[react-native-hox] Recover failed:", e);
239
+ });
240
+ const save = debounce((state) => {
241
+ if (isDestroyed) return;
258
242
  try {
259
243
  const prepared = beforePersist(state);
260
- s.setItem(persistKey, JSON.stringify(prepared));
244
+ const result = storage.setItem(persistKey, JSON.stringify(prepared));
245
+ Promise.resolve(result).catch((e) => {
246
+ logger.error("[react-native-hox] Save failed:", e);
247
+ });
261
248
  } catch (e) {
262
- console.error("[react-native-hox] Save failed:", e);
249
+ logger.error("[react-native-hox] Save failed:", e);
263
250
  }
264
251
  }, debounceMs);
265
- store.subscribe((state) => {
266
- save(state, storage);
267
- if (channel && !suppressBroadcast) {
268
- channel.postMessage({ __hox: true, source: instanceId, state });
269
- }
252
+ const unsubscribe = store.subscribe((state) => {
253
+ save(state);
270
254
  });
255
+ const originalDestroy = store.destroy;
256
+ store.destroy = () => {
257
+ unsubscribe();
258
+ save.flush();
259
+ isDestroyed = true;
260
+ save.cancel();
261
+ originalDestroy();
262
+ };
271
263
  }
272
264
 
273
- export { createModel, setup, shallow };
265
+ 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.1",
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,14 +39,14 @@
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
+ "use-sync-external-store": "^1.6.0"
49
+ },
37
50
  "peerDependencies": {
38
51
  "react": ">=16.8",
39
52
  "react-native": ">=0.64"
@@ -43,6 +56,7 @@
43
56
  "@types/react": "^19.2.8",
44
57
  "@types/react-native": "^0.72.8",
45
58
  "@types/react-test-renderer": "^19.1.0",
59
+ "@types/use-sync-external-store": "^1.5.0",
46
60
  "jest": "^30.2.0",
47
61
  "react": "^19.2.0",
48
62
  "react-test-renderer": "^19.2.3",
@@ -50,4 +64,4 @@
50
64
  "tsup": "^8.5.0",
51
65
  "typescript": "^5.9.3"
52
66
  }
53
- }
67
+ }
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/)