@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,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility hook for subscribing to a state handler or a state singleton.
|
|
3
|
+
* Manages the lifecycle and reference counting for shared state handler instances.
|
|
4
|
+
*/
|
|
1
5
|
import { useEffect, useMemo } from 'react';
|
|
2
6
|
|
|
3
7
|
import { useStateActions } from './state-actions.js';
|
|
@@ -5,28 +9,57 @@ import { useStateSubscriptionSelector } from './state-subscription-selector.js';
|
|
|
5
9
|
|
|
6
10
|
import type { StateSingleton } from '../../store/state-singleton.js';
|
|
7
11
|
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
12
|
+
import { useProvidedStateHandler } from './state-provider.js';
|
|
8
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Type signatures for selector and equality functions.
|
|
16
|
+
*/
|
|
9
17
|
type StateSelector<State, SelectedState> = (state: State) => SelectedState;
|
|
10
18
|
type EqualityFn<SelectedState> = (current: SelectedState, next: SelectedState) => boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Interface representing a state singleton with optional internal management methods.
|
|
22
|
+
*/
|
|
11
23
|
type ManagedSingleton = StateSingleton<unknown, unknown> & {
|
|
24
|
+
// Method to manually destroy the singleton instance.
|
|
12
25
|
destroyInstance?: () => void;
|
|
26
|
+
// Flag indicating if the instance should be destroyed when no more consumers are active.
|
|
13
27
|
destroyOnNoConsumers?: boolean;
|
|
14
28
|
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Alias for a standard state handler instance.
|
|
32
|
+
*/
|
|
15
33
|
type SharedStateHandler = StateSubscriptionHandler<unknown, unknown>;
|
|
16
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Global map to track reference counts for singleton state handler instances.
|
|
37
|
+
* Used to determine when it's safe to destroy a shared singleton.
|
|
38
|
+
*/
|
|
17
39
|
const singletonReferences = new WeakMap<
|
|
18
40
|
StateSingleton<unknown, unknown>,
|
|
19
41
|
{ count: number; stateHandler: SharedStateHandler }
|
|
20
42
|
>();
|
|
21
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Default identity selector returns the whole state.
|
|
46
|
+
*/
|
|
22
47
|
const identitySelector = <State,>(state: State) => state;
|
|
23
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Type guard function to check if a source is a StateSingleton.
|
|
51
|
+
*/
|
|
24
52
|
function isStateSingleton<V, A>(
|
|
25
53
|
source: StateSubscriptionHandler<V, A> | StateSingleton<V, A>
|
|
26
54
|
): source is StateSingleton<V, A> {
|
|
55
|
+
// Check for the presence of the getInstance method.
|
|
27
56
|
return 'getInstance' in source;
|
|
28
57
|
}
|
|
29
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Custom hook to subscribe to a state handler or singleton and receive its state and actions.
|
|
61
|
+
* Correctly manages shared instances and reference counting.
|
|
62
|
+
*/
|
|
30
63
|
export function useStateSubscription<V, A>(source: StateSubscriptionHandler<V, A>): [V, A];
|
|
31
64
|
export function useStateSubscription<V, A, Sel>(
|
|
32
65
|
source: StateSubscriptionHandler<V, A>,
|
|
@@ -40,66 +73,108 @@ export function useStateSubscription<V, A, Sel>(
|
|
|
40
73
|
isEqual?: EqualityFn<Sel>
|
|
41
74
|
): [Sel, A];
|
|
42
75
|
export function useStateSubscription<V, A, Sel = V>(
|
|
76
|
+
// Implementation of the overloaded useStateSubscription hook.
|
|
43
77
|
source: StateSubscriptionHandler<V, A> | StateSingleton<V, A>,
|
|
78
|
+
// Selector function to derive a specific value from the state.
|
|
44
79
|
selector: StateSelector<V, Sel> = identitySelector as StateSelector<V, Sel>,
|
|
80
|
+
// Optional equality function to compare selected values for changes.
|
|
45
81
|
isEqual: EqualityFn<Sel> = Object.is
|
|
46
82
|
) {
|
|
83
|
+
// Determine if the source is a singleton instance or a direct state handler.
|
|
47
84
|
const singletonSource = isStateSingleton(source) ? source : null;
|
|
85
|
+
// Resolve the final state subscription handler instance to use.
|
|
48
86
|
const stateSubscriptionHandler = useMemo<StateSubscriptionHandler<V, A>>(() => {
|
|
87
|
+
// If it's a singleton, access its managed instance.
|
|
49
88
|
if (singletonSource) {
|
|
50
89
|
return singletonSource.getInstance();
|
|
51
90
|
}
|
|
52
91
|
|
|
92
|
+
// Otherwise, return the source handler instance directly.
|
|
53
93
|
return source as StateSubscriptionHandler<V, A>;
|
|
54
94
|
}, [singletonSource, source]);
|
|
55
95
|
|
|
96
|
+
// Use an effect to manage the lifecycle and reference count of singleton instances.
|
|
56
97
|
useEffect(() => {
|
|
98
|
+
// If the source is not a singleton, no lifecycle management is needed here.
|
|
57
99
|
if (!singletonSource) {
|
|
58
100
|
return undefined;
|
|
59
101
|
}
|
|
60
102
|
|
|
103
|
+
// Cast the source to access management properties and retrieve the current reference.
|
|
61
104
|
const singleton = singletonSource as ManagedSingleton;
|
|
62
105
|
const sharedStateHandler = stateSubscriptionHandler as SharedStateHandler;
|
|
63
106
|
const singletonReference = singletonReferences.get(singleton);
|
|
64
107
|
|
|
108
|
+
// Update the reference count for the singleton instance.
|
|
65
109
|
if (!singletonReference || singletonReference.stateHandler !== sharedStateHandler) {
|
|
110
|
+
// Initialize the count if it doesn't already exist or has changed.
|
|
66
111
|
singletonReferences.set(singleton, { count: 1, stateHandler: sharedStateHandler });
|
|
67
112
|
} else {
|
|
113
|
+
// Increment the consumer count.
|
|
68
114
|
singletonReference.count += 1;
|
|
69
115
|
}
|
|
70
116
|
|
|
117
|
+
// Return an effect cleanup function to decrement the count when the component unmounts.
|
|
71
118
|
return () => {
|
|
119
|
+
// Access the active reference for the singleton.
|
|
72
120
|
const activeReference = singletonReferences.get(singleton);
|
|
73
121
|
|
|
122
|
+
// If no active reference is found, do nothing.
|
|
74
123
|
if (!activeReference || activeReference.stateHandler !== sharedStateHandler) {
|
|
75
124
|
return;
|
|
76
125
|
}
|
|
77
126
|
|
|
127
|
+
// Decrement the consumer count.
|
|
78
128
|
activeReference.count -= 1;
|
|
79
129
|
|
|
130
|
+
// If there are still active consumers, do not destroy the instance.
|
|
80
131
|
if (activeReference.count <= 0) {
|
|
132
|
+
// Remove the reference from the map when the count reaches zero.
|
|
81
133
|
singletonReferences.delete(singleton);
|
|
134
|
+
|
|
135
|
+
// Only proceed with destruction if destroyOnNoConsumers is explicitly enabled.
|
|
82
136
|
if (singleton.destroyOnNoConsumers !== true) {
|
|
83
137
|
return;
|
|
84
138
|
}
|
|
85
139
|
|
|
140
|
+
// Use the singleton's internal destroy method if available.
|
|
86
141
|
if (singleton.destroyInstance) {
|
|
87
142
|
singleton.destroyInstance();
|
|
88
143
|
return;
|
|
89
144
|
}
|
|
90
145
|
|
|
146
|
+
// Otherwise, invoke the handler's destroy method directly.
|
|
91
147
|
stateSubscriptionHandler.destroy();
|
|
92
148
|
}
|
|
93
149
|
};
|
|
94
150
|
}, [singletonSource, stateSubscriptionHandler]);
|
|
95
151
|
|
|
152
|
+
// Select and subscribe to the state value using the useStateSubscriptionSelector hook.
|
|
96
153
|
const state = useStateSubscriptionSelector(
|
|
97
154
|
stateSubscriptionHandler,
|
|
98
155
|
selector,
|
|
99
156
|
isEqual,
|
|
100
157
|
!singletonSource
|
|
101
158
|
);
|
|
159
|
+
// Retrieve the set of actions from the state handler.
|
|
102
160
|
const actions = useStateActions(stateSubscriptionHandler);
|
|
103
161
|
|
|
162
|
+
// Return the selected state and its actions as a tuple.
|
|
104
163
|
return [state, actions] as [Sel, A];
|
|
105
164
|
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Custom hook to subscribe to the state handler provided by the nearest StateProvider.
|
|
168
|
+
*/
|
|
169
|
+
export function useProvidedStateSubscription<V, A>(): [V, A];
|
|
170
|
+
export function useProvidedStateSubscription<V, A, Sel>(
|
|
171
|
+
selector: StateSelector<V, Sel>,
|
|
172
|
+
isEqual?: EqualityFn<Sel>
|
|
173
|
+
): [Sel, A];
|
|
174
|
+
export function useProvidedStateSubscription<V, A, Sel = V>(
|
|
175
|
+
selector: StateSelector<V, Sel> = identitySelector as StateSelector<V, Sel>,
|
|
176
|
+
isEqual: EqualityFn<Sel> = Object.is
|
|
177
|
+
) {
|
|
178
|
+
const stateHandler = useProvidedStateHandler<V, A>();
|
|
179
|
+
return useStateSubscription(stateHandler, selector, isEqual);
|
|
180
|
+
}
|
package/src/react/index.ts
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Export core React hooks and components for Status Quo.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Export hooks and components to manage state and actions.
|
|
6
|
+
export {
|
|
7
|
+
StateProvider,
|
|
8
|
+
useProvidedStateActions,
|
|
9
|
+
useProvidedStateHandler,
|
|
10
|
+
useProvidedStateSubscription,
|
|
11
|
+
useStateActions,
|
|
12
|
+
useStateFactory,
|
|
13
|
+
useStateHandler,
|
|
14
|
+
useStateSingleton,
|
|
15
|
+
useStateSubscription,
|
|
16
|
+
} from './hooks/index.js';
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { resetStatusQuoForTests, setupStatusQuo } from '../../config/status-quo-config.js';
|
|
2
|
+
import { NativeStateHandler } from '../native-state-handler.js';
|
|
3
|
+
import { makeStateSingleton } from '../state-singleton.js';
|
|
4
|
+
|
|
5
|
+
import type { DistinctOptions } from '../../config/status-quo-config.js';
|
|
6
|
+
|
|
7
|
+
type TestState = { test: string; test2: string };
|
|
8
|
+
type TestActions = { testAction: () => void };
|
|
9
|
+
type TestNativeHandlerOptions = {
|
|
10
|
+
withDevTools?: boolean;
|
|
11
|
+
distinct?: DistinctOptions<TestState>;
|
|
12
|
+
useDistinctUntilChanged?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
class TestNativeStateHandler extends NativeStateHandler<TestState, TestActions> {
|
|
16
|
+
constructor({ withDevTools, distinct, useDistinctUntilChanged }: TestNativeHandlerOptions = {}) {
|
|
17
|
+
super({
|
|
18
|
+
initialState: {
|
|
19
|
+
test: 'testValue',
|
|
20
|
+
test2: 'testValue2',
|
|
21
|
+
},
|
|
22
|
+
options: {
|
|
23
|
+
...(withDevTools && {
|
|
24
|
+
devTools: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
namespace: 'TestNativeStateHandler',
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
...(distinct && {
|
|
30
|
+
distinct,
|
|
31
|
+
}),
|
|
32
|
+
...(typeof useDistinctUntilChanged === 'boolean' && {
|
|
33
|
+
useDistinctUntilChanged,
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getActions(): TestActions {
|
|
40
|
+
return {
|
|
41
|
+
testAction: () => {
|
|
42
|
+
this.setState({ test: 'newValue' });
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type CounterState = { count: number };
|
|
49
|
+
type CounterActions = { increase: () => void };
|
|
50
|
+
type CounterBucketSelection = { bucket: number };
|
|
51
|
+
type CounterBucketState = { bucket: number };
|
|
52
|
+
|
|
53
|
+
class CounterNativeStateHandler extends NativeStateHandler<CounterState, CounterActions> {
|
|
54
|
+
constructor(initialCount = 0) {
|
|
55
|
+
super({
|
|
56
|
+
initialState: {
|
|
57
|
+
count: initialCount,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getActions(): CounterActions {
|
|
63
|
+
return {
|
|
64
|
+
increase: () => {
|
|
65
|
+
this.setState({ count: this.getState().count + 1 }, 'increase');
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class CounterNativeBridgeStateHandler extends NativeStateHandler<
|
|
72
|
+
CounterState,
|
|
73
|
+
{ noop: () => void }
|
|
74
|
+
> {
|
|
75
|
+
constructor(
|
|
76
|
+
counterSingleton: ReturnType<typeof makeStateSingleton<CounterState, CounterActions>>,
|
|
77
|
+
onCounterSync: (counterState: CounterState) => void
|
|
78
|
+
) {
|
|
79
|
+
super({
|
|
80
|
+
initialState: {
|
|
81
|
+
count: 0,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const counterStateHandler = counterSingleton.getInstance();
|
|
86
|
+
|
|
87
|
+
this.bindSubscribable<CounterState, CounterState>(
|
|
88
|
+
counterStateHandler,
|
|
89
|
+
(nextCounterState) => {
|
|
90
|
+
onCounterSync(nextCounterState);
|
|
91
|
+
this.setState({ count: nextCounterState.count }, 'sync-counter');
|
|
92
|
+
},
|
|
93
|
+
(counterState) => counterState
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getActions(): { noop: () => void } {
|
|
98
|
+
return {
|
|
99
|
+
noop: () => undefined,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class CounterNativeBucketBridgeStateHandler extends NativeStateHandler<
|
|
105
|
+
CounterBucketState,
|
|
106
|
+
{ noop: () => void }
|
|
107
|
+
> {
|
|
108
|
+
constructor(
|
|
109
|
+
counterSingleton: ReturnType<typeof makeStateSingleton<CounterState, CounterActions>>,
|
|
110
|
+
onCounterSync: (selection: CounterBucketSelection) => void
|
|
111
|
+
) {
|
|
112
|
+
super({
|
|
113
|
+
initialState: {
|
|
114
|
+
bucket: -1,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const counterStateHandler = counterSingleton.getInstance();
|
|
119
|
+
|
|
120
|
+
this.bindSubscribable<CounterState, CounterBucketSelection>(
|
|
121
|
+
counterStateHandler,
|
|
122
|
+
(nextSelection) => {
|
|
123
|
+
onCounterSync(nextSelection);
|
|
124
|
+
this.setState({ bucket: nextSelection.bucket }, 'sync-counter-bucket');
|
|
125
|
+
},
|
|
126
|
+
(counterState) => ({
|
|
127
|
+
bucket: Math.floor(counterState.count / 2),
|
|
128
|
+
}),
|
|
129
|
+
(current, next) => current.bucket === next.bucket
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getActions(): { noop: () => void } {
|
|
134
|
+
return {
|
|
135
|
+
noop: () => undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
describe('Native State Handler', () => {
|
|
141
|
+
let stateHandler: TestNativeStateHandler;
|
|
142
|
+
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
resetStatusQuoForTests();
|
|
145
|
+
stateHandler = new TestNativeStateHandler();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
afterEach(() => {
|
|
149
|
+
resetStatusQuoForTests();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should provide initial state', () => {
|
|
153
|
+
expect(stateHandler.getInitialState()).toStrictEqual({
|
|
154
|
+
test: 'testValue',
|
|
155
|
+
test2: 'testValue2',
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should provide current state', () => {
|
|
160
|
+
expect(stateHandler.getState()).toStrictEqual({
|
|
161
|
+
test: 'testValue',
|
|
162
|
+
test2: 'testValue2',
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should support state changing via setter and merge state object on first level', () => {
|
|
167
|
+
const expected = {
|
|
168
|
+
test: 'change',
|
|
169
|
+
test2: 'testValue2',
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
stateHandler.setState(expected);
|
|
173
|
+
|
|
174
|
+
expect(stateHandler.getState()).toStrictEqual(expected);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should support additional subscriptions handling', () => {
|
|
178
|
+
const spy = jest.fn();
|
|
179
|
+
const subscription = { unsubscribe: spy };
|
|
180
|
+
|
|
181
|
+
stateHandler.subscriptions = [subscription];
|
|
182
|
+
|
|
183
|
+
stateHandler.destroy();
|
|
184
|
+
|
|
185
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should call subscriber when state has changed and also on initial subscribe', () => {
|
|
189
|
+
const spy = jest.fn();
|
|
190
|
+
const unsubscribe = stateHandler.subscribe(spy);
|
|
191
|
+
|
|
192
|
+
stateHandler.setState({
|
|
193
|
+
test: 'test',
|
|
194
|
+
});
|
|
195
|
+
stateHandler.setState({
|
|
196
|
+
test: 'test2',
|
|
197
|
+
});
|
|
198
|
+
stateHandler.setState({
|
|
199
|
+
test: 'test2',
|
|
200
|
+
});
|
|
201
|
+
stateHandler.setState({
|
|
202
|
+
test: 'test2',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
unsubscribe();
|
|
206
|
+
|
|
207
|
+
expect(spy).toHaveBeenCalledTimes(3); // initial + change + change
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should respect global distinct setup when disabled', () => {
|
|
211
|
+
setupStatusQuo({
|
|
212
|
+
distinct: {
|
|
213
|
+
enabled: false,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const handler = new TestNativeStateHandler();
|
|
218
|
+
const spy = jest.fn();
|
|
219
|
+
const unsubscribe = handler.subscribe(spy);
|
|
220
|
+
|
|
221
|
+
handler.setState({ test: 'same' });
|
|
222
|
+
handler.setState({ test: 'same' });
|
|
223
|
+
|
|
224
|
+
unsubscribe();
|
|
225
|
+
|
|
226
|
+
expect(spy).toHaveBeenCalledTimes(3); // initial + change + change
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should respect global custom distinct comparator from setupStatusQuo', () => {
|
|
230
|
+
setupStatusQuo({
|
|
231
|
+
distinct: {
|
|
232
|
+
comparator: (previous: TestState, next: TestState) => {
|
|
233
|
+
return previous.test === next.test;
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const handler = new TestNativeStateHandler();
|
|
239
|
+
const spy = jest.fn();
|
|
240
|
+
const unsubscribe = handler.subscribe(spy);
|
|
241
|
+
|
|
242
|
+
handler.setState({ test2: 'newValue2' }); // test remains same -> skipped
|
|
243
|
+
handler.setState({ test: 'newValue' }); // test changed -> notified
|
|
244
|
+
|
|
245
|
+
unsubscribe();
|
|
246
|
+
|
|
247
|
+
expect(spy).toHaveBeenCalledTimes(2); // initial + one change
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should notify another state handler for each singleton counter update', () => {
|
|
251
|
+
const counterSingleton = makeStateSingleton(() => new CounterNativeStateHandler(0), {
|
|
252
|
+
destroyOnNoConsumers: false,
|
|
253
|
+
});
|
|
254
|
+
const syncSpy = jest.fn();
|
|
255
|
+
const bridgeStateHandler = new CounterNativeBridgeStateHandler(counterSingleton, syncSpy);
|
|
256
|
+
const counterStateHandler = counterSingleton.getInstance();
|
|
257
|
+
|
|
258
|
+
counterStateHandler.getActions().increase();
|
|
259
|
+
counterStateHandler.getActions().increase();
|
|
260
|
+
|
|
261
|
+
expect(syncSpy).toHaveBeenCalledTimes(3);
|
|
262
|
+
expect(syncSpy).toHaveBeenNthCalledWith(1, { count: 0 });
|
|
263
|
+
expect(syncSpy).toHaveBeenNthCalledWith(2, { count: 1 });
|
|
264
|
+
expect(syncSpy).toHaveBeenNthCalledWith(3, { count: 2 });
|
|
265
|
+
expect(bridgeStateHandler.getState()).toStrictEqual({ count: 2 });
|
|
266
|
+
|
|
267
|
+
bridgeStateHandler.destroy();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should support selector + equality filtering for bindSubscribable', () => {
|
|
271
|
+
const counterSingleton = makeStateSingleton(() => new CounterNativeStateHandler(0), {
|
|
272
|
+
destroyOnNoConsumers: false,
|
|
273
|
+
});
|
|
274
|
+
const syncSpy = jest.fn();
|
|
275
|
+
const bridgeStateHandler = new CounterNativeBucketBridgeStateHandler(counterSingleton, syncSpy);
|
|
276
|
+
const counterStateHandler = counterSingleton.getInstance();
|
|
277
|
+
|
|
278
|
+
counterStateHandler.getActions().increase(); // count 1 -> bucket 0 (no change)
|
|
279
|
+
counterStateHandler.getActions().increase(); // count 2 -> bucket 1
|
|
280
|
+
counterStateHandler.getActions().increase(); // count 3 -> bucket 1 (no change)
|
|
281
|
+
counterStateHandler.getActions().increase(); // count 4 -> bucket 2
|
|
282
|
+
|
|
283
|
+
expect(syncSpy).toHaveBeenCalledTimes(3);
|
|
284
|
+
expect(syncSpy).toHaveBeenNthCalledWith(1, { bucket: 0 });
|
|
285
|
+
expect(syncSpy).toHaveBeenNthCalledWith(2, { bucket: 1 });
|
|
286
|
+
expect(syncSpy).toHaveBeenNthCalledWith(3, { bucket: 2 });
|
|
287
|
+
expect(bridgeStateHandler.getState()).toStrictEqual({ bucket: 2 });
|
|
288
|
+
|
|
289
|
+
bridgeStateHandler.destroy();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -5,6 +5,14 @@ import { ObservableStateHandler } from '../observable-state-handler.js';
|
|
|
5
5
|
|
|
6
6
|
import type { DevToolsOptions, DistinctOptions } from '../../config/status-quo-config.js';
|
|
7
7
|
|
|
8
|
+
declare global {
|
|
9
|
+
interface Window {
|
|
10
|
+
__REDUX_DEVTOOLS_EXTENSION__?: {
|
|
11
|
+
connect: (options: any) => any;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
type TestState = { test: string; test2: string };
|
|
9
17
|
type TestActions = { testAction: () => void };
|
|
10
18
|
type TestObservableHandlerOptions = {
|