@veams/status-quo 1.6.0 → 1.8.0
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/.turbo/turbo-build.log +3 -3
- package/.turbo/turbo-check$colon$types.log +1 -1
- package/.turbo/turbo-lint.log +2 -2
- package/.turbo/turbo-test.log +115 -15
- package/CHANGELOG.md +2 -0
- package/README.md +51 -7
- package/dist/config/status-quo-config.d.ts +22 -1
- package/dist/config/status-quo-config.js +46 -2
- package/dist/config/status-quo-config.js.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/dist/react/hooks/__tests__/state-provider.spec.js +2 -2
- package/dist/react/hooks/__tests__/state-provider.spec.js.map +1 -1
- package/dist/react/hooks/__tests__/state-selector.spec.js +48 -0
- package/dist/react/hooks/__tests__/state-selector.spec.js.map +1 -1
- package/dist/react/hooks/index.d.ts +6 -3
- package/dist/react/hooks/index.js +12 -3
- package/dist/react/hooks/index.js.map +1 -1
- package/dist/react/hooks/state-actions.d.ts +9 -1
- package/dist/react/hooks/state-actions.js +21 -2
- package/dist/react/hooks/state-actions.js.map +1 -1
- package/dist/react/hooks/state-factory.d.ts +7 -0
- package/dist/react/hooks/state-factory.js +23 -1
- package/dist/react/hooks/state-factory.js.map +1 -1
- package/dist/react/hooks/state-handler.d.ts +4 -0
- package/dist/react/hooks/state-handler.js +18 -1
- package/dist/react/hooks/state-handler.js.map +1 -1
- package/dist/react/hooks/state-provider.d.ts +18 -9
- package/dist/react/hooks/state-provider.js +25 -17
- package/dist/react/hooks/state-provider.js.map +1 -1
- package/dist/react/hooks/state-singleton.d.ts +8 -2
- package/dist/react/hooks/state-singleton.js +21 -3
- package/dist/react/hooks/state-singleton.js.map +1 -1
- package/dist/react/hooks/state-subscription-selector.d.ts +4 -0
- package/dist/react/hooks/state-subscription-selector.js +111 -4
- package/dist/react/hooks/state-subscription-selector.js.map +1 -1
- package/dist/react/hooks/state-subscription.d.ts +12 -0
- package/dist/react/hooks/state-subscription.js +49 -1
- package/dist/react/hooks/state-subscription.js.map +1 -1
- package/dist/react/index.d.ts +4 -1
- package/dist/react/index.js +5 -1
- package/dist/react/index.js.map +1 -1
- package/dist/store/__tests__/native-state-handler.spec.d.ts +1 -0
- package/dist/store/__tests__/native-state-handler.spec.js +210 -0
- package/dist/store/__tests__/native-state-handler.spec.js.map +1 -0
- package/dist/store/__tests__/observable-state-handler.spec.d.ts +7 -0
- package/dist/store/__tests__/observable-state-handler.spec.js.map +1 -1
- package/dist/store/base-state-handler.d.ts +42 -0
- package/dist/store/base-state-handler.js +73 -10
- package/dist/store/base-state-handler.js.map +1 -1
- package/dist/store/dev-tools.d.ts +42 -17
- package/dist/store/dev-tools.js +24 -8
- package/dist/store/dev-tools.js.map +1 -1
- package/dist/store/index.d.ts +7 -0
- package/dist/store/index.js +9 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/native-state-handler.d.ts +44 -0
- package/dist/store/native-state-handler.js +62 -0
- package/dist/store/native-state-handler.js.map +1 -0
- package/dist/store/observable-state-handler.d.ts +34 -0
- package/dist/store/observable-state-handler.js +45 -1
- package/dist/store/observable-state-handler.js.map +1 -1
- package/dist/store/signal-state-handler.d.ts +26 -0
- package/dist/store/signal-state-handler.js +35 -0
- package/dist/store/signal-state-handler.js.map +1 -1
- package/dist/store/state-singleton.d.ts +14 -0
- package/dist/store/state-singleton.js +20 -1
- package/dist/store/state-singleton.js.map +1 -1
- package/dist/types/types.d.ts +9 -0
- package/dist/types/types.js +3 -0
- package/dist/types/types.js.map +1 -1
- package/dist/utils/selector-cache.d.ts +17 -0
- package/dist/utils/selector-cache.js +28 -1
- package/dist/utils/selector-cache.js.map +1 -1
- package/package.json +12 -1
- package/src/config/status-quo-config.ts +64 -1
- package/src/index.ts +29 -0
- package/src/react/hooks/__tests__/state-provider.spec.tsx +2 -2
- package/src/react/hooks/__tests__/state-selector.spec.tsx +66 -0
- package/src/react/hooks/index.ts +13 -8
- package/src/react/hooks/state-actions.tsx +23 -2
- package/src/react/hooks/state-factory.tsx +34 -0
- package/src/react/hooks/state-handler.tsx +15 -0
- package/src/react/hooks/state-provider.tsx +36 -40
- package/src/react/hooks/state-singleton.tsx +37 -7
- package/src/react/hooks/state-subscription-selector.tsx +151 -3
- package/src/react/hooks/state-subscription.tsx +75 -0
- package/src/react/index.ts +16 -1
- package/src/store/__tests__/native-state-handler.spec.ts +291 -0
- package/src/store/__tests__/observable-state-handler.spec.ts +8 -0
- package/src/store/base-state-handler.ts +89 -12
- package/src/store/dev-tools.ts +72 -27
- package/src/store/index.ts +16 -0
- package/src/store/native-state-handler.ts +98 -0
- package/src/store/observable-state-handler.ts +57 -0
- package/src/store/signal-state-handler.ts +47 -1
- package/src/store/state-singleton.ts +30 -0
- package/src/types/types.ts +16 -0
- package/src/utils/selector-cache.ts +37 -0
|
@@ -1,7 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility hook for accessing actions from a state handler instance.
|
|
3
|
+
*/
|
|
1
4
|
import { useMemo } from 'react';
|
|
2
5
|
|
|
3
6
|
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
7
|
+
import { useProvidedStateHandler } from './state-provider.js';
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Returns the actions of a state handler instance.
|
|
11
|
+
* memoized based on the state handler instance itself.
|
|
12
|
+
*/
|
|
13
|
+
export function useStateActions<V, A>(stateHandler: StateSubscriptionHandler<V, A>): A {
|
|
14
|
+
// Access and memoize the actions from the state handler.
|
|
15
|
+
// This ensures the action object remains referentially stable as long as the state handler is the same.
|
|
16
|
+
const actions = useMemo(() => stateHandler.getActions(), [stateHandler]);
|
|
17
|
+
|
|
18
|
+
// Return the set of actions.
|
|
19
|
+
return actions;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns the actions of the state handler provided by the nearest StateProvider.
|
|
24
|
+
*/
|
|
25
|
+
export function useProvidedStateActions<V, A>(): A {
|
|
26
|
+
const stateHandler = useProvidedStateHandler<V, A>();
|
|
27
|
+
return useStateActions(stateHandler);
|
|
7
28
|
}
|
|
@@ -1,42 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility hook for creating and subscribing to a state handler within a component.
|
|
3
|
+
* Combines creation and subscription logic in a single call.
|
|
4
|
+
*/
|
|
1
5
|
import { useStateHandler } from './state-handler.js';
|
|
2
6
|
import { useStateSubscription } from './state-subscription.js';
|
|
3
7
|
|
|
4
8
|
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
5
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Type signatures for selector and equality functions.
|
|
12
|
+
*/
|
|
6
13
|
type StateSelector<State, SelectedState> = (state: State) => SelectedState;
|
|
7
14
|
type EqualityFn<SelectedState> = (current: SelectedState, next: SelectedState) => boolean;
|
|
8
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Default identity selector returns the whole state.
|
|
18
|
+
*/
|
|
9
19
|
const identitySelector = <State,>(state: State) => state;
|
|
10
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Factory hook to create and subscribe to a state handler.
|
|
23
|
+
* Manages the handler instance using useStateHandler and its subscription with useStateSubscription.
|
|
24
|
+
*/
|
|
11
25
|
export function useStateFactory<V, A, P extends unknown[]>(
|
|
26
|
+
// Function to create a new state handler instance.
|
|
12
27
|
stateFactoryFunction: (...args: P) => StateSubscriptionHandler<V, A>,
|
|
28
|
+
// Parameters to pass to the factory function.
|
|
13
29
|
params?: P
|
|
14
30
|
): [V, A];
|
|
15
31
|
export function useStateFactory<V, A, P extends unknown[], Sel>(
|
|
32
|
+
// Function to create a new state handler instance.
|
|
16
33
|
stateFactoryFunction: (...args: P) => StateSubscriptionHandler<V, A>,
|
|
34
|
+
// Selector function to derive a specific value from the state.
|
|
17
35
|
selector: StateSelector<V, Sel>,
|
|
36
|
+
// Parameters to pass to the factory function.
|
|
18
37
|
params?: P
|
|
19
38
|
): [Sel, A];
|
|
20
39
|
export function useStateFactory<V, A, P extends unknown[], Sel>(
|
|
40
|
+
// Function to create a new state handler instance.
|
|
21
41
|
stateFactoryFunction: (...args: P) => StateSubscriptionHandler<V, A>,
|
|
42
|
+
// Selector function to derive a specific value from the state.
|
|
22
43
|
selector: StateSelector<V, Sel>,
|
|
44
|
+
// Optional equality function to compare selected values.
|
|
23
45
|
isEqual?: EqualityFn<Sel>,
|
|
46
|
+
// Parameters to pass to the factory function.
|
|
24
47
|
params?: P
|
|
25
48
|
): [Sel, A];
|
|
26
49
|
export function useStateFactory<V, A, P extends unknown[], Sel = V>(
|
|
50
|
+
// Implementation of the overloaded useStateFactory hook.
|
|
27
51
|
stateFactoryFunction: (...args: P) => StateSubscriptionHandler<V, A>,
|
|
52
|
+
// Mixed argument: can be a selector or params array.
|
|
28
53
|
selectorOrParams: StateSelector<V, Sel> | P = [] as unknown as P,
|
|
54
|
+
// Mixed argument: can be an equality function or params array.
|
|
29
55
|
isEqualOrParams: EqualityFn<Sel> | P = Object.is as EqualityFn<Sel>,
|
|
56
|
+
// Optional params array.
|
|
30
57
|
params: P = [] as unknown as P
|
|
31
58
|
) {
|
|
59
|
+
// Determine whether a selector was provided.
|
|
32
60
|
const hasSelector = typeof selectorOrParams === 'function';
|
|
61
|
+
// Fallback to identity selector if none is provided.
|
|
33
62
|
const selector = (hasSelector ? selectorOrParams : identitySelector) as StateSelector<V, Sel>;
|
|
63
|
+
// Determine whether a custom equality function was provided.
|
|
34
64
|
const hasCustomEquality = hasSelector && typeof isEqualOrParams === 'function';
|
|
65
|
+
// Fallback to default equality check (Object.is).
|
|
35
66
|
const isEqual = (hasCustomEquality ? isEqualOrParams : Object.is);
|
|
67
|
+
// Resolve the parameters for the state handler factory.
|
|
36
68
|
const stateFactoryParams = (
|
|
37
69
|
hasSelector ? (hasCustomEquality ? params : isEqualOrParams) : selectorOrParams
|
|
38
70
|
) as P;
|
|
71
|
+
// Create and persist the state handler instance using its factory and parameters.
|
|
39
72
|
const stateHandler = useStateHandler(stateFactoryFunction, stateFactoryParams);
|
|
40
73
|
|
|
74
|
+
// Subscribe to the state handler and return the selected state and actions.
|
|
41
75
|
return useStateSubscription(stateHandler, selector, isEqual);
|
|
42
76
|
}
|
|
@@ -1,16 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility hook for managing the lifecycle of a state handler instance.
|
|
3
|
+
* Ensures the instance is created once and persisted across component re-renders.
|
|
4
|
+
*/
|
|
1
5
|
import { useRef } from 'react';
|
|
2
6
|
|
|
3
7
|
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
4
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Returns a stable state handler instance based on a factory function.
|
|
11
|
+
* Uses a ref to ensure the factory function is only executed during initial render.
|
|
12
|
+
*/
|
|
5
13
|
export function useStateHandler<V, A, P extends unknown[]>(
|
|
14
|
+
// Function to create a new state handler instance.
|
|
6
15
|
stateFactoryFunction: (...args: P) => StateSubscriptionHandler<V, A>,
|
|
16
|
+
// Parameters to pass to the factory function.
|
|
7
17
|
params: P = [] as unknown as P
|
|
8
18
|
) {
|
|
19
|
+
// Use a ref to store the state handler instance.
|
|
20
|
+
// This prevents the state handler from being recreated on every re-render.
|
|
9
21
|
const stateHandlerRef = useRef<StateSubscriptionHandler<V, A> | null>(null);
|
|
10
22
|
|
|
23
|
+
// If the ref is currently null, we create the instance for the first time.
|
|
11
24
|
if (!stateHandlerRef.current) {
|
|
25
|
+
// Invoke the factory function with the provided parameters.
|
|
12
26
|
stateHandlerRef.current = stateFactoryFunction(...params);
|
|
13
27
|
}
|
|
14
28
|
|
|
29
|
+
// Return the stable state handler instance.
|
|
15
30
|
return stateHandlerRef.current;
|
|
16
31
|
}
|
|
@@ -1,56 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility hook for providing a state handler through React Context.
|
|
3
|
+
* Allows components deep in the component tree to access a common state handler instance.
|
|
4
|
+
*/
|
|
1
5
|
import React, { createContext, useContext } from 'react';
|
|
2
6
|
|
|
3
|
-
import { useStateActions } from './state-actions.js';
|
|
4
|
-
import { useStateSubscription } from './state-subscription.js';
|
|
5
|
-
|
|
6
|
-
import type { PropsWithChildren } from 'react';
|
|
7
7
|
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export type StateProviderProps<V, A> = PropsWithChildren<{
|
|
9
|
+
/**
|
|
10
|
+
* Interface for the state provider component props.
|
|
11
|
+
*/
|
|
12
|
+
interface StateProviderProps<V, A> {
|
|
13
|
+
// Children components that will have access to the state handler.
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
// The state handler instance to be provided to the component tree.
|
|
17
16
|
instance: StateSubscriptionHandler<V, A>;
|
|
18
|
-
}>;
|
|
19
|
-
|
|
20
|
-
export function useProvidedStateHandler<V, A>() {
|
|
21
|
-
const stateHandler = useContext(StateProviderContext);
|
|
22
|
-
|
|
23
|
-
if (!stateHandler) {
|
|
24
|
-
throw new Error('No StateProvider instance found in the current React tree.');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return stateHandler as StateSubscriptionHandler<V, A>;
|
|
28
17
|
}
|
|
29
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Creates a React Context for storing and providing the state handler instance.
|
|
21
|
+
* Initialized with null as there is no default state handler.
|
|
22
|
+
*/
|
|
23
|
+
const StateContext = createContext<StateSubscriptionHandler<unknown, unknown> | null>(null);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Provides a state handler instance to its descendant components using React Context.
|
|
27
|
+
*/
|
|
30
28
|
export function StateProvider<V, A>({ children, instance }: StateProviderProps<V, A>) {
|
|
29
|
+
// Use a context provider to share the state handler instance.
|
|
31
30
|
return (
|
|
32
|
-
<
|
|
31
|
+
<StateContext.Provider value={instance as StateSubscriptionHandler<unknown, unknown>}>
|
|
33
32
|
{children}
|
|
34
|
-
</
|
|
33
|
+
</StateContext.Provider>
|
|
35
34
|
);
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Custom hook to access the state handler provided by the StateProvider.
|
|
39
|
+
* Throws an error if the hook is used outside of a StateProvider.
|
|
40
|
+
*/
|
|
41
|
+
export function useProvidedStateHandler<V, A>(): StateSubscriptionHandler<V, A> {
|
|
42
|
+
// Retrieve the state handler from the nearest context provider.
|
|
43
|
+
const stateHandler = useContext(StateContext);
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
): [Sel, A];
|
|
49
|
-
export function useProvidedStateSubscription<V, A, Sel = V>(
|
|
50
|
-
selector: StateSelector<V, Sel> = identitySelector as StateSelector<V, Sel>,
|
|
51
|
-
isEqual: EqualityFn<Sel> = Object.is
|
|
52
|
-
) {
|
|
53
|
-
const stateHandler = useProvidedStateHandler<V, A>();
|
|
45
|
+
// If no state handler is found, it means the hook is being used incorrectly.
|
|
46
|
+
if (!stateHandler) {
|
|
47
|
+
throw new Error('useProvidedStateHandler must be used within a StateProvider');
|
|
48
|
+
}
|
|
54
49
|
|
|
55
|
-
return
|
|
50
|
+
// Cast and return the state handler instance.
|
|
51
|
+
return stateHandler as StateSubscriptionHandler<V, A>;
|
|
56
52
|
}
|
|
@@ -1,24 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility hook for subscribing to a state singleton within a component.
|
|
3
|
+
*/
|
|
1
4
|
import { useStateSubscription } from './state-subscription.js';
|
|
2
5
|
|
|
3
6
|
import type { StateSingleton } from '../../store/state-singleton.js';
|
|
4
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Type signatures for selector and equality functions.
|
|
10
|
+
*/
|
|
5
11
|
type StateSelector<State, SelectedState> = (state: State) => SelectedState;
|
|
6
12
|
type EqualityFn<SelectedState> = (current: SelectedState, next: SelectedState) => boolean;
|
|
7
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Default identity selector returns the whole state.
|
|
16
|
+
*/
|
|
8
17
|
const identitySelector = <State,>(state: State) => state;
|
|
9
18
|
|
|
10
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Singleton hook to subscribe to a state singleton.
|
|
21
|
+
*/
|
|
22
|
+
export function useStateSingleton<V, A>(
|
|
23
|
+
// The state singleton instance to subscribe to.
|
|
24
|
+
singleton: StateSingleton<V, A>
|
|
25
|
+
): [V, A];
|
|
11
26
|
export function useStateSingleton<V, A, Sel>(
|
|
12
|
-
|
|
27
|
+
// The state singleton instance to subscribe to.
|
|
28
|
+
singleton: StateSingleton<V, A>,
|
|
29
|
+
// Selector function to derive a specific value from the state.
|
|
13
30
|
selector: StateSelector<V, Sel>,
|
|
31
|
+
// Optional equality function to compare selected values.
|
|
14
32
|
isEqual?: EqualityFn<Sel>
|
|
15
33
|
): [Sel, A];
|
|
16
34
|
export function useStateSingleton<V, A, Sel = V>(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
35
|
+
// Implementation of the overloaded useStateSingleton hook.
|
|
36
|
+
singleton: StateSingleton<V, A>,
|
|
37
|
+
// Mixed argument: can be a selector or equality function.
|
|
38
|
+
selectorOrIsEqual: StateSelector<V, Sel> | EqualityFn<Sel> = identitySelector as StateSelector<
|
|
39
|
+
V,
|
|
40
|
+
Sel
|
|
41
|
+
>,
|
|
42
|
+
// Mixed argument: can be an equality function or undefined.
|
|
43
|
+
isEqual: EqualityFn<Sel> = Object.is as EqualityFn<Sel>
|
|
20
44
|
) {
|
|
21
|
-
|
|
45
|
+
// Determine whether a selector was provided as the first optional argument.
|
|
46
|
+
const hasSelector = typeof selectorOrIsEqual === 'function' && selectorOrIsEqual.length === 1;
|
|
47
|
+
// Fallback to identity selector if none is provided.
|
|
48
|
+
const selector = (hasSelector ? selectorOrIsEqual : identitySelector) as StateSelector<V, Sel>;
|
|
49
|
+
// Resolve the equality function to use.
|
|
50
|
+
const equalityFn = (hasSelector ? isEqual : selectorOrIsEqual) as EqualityFn<Sel>;
|
|
22
51
|
|
|
23
|
-
|
|
52
|
+
// Subscribe to the singleton state handler and return the selected state and actions.
|
|
53
|
+
return useStateSubscription(singleton, selector, equalityFn);
|
|
24
54
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility hook for subscribing to a state handler and selecting a specific value from the state.
|
|
3
|
+
* Uses useSyncExternalStore to ensure consistent state reads and avoid unnecessary re-renders.
|
|
4
|
+
*/
|
|
1
5
|
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
2
6
|
|
|
3
7
|
import { createSelectorCache, selectWithCache } from '../../utils/selector-cache.js';
|
|
@@ -5,24 +9,67 @@ import { createSelectorCache, selectWithCache } from '../../utils/selector-cache
|
|
|
5
9
|
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
6
10
|
import type { EqualityFn, Selector } from '../../utils/selector-cache.js';
|
|
7
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Type signature for listener functions.
|
|
14
|
+
*/
|
|
8
15
|
type Listener = () => void;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Represents a state subscription handler with unknown state and actions.
|
|
19
|
+
*/
|
|
9
20
|
type SharedStateSubscriptionHandler = StateSubscriptionHandler<unknown, unknown>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Tracks the reference count and deferred destruction status of a state handler.
|
|
24
|
+
*/
|
|
10
25
|
type DeferredDestroy = {
|
|
26
|
+
// Number of active consumers of the state handler.
|
|
11
27
|
refCount: number;
|
|
28
|
+
// ID of the timeout for deferred destruction.
|
|
12
29
|
timeoutId: ReturnType<typeof setTimeout> | null;
|
|
13
30
|
};
|
|
14
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Cache entry for a selected state snapshot.
|
|
34
|
+
*/
|
|
35
|
+
type SnapshotCacheEntry<Source, Selected> = {
|
|
36
|
+
// The selected value derived from the source snapshot.
|
|
37
|
+
selectedSnapshot: Selected;
|
|
38
|
+
// The source snapshot from which the value was selected.
|
|
39
|
+
sourceSnapshot: Source;
|
|
40
|
+
// A version number to track state changes.
|
|
41
|
+
version: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Cache entry for a selected server state snapshot (SSR).
|
|
46
|
+
*/
|
|
47
|
+
type ServerSnapshotCacheEntry<Source, Selected> = {
|
|
48
|
+
// The selected value derived from the source snapshot.
|
|
49
|
+
selectedSnapshot: Selected;
|
|
50
|
+
// The source snapshot from which the value was selected.
|
|
51
|
+
sourceSnapshot: Source;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Global map to track deferred destruction status for each state handler instance.
|
|
15
55
|
const deferredDestroyMap = new WeakMap<SharedStateSubscriptionHandler, DeferredDestroy>();
|
|
16
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Returns the deferred destruction status for a given state handler instance.
|
|
59
|
+
* Initializes the status if it does not already exist.
|
|
60
|
+
*/
|
|
17
61
|
function getDeferredDestroyState(
|
|
18
62
|
stateSubscriptionHandler: SharedStateSubscriptionHandler
|
|
19
63
|
): DeferredDestroy {
|
|
64
|
+
// Retrieve the existing status from the map.
|
|
20
65
|
const existingState = deferredDestroyMap.get(stateSubscriptionHandler);
|
|
21
66
|
|
|
67
|
+
// If status already exists, return it.
|
|
22
68
|
if (existingState) {
|
|
23
69
|
return existingState;
|
|
24
70
|
}
|
|
25
71
|
|
|
72
|
+
// Create and store a new status for the handler.
|
|
26
73
|
const nextState: DeferredDestroy = {
|
|
27
74
|
refCount: 0,
|
|
28
75
|
timeoutId: null,
|
|
@@ -33,65 +80,105 @@ function getDeferredDestroyState(
|
|
|
33
80
|
return nextState;
|
|
34
81
|
}
|
|
35
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Custom hook to select and subscribe to a specific piece of state from a handler.
|
|
85
|
+
* Efficiently manages subscriptions and selector results.
|
|
86
|
+
*/
|
|
36
87
|
export function useStateSubscriptionSelector<V, A, Sel>(
|
|
88
|
+
// The state handler instance to subscribe to.
|
|
37
89
|
stateSubscriptionHandler: StateSubscriptionHandler<V, A>,
|
|
90
|
+
// Selector function to derive a value from the state.
|
|
38
91
|
selector: Selector<V, Sel>,
|
|
92
|
+
// Equality function to compare selected values for changes.
|
|
39
93
|
isEqual: EqualityFn<Sel> = Object.is,
|
|
94
|
+
// Whether to automatically destroy the handler instance on component unmount.
|
|
40
95
|
destroyOnCleanup = true
|
|
41
96
|
) {
|
|
97
|
+
// Cache for the selector results to ensure referential stability.
|
|
42
98
|
const selectorCacheRef = useRef<ReturnType<typeof createSelectorCache<Sel>> | null>(null);
|
|
99
|
+
// Tracks store notifications so getSnapshot can reuse the same selected value within one store version.
|
|
100
|
+
const snapshotVersionRef = useRef(0);
|
|
101
|
+
// Client-side cache for selected snapshots per source snapshot/version pair.
|
|
102
|
+
const snapshotCacheRef = useRef<SnapshotCacheEntry<V, Sel> | null>(null);
|
|
103
|
+
// Separate cache for the server snapshot function used by SSR/hydration paths.
|
|
104
|
+
const serverSnapshotCacheRef = useRef<ServerSnapshotCacheEntry<V, Sel> | null>(null);
|
|
43
105
|
|
|
106
|
+
// Initialize the selector cache if it doesn't already exist.
|
|
44
107
|
if (!selectorCacheRef.current) {
|
|
45
108
|
selectorCacheRef.current = createSelectorCache<Sel>();
|
|
46
109
|
}
|
|
47
110
|
|
|
48
111
|
const selectorCache = selectorCacheRef.current;
|
|
49
112
|
|
|
113
|
+
// Subscription function to be used by useSyncExternalStore.
|
|
50
114
|
const subscribe = useCallback(
|
|
51
115
|
(listener: Listener) => {
|
|
116
|
+
// Access the deferred destruction status for this handler.
|
|
52
117
|
const sharedStateSubscriptionHandler =
|
|
53
118
|
stateSubscriptionHandler as unknown as SharedStateSubscriptionHandler;
|
|
54
119
|
const deferredDestroyState = getDeferredDestroyState(sharedStateSubscriptionHandler);
|
|
120
|
+
// Increment the consumer reference count.
|
|
55
121
|
deferredDestroyState.refCount += 1;
|
|
56
122
|
|
|
123
|
+
// If a pending destruction timeout is scheduled, cancel it.
|
|
57
124
|
if (deferredDestroyState.timeoutId) {
|
|
58
125
|
clearTimeout(deferredDestroyState.timeoutId);
|
|
59
126
|
deferredDestroyState.timeoutId = null;
|
|
60
127
|
}
|
|
61
128
|
|
|
62
|
-
|
|
129
|
+
// Subscribe to the state handler.
|
|
130
|
+
const unsubscribe = stateSubscriptionHandler.subscribe(() => {
|
|
131
|
+
// Invalidate the selected snapshot cache before notifying React of a change.
|
|
132
|
+
snapshotVersionRef.current += 1;
|
|
133
|
+
snapshotCacheRef.current = null;
|
|
134
|
+
// Notify React to re-trigger a getSnapshot call.
|
|
135
|
+
listener();
|
|
136
|
+
});
|
|
63
137
|
|
|
138
|
+
// Return an unsubscribe function to be called by React.
|
|
64
139
|
return () => {
|
|
140
|
+
// Execute the handler's unsubscribe method.
|
|
65
141
|
unsubscribe();
|
|
66
142
|
|
|
143
|
+
// If automatic cleanup is disabled, stop here.
|
|
67
144
|
if (!destroyOnCleanup) {
|
|
68
145
|
return;
|
|
69
146
|
}
|
|
70
147
|
|
|
148
|
+
// Retrieve the current destruction status.
|
|
71
149
|
const activeDeferredDestroyState = deferredDestroyMap.get(sharedStateSubscriptionHandler);
|
|
72
150
|
|
|
151
|
+
// If no status is found, stop here.
|
|
73
152
|
if (!activeDeferredDestroyState) {
|
|
74
153
|
return;
|
|
75
154
|
}
|
|
76
155
|
|
|
156
|
+
// Decrement the consumer reference count.
|
|
77
157
|
activeDeferredDestroyState.refCount -= 1;
|
|
78
158
|
|
|
159
|
+
// If there are still active consumers, do not destroy the handler.
|
|
79
160
|
if (activeDeferredDestroyState.refCount > 0) {
|
|
80
161
|
return;
|
|
81
162
|
}
|
|
82
163
|
|
|
164
|
+
// Reset the reference count to zero.
|
|
83
165
|
activeDeferredDestroyState.refCount = 0;
|
|
166
|
+
// Schedule deferred destruction to allow for potential immediate re-subscriptions.
|
|
84
167
|
activeDeferredDestroyState.timeoutId = setTimeout(() => {
|
|
168
|
+
// Check if the handler still has no consumers after the timeout.
|
|
85
169
|
const pendingDeferredDestroyState = deferredDestroyMap.get(
|
|
86
170
|
sharedStateSubscriptionHandler
|
|
87
171
|
);
|
|
88
172
|
|
|
173
|
+
// If consumers have reappeared, do not destroy the handler.
|
|
89
174
|
if (!pendingDeferredDestroyState || pendingDeferredDestroyState.refCount > 0) {
|
|
90
175
|
return;
|
|
91
176
|
}
|
|
92
177
|
|
|
178
|
+
// Clear the pending timeout and destroy the state handler.
|
|
93
179
|
pendingDeferredDestroyState.timeoutId = null;
|
|
94
180
|
stateSubscriptionHandler.destroy();
|
|
181
|
+
// Remove the status from the global map.
|
|
95
182
|
deferredDestroyMap.delete(sharedStateSubscriptionHandler);
|
|
96
183
|
}, 0);
|
|
97
184
|
};
|
|
@@ -99,6 +186,7 @@ export function useStateSubscriptionSelector<V, A, Sel>(
|
|
|
99
186
|
[destroyOnCleanup, stateSubscriptionHandler]
|
|
100
187
|
);
|
|
101
188
|
|
|
189
|
+
// Helper to execute selection using the cache.
|
|
102
190
|
const selectSnapshot = useCallback(
|
|
103
191
|
(snapshot: V) => {
|
|
104
192
|
return selectWithCache(selectorCache, snapshot, selector, isEqual).value;
|
|
@@ -106,15 +194,75 @@ export function useStateSubscriptionSelector<V, A, Sel>(
|
|
|
106
194
|
[isEqual, selector, selectorCache]
|
|
107
195
|
);
|
|
108
196
|
|
|
197
|
+
// Reference to track changes in selection strategy.
|
|
198
|
+
const selectorCacheControlRef = useRef(selectSnapshot);
|
|
199
|
+
|
|
200
|
+
// If the selector or equality function changes, clear all caches.
|
|
201
|
+
if (selectorCacheControlRef.current !== selectSnapshot) {
|
|
202
|
+
selectorCacheControlRef.current = selectSnapshot;
|
|
203
|
+
snapshotVersionRef.current = 0;
|
|
204
|
+
snapshotCacheRef.current = null;
|
|
205
|
+
serverSnapshotCacheRef.current = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Snapshot retrieval function to be used by useSyncExternalStore.
|
|
109
209
|
const getSnapshot = useCallback(
|
|
110
|
-
() =>
|
|
210
|
+
() => {
|
|
211
|
+
// Retrieve the current source state from the handler.
|
|
212
|
+
const sourceSnapshot = stateSubscriptionHandler.getSnapshot();
|
|
213
|
+
// Access the current version and cached value.
|
|
214
|
+
const version = snapshotVersionRef.current;
|
|
215
|
+
const cachedSnapshot = snapshotCacheRef.current;
|
|
216
|
+
|
|
217
|
+
// If the cached selection is still valid for this version and source state, return it.
|
|
218
|
+
if (
|
|
219
|
+
cachedSnapshot &&
|
|
220
|
+
cachedSnapshot.version === version &&
|
|
221
|
+
Object.is(cachedSnapshot.sourceSnapshot, sourceSnapshot)
|
|
222
|
+
) {
|
|
223
|
+
return cachedSnapshot.selectedSnapshot;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Compute a new selection and update the cache.
|
|
227
|
+
const selectedSnapshot = selectSnapshot(sourceSnapshot);
|
|
228
|
+
snapshotCacheRef.current = {
|
|
229
|
+
selectedSnapshot,
|
|
230
|
+
sourceSnapshot,
|
|
231
|
+
version,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Return the new selection.
|
|
235
|
+
return selectedSnapshot;
|
|
236
|
+
},
|
|
111
237
|
[selectSnapshot, stateSubscriptionHandler]
|
|
112
238
|
);
|
|
113
239
|
|
|
240
|
+
// Server snapshot retrieval function to be used for SSR/hydration.
|
|
114
241
|
const getServerSnapshot = useCallback(
|
|
115
|
-
() =>
|
|
242
|
+
() => {
|
|
243
|
+
// Retrieve the initial source state from the handler.
|
|
244
|
+
const sourceSnapshot = stateSubscriptionHandler.getInitialState();
|
|
245
|
+
// Access the current cached server snapshot.
|
|
246
|
+
const cachedSnapshot = serverSnapshotCacheRef.current;
|
|
247
|
+
|
|
248
|
+
// If the cached server selection is still valid for the initial state, return it.
|
|
249
|
+
if (cachedSnapshot && Object.is(cachedSnapshot.sourceSnapshot, sourceSnapshot)) {
|
|
250
|
+
return cachedSnapshot.selectedSnapshot;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Compute a new server selection and update its cache.
|
|
254
|
+
const selectedSnapshot = selectSnapshot(sourceSnapshot);
|
|
255
|
+
serverSnapshotCacheRef.current = {
|
|
256
|
+
selectedSnapshot,
|
|
257
|
+
sourceSnapshot,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Return the initial selection result.
|
|
261
|
+
return selectedSnapshot;
|
|
262
|
+
},
|
|
116
263
|
[selectSnapshot, stateSubscriptionHandler]
|
|
117
264
|
);
|
|
118
265
|
|
|
266
|
+
// Use the useSyncExternalStore hook to integrate the state with React's rendering lifecycle.
|
|
119
267
|
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
120
268
|
}
|