@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.
Files changed (100) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/.turbo/turbo-check$colon$types.log +1 -1
  3. package/.turbo/turbo-lint.log +2 -2
  4. package/.turbo/turbo-test.log +115 -15
  5. package/CHANGELOG.md +2 -0
  6. package/README.md +51 -7
  7. package/dist/config/status-quo-config.d.ts +22 -1
  8. package/dist/config/status-quo-config.js +46 -2
  9. package/dist/config/status-quo-config.js.map +1 -1
  10. package/dist/index.d.ts +12 -2
  11. package/dist/index.js +22 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/react/hooks/__tests__/state-provider.spec.js +2 -2
  14. package/dist/react/hooks/__tests__/state-provider.spec.js.map +1 -1
  15. package/dist/react/hooks/__tests__/state-selector.spec.js +48 -0
  16. package/dist/react/hooks/__tests__/state-selector.spec.js.map +1 -1
  17. package/dist/react/hooks/index.d.ts +6 -3
  18. package/dist/react/hooks/index.js +12 -3
  19. package/dist/react/hooks/index.js.map +1 -1
  20. package/dist/react/hooks/state-actions.d.ts +9 -1
  21. package/dist/react/hooks/state-actions.js +21 -2
  22. package/dist/react/hooks/state-actions.js.map +1 -1
  23. package/dist/react/hooks/state-factory.d.ts +7 -0
  24. package/dist/react/hooks/state-factory.js +23 -1
  25. package/dist/react/hooks/state-factory.js.map +1 -1
  26. package/dist/react/hooks/state-handler.d.ts +4 -0
  27. package/dist/react/hooks/state-handler.js +18 -1
  28. package/dist/react/hooks/state-handler.js.map +1 -1
  29. package/dist/react/hooks/state-provider.d.ts +18 -9
  30. package/dist/react/hooks/state-provider.js +25 -17
  31. package/dist/react/hooks/state-provider.js.map +1 -1
  32. package/dist/react/hooks/state-singleton.d.ts +8 -2
  33. package/dist/react/hooks/state-singleton.js +21 -3
  34. package/dist/react/hooks/state-singleton.js.map +1 -1
  35. package/dist/react/hooks/state-subscription-selector.d.ts +4 -0
  36. package/dist/react/hooks/state-subscription-selector.js +111 -4
  37. package/dist/react/hooks/state-subscription-selector.js.map +1 -1
  38. package/dist/react/hooks/state-subscription.d.ts +12 -0
  39. package/dist/react/hooks/state-subscription.js +49 -1
  40. package/dist/react/hooks/state-subscription.js.map +1 -1
  41. package/dist/react/index.d.ts +4 -1
  42. package/dist/react/index.js +5 -1
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/store/__tests__/native-state-handler.spec.d.ts +1 -0
  45. package/dist/store/__tests__/native-state-handler.spec.js +210 -0
  46. package/dist/store/__tests__/native-state-handler.spec.js.map +1 -0
  47. package/dist/store/__tests__/observable-state-handler.spec.d.ts +7 -0
  48. package/dist/store/__tests__/observable-state-handler.spec.js.map +1 -1
  49. package/dist/store/base-state-handler.d.ts +42 -0
  50. package/dist/store/base-state-handler.js +73 -10
  51. package/dist/store/base-state-handler.js.map +1 -1
  52. package/dist/store/dev-tools.d.ts +42 -17
  53. package/dist/store/dev-tools.js +24 -8
  54. package/dist/store/dev-tools.js.map +1 -1
  55. package/dist/store/index.d.ts +7 -0
  56. package/dist/store/index.js +9 -0
  57. package/dist/store/index.js.map +1 -1
  58. package/dist/store/native-state-handler.d.ts +44 -0
  59. package/dist/store/native-state-handler.js +62 -0
  60. package/dist/store/native-state-handler.js.map +1 -0
  61. package/dist/store/observable-state-handler.d.ts +34 -0
  62. package/dist/store/observable-state-handler.js +45 -1
  63. package/dist/store/observable-state-handler.js.map +1 -1
  64. package/dist/store/signal-state-handler.d.ts +26 -0
  65. package/dist/store/signal-state-handler.js +35 -0
  66. package/dist/store/signal-state-handler.js.map +1 -1
  67. package/dist/store/state-singleton.d.ts +14 -0
  68. package/dist/store/state-singleton.js +20 -1
  69. package/dist/store/state-singleton.js.map +1 -1
  70. package/dist/types/types.d.ts +9 -0
  71. package/dist/types/types.js +3 -0
  72. package/dist/types/types.js.map +1 -1
  73. package/dist/utils/selector-cache.d.ts +17 -0
  74. package/dist/utils/selector-cache.js +28 -1
  75. package/dist/utils/selector-cache.js.map +1 -1
  76. package/package.json +12 -1
  77. package/src/config/status-quo-config.ts +64 -1
  78. package/src/index.ts +29 -0
  79. package/src/react/hooks/__tests__/state-provider.spec.tsx +2 -2
  80. package/src/react/hooks/__tests__/state-selector.spec.tsx +66 -0
  81. package/src/react/hooks/index.ts +13 -8
  82. package/src/react/hooks/state-actions.tsx +23 -2
  83. package/src/react/hooks/state-factory.tsx +34 -0
  84. package/src/react/hooks/state-handler.tsx +15 -0
  85. package/src/react/hooks/state-provider.tsx +36 -40
  86. package/src/react/hooks/state-singleton.tsx +37 -7
  87. package/src/react/hooks/state-subscription-selector.tsx +151 -3
  88. package/src/react/hooks/state-subscription.tsx +75 -0
  89. package/src/react/index.ts +16 -1
  90. package/src/store/__tests__/native-state-handler.spec.ts +291 -0
  91. package/src/store/__tests__/observable-state-handler.spec.ts +8 -0
  92. package/src/store/base-state-handler.ts +89 -12
  93. package/src/store/dev-tools.ts +72 -27
  94. package/src/store/index.ts +16 -0
  95. package/src/store/native-state-handler.ts +98 -0
  96. package/src/store/observable-state-handler.ts +57 -0
  97. package/src/store/signal-state-handler.ts +47 -1
  98. package/src/store/state-singleton.ts +30 -0
  99. package/src/types/types.ts +16 -0
  100. 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
+ }
@@ -1 +1,16 @@
1
- export * from './hooks/index.js';
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 = {