floppy-disk 3.0.0-beta.1 → 3.0.0-beta.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/react.js CHANGED
@@ -5,28 +5,73 @@ var vanilla = require('floppy-disk/vanilla');
5
5
 
6
6
  const useIsomorphicLayoutEffect = vanilla.isClient ? react.useLayoutEffect : react.useEffect;
7
7
 
8
- const useStoreUpdateNotifier = (store, selector) => {
9
- const [, reRender] = react.useState({});
10
- const selectorRef = react.useRef(selector);
11
- selectorRef.current = selector;
12
- useIsomorphicLayoutEffect(
13
- () => store.subscribe((state, prevState) => {
14
- if (selectorRef.current === vanilla.identity) return reRender({});
15
- const prevSlice = selectorRef.current(prevState);
16
- const nextSlice = selectorRef.current(state);
17
- if (!vanilla.shallow(prevSlice, nextSlice)) reRender({});
18
- }),
19
- [store]
20
- );
8
+ const getValueByPath = (obj, path) => path.reduce((acc, key) => acc == null ? void 0 : acc[key], obj);
9
+ const isPrefixPath = (candidatePrefix, targetPath) => {
10
+ if (candidatePrefix.length >= targetPath.length) return false;
11
+ for (let i = 0; i < candidatePrefix.length; i++) {
12
+ if (candidatePrefix[i] !== targetPath[i]) return false;
13
+ }
14
+ return true;
15
+ };
16
+ const compressPaths = (paths) => {
17
+ const result = [];
18
+ let prev = null;
19
+ for (let i = paths.length - 1; i >= 0; i--) {
20
+ const current = paths[i];
21
+ if (!prev || !isPrefixPath(current, prev)) result.push(current);
22
+ prev = current;
23
+ }
24
+ return result;
21
25
  };
22
- const useStoreState = (store, selector = vanilla.identity) => {
23
- useStoreUpdateNotifier(store, selector);
24
- return selector(store.getState());
26
+ const useStoreStateProxy = (storeState) => {
27
+ const usedPathsRef = react.useRef([]);
28
+ usedPathsRef.current = [];
29
+ const trackedState = react.useMemo(() => {
30
+ const track = (path) => usedPathsRef.current.push(path);
31
+ const proxyCache = /* @__PURE__ */ new WeakMap();
32
+ const createDeepProxy = (target, path = []) => {
33
+ if (typeof target !== "object" || target === null) {
34
+ return target;
35
+ }
36
+ if (proxyCache.has(target)) {
37
+ return proxyCache.get(target);
38
+ }
39
+ const proxy = new Proxy(target, {
40
+ get(obj, key) {
41
+ const newPath = [...path, key];
42
+ track(newPath);
43
+ const value = obj[key];
44
+ return createDeepProxy(value, newPath);
45
+ }
46
+ });
47
+ proxyCache.set(target, proxy);
48
+ return proxy;
49
+ };
50
+ return createDeepProxy(storeState);
51
+ }, [storeState]);
52
+ return [trackedState, usedPathsRef];
53
+ };
54
+ const useStoreState = (storeState, subscribe) => {
55
+ const [trackedState, usedPathsRef] = useStoreStateProxy(storeState);
56
+ const [, reRender] = react.useState({});
57
+ useIsomorphicLayoutEffect(() => {
58
+ return subscribe((nextState, prevState, changedKeys) => {
59
+ const paths = compressPaths(usedPathsRef.current);
60
+ for (const path of paths) {
61
+ const rootKey = path[0];
62
+ if (!changedKeys.includes(rootKey)) continue;
63
+ const prevVal = getValueByPath(prevState, path);
64
+ const nextVal = getValueByPath(nextState, path);
65
+ if (!Object.is(prevVal, nextVal)) return reRender({});
66
+ }
67
+ });
68
+ }, [subscribe]);
69
+ return trackedState;
25
70
  };
26
71
 
27
72
  const createStore = (initialState, options) => {
28
73
  const store = vanilla.initStore(initialState, options);
29
- const useStore = (selector) => useStoreState(store, selector);
74
+ const useStore = () => useStoreState(store.getState(), store.subscribe);
30
75
  return Object.assign(useStore, store);
31
76
  };
32
77
 
@@ -41,9 +86,7 @@ const createStores = (initialState, options) => {
41
86
  store = vanilla.initStore(initialState, options);
42
87
  stores.set(keyHash, store);
43
88
  }
44
- const useStore = (selector) => {
45
- return useStoreState(store, selector);
46
- };
89
+ const useStore = () => useStoreState(store.getState(), store.subscribe);
47
90
  return Object.assign(useStore, {
48
91
  ...store,
49
92
  delete: () => {
@@ -325,30 +368,51 @@ const createQuery = (queryFn, options = {}) => {
325
368
  stores.set(variableHash, store);
326
369
  internals.set(store, configureInternals(store, variable, variableHash));
327
370
  }
328
- function useStore(optionsOrSelector = {}, maybeSelector) {
329
- let selector;
330
- let options2;
331
- if (typeof optionsOrSelector === "function") {
332
- options2 = {};
333
- selector = optionsOrSelector;
334
- } else {
335
- options2 = optionsOrSelector;
336
- selector = maybeSelector || vanilla.identity;
337
- }
338
- useStoreUpdateNotifier(store, selector);
339
- useIsomorphicLayoutEffect(() => {
340
- if (options2.enabled !== false) revalidate(store, variable, false);
341
- }, [store, options2.enabled]);
371
+ const useStore = (options2 = {}) => {
372
+ const { enabled = true, keepPreviousData } = options2;
342
373
  const storeState = store.getState();
343
- let storeStateToBeUsed = storeState;
344
374
  const prevState = react.useRef({});
345
- if (storeState.isSuccess) {
346
- prevState.current = { data: storeState.data, dataUpdatedAt: storeState.dataUpdatedAt };
347
- } else if (storeState.state === "INITIAL" && options2.keepPreviousData) {
375
+ let storeStateToBeUsed = storeState;
376
+ if (storeState.state !== "INITIAL") {
377
+ prevState.current = {
378
+ data: storeState.data,
379
+ dataUpdatedAt: storeState.dataUpdatedAt
380
+ };
381
+ } else if (keepPreviousData) {
348
382
  storeStateToBeUsed = { ...storeState, ...prevState.current };
349
383
  }
350
- return selector(storeStateToBeUsed);
351
- }
384
+ const [trackedState, usedPathsRef] = useStoreStateProxy(
385
+ enabled && storeState.state === "INITIAL" ? (
386
+ // Optimize rendering on initial state
387
+ // Do { isPending: true } → result
388
+ // instead of { isPending: false } → { isPending: true } → result
389
+ { ...storeStateToBeUsed, isPending: true }
390
+ ) : storeStateToBeUsed
391
+ );
392
+ const [, reRender] = react.useState({});
393
+ useIsomorphicLayoutEffect(() => {
394
+ return store.subscribe((nextState, prevState2, changedKeys) => {
395
+ if (prevState2.state === "INITIAL" && !prevState2.isPending && nextState.isPending) {
396
+ return;
397
+ }
398
+ const paths = compressPaths(usedPathsRef.current);
399
+ for (const path of paths) {
400
+ const rootKey = path[0];
401
+ if (!changedKeys.includes(rootKey)) continue;
402
+ const prevVal = getValueByPath(prevState2, path);
403
+ const nextVal = getValueByPath(nextState, path);
404
+ if (!Object.is(prevVal, nextVal)) return reRender({});
405
+ }
406
+ });
407
+ }, [store]);
408
+ useIsomorphicLayoutEffect(() => {
409
+ if (enabled !== false) revalidate(store, variable, false);
410
+ }, [store, enabled]);
411
+ if (keepPreviousData) {
412
+ !!trackedState.error;
413
+ }
414
+ return trackedState;
415
+ };
352
416
  return Object.assign(useStore, {
353
417
  subscribe: store.subscribe,
354
418
  getSubscribers: store.getSubscribers,
@@ -419,7 +483,7 @@ const createMutation = (mutationFn, options = {}) => {
419
483
  const { onSuccess = vanilla.noop, onError, onSettled = vanilla.noop } = options;
420
484
  const initialState = INITIAL_STATE;
421
485
  const store = vanilla.initStore(initialState, options);
422
- const useStore = (selector) => useStoreState(store, selector);
486
+ const useStore = () => useStoreState(store.getState(), store.subscribe);
423
487
  const execute = (variable) => {
424
488
  const stateBeforeExecute = store.getState();
425
489
  if (stateBeforeExecute.isPending) {
@@ -517,4 +581,3 @@ exports.createStore = createStore;
517
581
  exports.createStores = createStores;
518
582
  exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect;
519
583
  exports.useStoreState = useStoreState;
520
- exports.useStoreUpdateNotifier = useStoreUpdateNotifier;
@@ -6,14 +6,6 @@ export declare const isClient: boolean;
6
6
  * Empty function.
7
7
  */
8
8
  export declare const noop: () => void;
9
- /**
10
- * Identity function.
11
- *
12
- * It accepts 1 argument, and simply return it.
13
- *
14
- * `const identity = value => value`
15
- */
16
- export declare const identity: <T>(value: T) => T;
17
9
  /**
18
10
  * If the value is a function, it will invoke the function.\
19
11
  * If the value is not a function, it will just return it.
@@ -11,18 +11,21 @@ export type SetState<TState> = Partial<TState> | ((state: TState) => Partial<TSt
11
11
  *
12
12
  * @param state - The latest state
13
13
  * @param prevState - The previous state before the update
14
+ * @param changedKeys - The top-level keys that changed (shallow diff)
14
15
  *
15
16
  * @remarks
16
- * - Subscribers are only called when the state actually changes.
17
+ * - Subscribers are only called when at least one field changes.
17
18
  * - Change detection is performed per key using `Object.is`.
19
+ * - `changedKeys` only includes top-level keys; nested changes must be inferred by the consumer.
18
20
  */
19
- export type Subscriber<TState> = (state: TState, prevState: TState) => void;
21
+ export type Subscriber<TState> = (state: TState, prevState: TState, changedKeys: Array<keyof TState>) => void;
20
22
  /**
21
23
  * Core store API for managing state.
22
24
  *
23
25
  * @remarks
24
26
  * - The store performs **shallow change detection per key** before notifying subscribers.
25
27
  * - Subscribers are only notified when at least one field changes.
28
+ * - State is treated as **immutable**. Mutating nested values directly will not trigger updates.
26
29
  * - Designed to be framework-agnostic (React bindings are built separately).
27
30
  * - By default, `setState` is **disabled on the server** to prevent accidental shared state between requests.
28
31
  */
@@ -48,13 +51,17 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
48
51
  onSubscribe?: (state: TState, store: StoreApi<TState>) => void;
49
52
  onUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
50
53
  onLastUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
54
+ /**
55
+ * By default, calling `setState` on the server is disallowed to prevent shared state across requests.
56
+ * Set this to `true` only if you explicitly intend to mutate state during server execution.
57
+ */
51
58
  allowSetStateServerSide?: boolean;
52
59
  };
53
60
  /**
54
61
  * Creates a vanilla store with pub-sub capabilities.
55
62
  *
56
- * The store state is expected to be an **object**.\
57
- * Updates are applied as partial merges, so non-object states are not supported.
63
+ * The store state must be an **object**.\
64
+ * Updates are applied as shallow merges, so non-object states are not supported.
58
65
  *
59
66
  * @param initialState - The initial state of the store
60
67
  * @param options - Optional lifecycle hooks
@@ -64,11 +71,13 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
64
71
  * @remarks
65
72
  * - State updates are **shallowly compared per key** before notifying subscribers.
66
73
  * - Subscribers are only notified when at least one updated field changes (using `Object.is` comparison).
67
- * - Subscribers receive both the new state and the previous state.
74
+ * - Subscribers receive the new state, previous state, and changed top-level keys.
75
+ * - State is expected to be treated as **immutable**.
76
+ * - Mutating nested values directly will not trigger updates.
68
77
  * - Lifecycle hooks allow side-effect management tied to subscription count.
69
- * - By default, `setState` is **disabled on the server** to prevent accidental shared state between requests.
70
- * - This avoids leaking data between users in server environments.
71
- * - You can override this by setting `allowSetStateServerSide: true`.
78
+ * - By default, `setState` is **not allowed on the server** to prevent accidental shared state between requests.
79
+ * - This helps avoid leaking data between users in server environments.
80
+ * - If you intentionally want to allow this behavior, set `allowSetStateServerSide: true`.
72
81
  *
73
82
  * @example
74
83
  * const store = initStore({ count: 0 });
package/vanilla.d.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export * from './vanilla/basic';
2
- export * from './vanilla/shallow';
3
2
  export * from './vanilla/hash';
4
3
  export * from './vanilla/store';
package/vanilla.js CHANGED
@@ -3,7 +3,6 @@
3
3
  const isClient = typeof window !== "undefined" && !("Deno" in window);
4
4
  const noop = () => {
5
5
  };
6
- const identity = (value) => value;
7
6
  const getValue = (valueOrComputeValueFn, ...params) => {
8
7
  if (typeof valueOrComputeValueFn === "function") {
9
8
  return valueOrComputeValueFn(...params);
@@ -11,41 +10,6 @@ const getValue = (valueOrComputeValueFn, ...params) => {
11
10
  return valueOrComputeValueFn;
12
11
  };
13
12
 
14
- const shallow = (a, b) => {
15
- if (Object.is(a, b)) {
16
- return true;
17
- }
18
- if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
19
- return false;
20
- }
21
- if (a instanceof Map && b instanceof Map) {
22
- if (a.size !== b.size) return false;
23
- for (const [key, value] of a) {
24
- if (!Object.is(value, b.get(key))) {
25
- return false;
26
- }
27
- }
28
- return true;
29
- }
30
- if (a instanceof Set && b instanceof Set) {
31
- if (a.size !== b.size) return false;
32
- for (const value of a) {
33
- if (!b.has(value)) return false;
34
- }
35
- return true;
36
- }
37
- const keysA = Object.keys(a);
38
- if (keysA.length !== Object.keys(b).length) {
39
- return false;
40
- }
41
- for (let i = 0; i < keysA.length; i++) {
42
- if (!Object.prototype.hasOwnProperty.call(b, keysA[i]) || !Object.is(a[keysA[i]], b[keysA[i]])) {
43
- return false;
44
- }
45
- }
46
- return true;
47
- };
48
-
49
13
  const hasObjectPrototype = (value) => {
50
14
  return Object.prototype.toString.call(value) === "[object Object]";
51
15
  };
@@ -98,13 +62,15 @@ const initStore = (initialState, options = {}) => {
98
62
  }
99
63
  const prevState = state;
100
64
  const newValue = getValue(value, state);
65
+ const changedKeys = [];
101
66
  for (const key in newValue) {
102
67
  if (!Object.is(prevState[key], newValue[key])) {
103
- state = { ...prevState, ...newValue };
104
- [...subscribers].forEach((subscriber) => subscriber(state, prevState));
105
- return;
68
+ changedKeys.push(key);
106
69
  }
107
70
  }
71
+ if (changedKeys.length === 0) return;
72
+ state = { ...prevState, ...newValue };
73
+ [...subscribers].forEach((subscriber) => subscriber(state, prevState, changedKeys));
108
74
  };
109
75
  const storeApi = {
110
76
  getState,
@@ -117,9 +83,7 @@ const initStore = (initialState, options = {}) => {
117
83
 
118
84
  exports.getHash = getHash;
119
85
  exports.getValue = getValue;
120
- exports.identity = identity;
121
86
  exports.initStore = initStore;
122
87
  exports.isClient = isClient;
123
88
  exports.isPlainObject = isPlainObject;
124
89
  exports.noop = noop;
125
- exports.shallow = shallow;
@@ -1,6 +0,0 @@
1
- /**
2
- * Shallow compare 2 values.
3
- *
4
- * Reference: https://github.com/pmndrs/zustand/blob/e414f7ccf41eae09517ee2dcb44c9f5ae8a35a25/src/vanilla/shallow.ts
5
- */
6
- export declare const shallow: <T>(a: T, b: T) => boolean;
@@ -1,6 +0,0 @@
1
- /**
2
- * Shallow compare 2 values.
3
- *
4
- * Reference: https://github.com/pmndrs/zustand/blob/e414f7ccf41eae09517ee2dcb44c9f5ae8a35a25/src/vanilla/shallow.ts
5
- */
6
- export declare const shallow: <T>(a: T, b: T) => boolean;