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