@veams/status-quo 1.11.0 → 1.12.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.
@@ -22,6 +22,8 @@ type ManagedSubscription = {
22
22
  export declare abstract class BaseStateHandler<S, A> implements StateSubscriptionHandler<S, A> {
23
23
  protected readonly initialState: S;
24
24
  protected devTools: DevTools | null;
25
+ private connectCount;
26
+ private isConnected;
25
27
  subscriptions: ManagedSubscription[];
26
28
  namedSubscriptions: Map<string, ManagedSubscription>;
27
29
  /**
@@ -49,10 +51,30 @@ export declare abstract class BaseStateHandler<S, A> implements StateSubscriptio
49
51
  * Also sends the update to DevTools if enabled.
50
52
  */
51
53
  setState(newState: Partial<S>, actionName?: string): void;
54
+ /**
55
+ * Starts external effects for this handler.
56
+ */
57
+ connect(): void;
58
+ /**
59
+ * Stops external effects once all connected consumers have disconnected.
60
+ */
61
+ disconnect(): void;
52
62
  /**
53
63
  * Cleans up all active subscriptions when the handler is destroyed.
54
64
  */
55
65
  destroy(): void;
66
+ /**
67
+ * Optional hook for subclasses that need to start external effects.
68
+ */
69
+ protected onConnect(): void;
70
+ /**
71
+ * Optional hook for subclasses that need to stop external effects.
72
+ */
73
+ protected onDisconnect(): void;
74
+ /**
75
+ * Clears all subscriptions managed through this base handler.
76
+ */
77
+ protected clearManagedSubscriptions(): void;
56
78
  protected abstract getStateValue(): S;
57
79
  protected abstract setStateValue(nextState: S): void;
58
80
  /**
@@ -28,6 +28,10 @@ export class BaseStateHandler {
28
28
  initialState;
29
29
  // Holds the Redux DevTools instance if enabled.
30
30
  devTools = null;
31
+ // Tracks mounted consumers that have connected this handler.
32
+ connectCount = 0;
33
+ // Prevents duplicate side effects while the handler is already connected.
34
+ isConnected = false;
31
35
  // Keeps track of active subscriptions to allow for cleanup.
32
36
  subscriptions = [];
33
37
  // Tracks keyed subscriptions so handlers can replace them by name.
@@ -91,10 +95,82 @@ export class BaseStateHandler {
91
95
  // Notify DevTools of the state change.
92
96
  this.devTools?.send(actionName, nextState);
93
97
  }
98
+ /**
99
+ * Starts external effects for this handler.
100
+ */
101
+ connect() {
102
+ const previousConnectCount = this.connectCount;
103
+ this.connectCount += 1;
104
+ if (this.isConnected) {
105
+ return;
106
+ }
107
+ this.isConnected = true;
108
+ try {
109
+ this.onConnect();
110
+ }
111
+ catch (error) {
112
+ this.connectCount = previousConnectCount;
113
+ this.isConnected = false;
114
+ this.clearManagedSubscriptions();
115
+ throw error;
116
+ }
117
+ }
118
+ /**
119
+ * Stops external effects once all connected consumers have disconnected.
120
+ */
121
+ disconnect() {
122
+ if (this.connectCount === 0) {
123
+ return;
124
+ }
125
+ this.connectCount -= 1;
126
+ if (this.connectCount > 0) {
127
+ return;
128
+ }
129
+ if (!this.isConnected) {
130
+ this.clearManagedSubscriptions();
131
+ return;
132
+ }
133
+ try {
134
+ this.onDisconnect();
135
+ }
136
+ finally {
137
+ this.isConnected = false;
138
+ this.clearManagedSubscriptions();
139
+ }
140
+ }
94
141
  /**
95
142
  * Cleans up all active subscriptions when the handler is destroyed.
96
143
  */
97
144
  destroy() {
145
+ this.connectCount = 0;
146
+ if (!this.isConnected) {
147
+ this.clearManagedSubscriptions();
148
+ return;
149
+ }
150
+ try {
151
+ this.onDisconnect();
152
+ }
153
+ finally {
154
+ this.isConnected = false;
155
+ this.clearManagedSubscriptions();
156
+ }
157
+ }
158
+ /**
159
+ * Optional hook for subclasses that need to start external effects.
160
+ */
161
+ onConnect() {
162
+ // Optional override.
163
+ }
164
+ /**
165
+ * Optional hook for subclasses that need to stop external effects.
166
+ */
167
+ onDisconnect() {
168
+ // Optional override.
169
+ }
170
+ /**
171
+ * Clears all subscriptions managed through this base handler.
172
+ */
173
+ clearManagedSubscriptions() {
98
174
  const subscriptions = [...this.subscriptions];
99
175
  const namedSubscriptions = [...this.namedSubscriptions.values()];
100
176
  this.subscriptions = [];
@@ -1 +1 @@
1
- {"version":3,"file":"base-state-handler.js","sourceRoot":"","sources":["../../src/store/base-state-handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"base-state-handler.js","sourceRoot":"","sources":["../../src/store/base-state-handler.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,8 @@
10
10
  export interface StateSubscriptionHandler<V, A> {
11
11
  subscribe(listener: () => void): () => void;
12
12
  subscribe(listener: (value: V) => void): () => void;
13
+ connect?: () => void;
14
+ disconnect?: () => void;
13
15
  getSnapshot: () => V;
14
16
  destroy: () => void;
15
17
  getInitialState: () => V;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veams/status-quo",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "The manager to rule states in frontend.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -13,7 +13,7 @@ import type { StateSubscriptionHandler } from '../../../types/types.js';
13
13
 
14
14
  declare global {
15
15
  // React 19 requires this flag in test environments that use manual act() calls.
16
-
16
+
17
17
  var IS_REACT_ACT_ENVIRONMENT: boolean;
18
18
  }
19
19
 
@@ -106,6 +106,11 @@ class TestStateHandler implements StateSubscriptionHandler<TestState, TestAction
106
106
  }
107
107
  }
108
108
 
109
+ class LifecycleTestStateHandler extends TestStateHandler {
110
+ connect = jest.fn();
111
+ disconnect = jest.fn();
112
+ }
113
+
109
114
  class CounterStateHandler implements StateSubscriptionHandler<CounterState, CounterActions> {
110
115
  private readonly initialState: CounterState;
111
116
  private state: CounterState;
@@ -677,6 +682,160 @@ describe('Selector hooks', () => {
677
682
  });
678
683
  });
679
684
 
685
+ it('useStateSubscription should connect after commit and defer disconnect cleanup', async () => {
686
+ const stateHandler = new LifecycleTestStateHandler({
687
+ user: { name: 'Ada' },
688
+ counter: 0,
689
+ });
690
+ const createStateHandler = jest.fn(() => stateHandler);
691
+ const renderConnectCallCounts: number[] = [];
692
+ const renderSpy = jest.fn(() => {
693
+ renderConnectCallCounts.push(stateHandler.connect.mock.calls.length);
694
+ });
695
+ const actionsReadySpy = jest.fn<void, [TestActions]>();
696
+
697
+ act(() => {
698
+ root.render(
699
+ <FullSubscriptionConsumer
700
+ createStateHandler={createStateHandler}
701
+ onRender={renderSpy}
702
+ onActionsReady={actionsReadySpy}
703
+ />
704
+ );
705
+ });
706
+
707
+ expect(renderConnectCallCounts).toStrictEqual([0]);
708
+ expect(stateHandler.connect).toHaveBeenCalledTimes(1);
709
+ expect(stateHandler.disconnect).not.toHaveBeenCalled();
710
+ expect(stateHandler.destroy).not.toHaveBeenCalled();
711
+
712
+ act(() => {
713
+ root.render(<React.Fragment />);
714
+ });
715
+
716
+ expect(stateHandler.disconnect).not.toHaveBeenCalled();
717
+ expect(stateHandler.destroy).not.toHaveBeenCalled();
718
+
719
+ await act(async () => {
720
+ await new Promise((resolve) => {
721
+ setTimeout(resolve, 0);
722
+ });
723
+ });
724
+
725
+ expect(stateHandler.disconnect).toHaveBeenCalledTimes(1);
726
+ expect(stateHandler.destroy).toHaveBeenCalledTimes(1);
727
+ });
728
+
729
+ it('useStateSubscription should keep the connection through immediate resubscription', async () => {
730
+ const stateHandler = new LifecycleTestStateHandler({
731
+ user: { name: 'Ada' },
732
+ counter: 0,
733
+ });
734
+ const createStateHandler = jest.fn(() => stateHandler);
735
+ const renderSpy = jest.fn();
736
+ const actionsReadySpy = jest.fn<void, [TestActions]>();
737
+
738
+ act(() => {
739
+ root.render(
740
+ <FullSubscriptionConsumer
741
+ createStateHandler={createStateHandler}
742
+ onRender={renderSpy}
743
+ onActionsReady={actionsReadySpy}
744
+ />
745
+ );
746
+ });
747
+
748
+ act(() => {
749
+ root.render(<React.Fragment />);
750
+ });
751
+
752
+ act(() => {
753
+ root.render(
754
+ <FullSubscriptionConsumer
755
+ createStateHandler={createStateHandler}
756
+ onRender={renderSpy}
757
+ onActionsReady={actionsReadySpy}
758
+ />
759
+ );
760
+ });
761
+
762
+ await act(async () => {
763
+ await new Promise((resolve) => {
764
+ setTimeout(resolve, 0);
765
+ });
766
+ });
767
+
768
+ expect(stateHandler.connect).toHaveBeenCalledTimes(1);
769
+ expect(stateHandler.disconnect).not.toHaveBeenCalled();
770
+ expect(stateHandler.destroy).not.toHaveBeenCalled();
771
+
772
+ act(() => {
773
+ root.render(<React.Fragment />);
774
+ });
775
+
776
+ await act(async () => {
777
+ await new Promise((resolve) => {
778
+ setTimeout(resolve, 0);
779
+ });
780
+ });
781
+
782
+ expect(stateHandler.disconnect).toHaveBeenCalledTimes(1);
783
+ expect(stateHandler.destroy).toHaveBeenCalledTimes(1);
784
+ });
785
+
786
+ it('useStateSubscription should disconnect only after the last consumer unsubscribes', async () => {
787
+ const stateHandler = new LifecycleTestStateHandler({
788
+ user: { name: 'Ada' },
789
+ counter: 0,
790
+ });
791
+ const createStateHandler = jest.fn(() => stateHandler);
792
+ const firstRenderSpy = jest.fn();
793
+ const secondRenderSpy = jest.fn();
794
+ const actionsReadySpy = jest.fn<void, [TestActions]>();
795
+ const Consumers = ({ showSecond }: { showSecond: boolean }) => (
796
+ <React.Fragment>
797
+ <FullSubscriptionConsumer
798
+ createStateHandler={createStateHandler}
799
+ onRender={firstRenderSpy}
800
+ onActionsReady={actionsReadySpy}
801
+ />
802
+ {showSecond && (
803
+ <FullSubscriptionConsumer
804
+ createStateHandler={createStateHandler}
805
+ onRender={secondRenderSpy}
806
+ onActionsReady={actionsReadySpy}
807
+ />
808
+ )}
809
+ </React.Fragment>
810
+ );
811
+
812
+ act(() => {
813
+ root.render(<Consumers showSecond />);
814
+ });
815
+
816
+ expect(stateHandler.connect).toHaveBeenCalledTimes(1);
817
+
818
+ act(() => {
819
+ root.render(<Consumers showSecond={false} />);
820
+ });
821
+
822
+ expect(stateHandler.disconnect).not.toHaveBeenCalled();
823
+ expect(stateHandler.destroy).not.toHaveBeenCalled();
824
+
825
+ act(() => {
826
+ root.render(<React.Fragment />);
827
+ });
828
+
829
+ await act(async () => {
830
+ await new Promise((resolve) => {
831
+ setTimeout(resolve, 0);
832
+ });
833
+ });
834
+
835
+ expect(stateHandler.disconnect).toHaveBeenCalledTimes(1);
836
+ expect(stateHandler.destroy).toHaveBeenCalledTimes(1);
837
+ });
838
+
680
839
  it('useStateSingleton selector should keep singleton alive while consumers exist', () => {
681
840
  const stateHandler = new TestStateHandler({
682
841
  user: { name: 'Ada' },
@@ -20,12 +20,12 @@ type Listener = () => void;
20
20
  type SharedStateSubscriptionHandler = StateSubscriptionHandler<unknown, unknown>;
21
21
 
22
22
  /**
23
- * Tracks the reference count and deferred destruction status of a state handler.
23
+ * Tracks the reference count and deferred lifecycle cleanup status of a state handler.
24
24
  */
25
- type DeferredDestroy = {
25
+ type DeferredLifecycle = {
26
26
  // Number of active consumers of the state handler.
27
27
  refCount: number;
28
- // ID of the timeout for deferred destruction.
28
+ // ID of the timeout for deferred disconnect/destruction.
29
29
  timeoutId: ReturnType<typeof setTimeout> | null;
30
30
  };
31
31
 
@@ -51,18 +51,18 @@ type ServerSnapshotCacheEntry<Source, Selected> = {
51
51
  sourceSnapshot: Source;
52
52
  };
53
53
 
54
- // Global map to track deferred destruction status for each state handler instance.
55
- const deferredDestroyMap = new WeakMap<SharedStateSubscriptionHandler, DeferredDestroy>();
54
+ // Global map to track deferred lifecycle status for each state handler instance.
55
+ const deferredLifecycleMap = new WeakMap<SharedStateSubscriptionHandler, DeferredLifecycle>();
56
56
 
57
57
  /**
58
- * Returns the deferred destruction status for a given state handler instance.
58
+ * Returns the deferred lifecycle status for a given state handler instance.
59
59
  * Initializes the status if it does not already exist.
60
60
  */
61
- function getDeferredDestroyState(
61
+ function getDeferredLifecycleState(
62
62
  stateSubscriptionHandler: SharedStateSubscriptionHandler
63
- ): DeferredDestroy {
63
+ ): DeferredLifecycle {
64
64
  // Retrieve the existing status from the map.
65
- const existingState = deferredDestroyMap.get(stateSubscriptionHandler);
65
+ const existingState = deferredLifecycleMap.get(stateSubscriptionHandler);
66
66
 
67
67
  // If status already exists, return it.
68
68
  if (existingState) {
@@ -70,12 +70,12 @@ function getDeferredDestroyState(
70
70
  }
71
71
 
72
72
  // Create and store a new status for the handler.
73
- const nextState: DeferredDestroy = {
73
+ const nextState: DeferredLifecycle = {
74
74
  refCount: 0,
75
75
  timeoutId: null,
76
76
  };
77
77
 
78
- deferredDestroyMap.set(stateSubscriptionHandler, nextState);
78
+ deferredLifecycleMap.set(stateSubscriptionHandler, nextState);
79
79
 
80
80
  return nextState;
81
81
  }
@@ -113,17 +113,20 @@ export function useStateSubscriptionSelector<V, A, Sel>(
113
113
  // Subscription function to be used by useSyncExternalStore.
114
114
  const subscribe = useCallback(
115
115
  (listener: Listener) => {
116
- // Access the deferred destruction status for this handler.
116
+ // Access the deferred lifecycle status for this handler.
117
117
  const sharedStateSubscriptionHandler =
118
118
  stateSubscriptionHandler as unknown as SharedStateSubscriptionHandler;
119
- const deferredDestroyState = getDeferredDestroyState(sharedStateSubscriptionHandler);
119
+ const deferredLifecycleState = getDeferredLifecycleState(sharedStateSubscriptionHandler);
120
+ const hadPendingCleanup = deferredLifecycleState.timeoutId !== null;
121
+ const wasIdle = deferredLifecycleState.refCount === 0;
122
+
120
123
  // Increment the consumer reference count.
121
- deferredDestroyState.refCount += 1;
124
+ deferredLifecycleState.refCount += 1;
122
125
 
123
- // If a pending destruction timeout is scheduled, cancel it.
124
- if (deferredDestroyState.timeoutId) {
125
- clearTimeout(deferredDestroyState.timeoutId);
126
- deferredDestroyState.timeoutId = null;
126
+ // If a pending cleanup timeout is scheduled, cancel it.
127
+ if (deferredLifecycleState.timeoutId) {
128
+ clearTimeout(deferredLifecycleState.timeoutId);
129
+ deferredLifecycleState.timeoutId = null;
127
130
  }
128
131
 
129
132
  // Subscribe to the state handler.
@@ -135,51 +138,62 @@ export function useStateSubscriptionSelector<V, A, Sel>(
135
138
  listener();
136
139
  });
137
140
 
141
+ if (wasIdle && !hadPendingCleanup) {
142
+ stateSubscriptionHandler.connect?.();
143
+ }
144
+
138
145
  // Return an unsubscribe function to be called by React.
139
146
  return () => {
140
147
  // Execute the handler's unsubscribe method.
141
148
  unsubscribe();
142
149
 
143
- // If automatic cleanup is disabled, stop here.
144
- if (!destroyOnCleanup) {
145
- return;
146
- }
147
-
148
- // Retrieve the current destruction status.
149
- const activeDeferredDestroyState = deferredDestroyMap.get(sharedStateSubscriptionHandler);
150
+ // Retrieve the current lifecycle status.
151
+ const activeDeferredLifecycleState = deferredLifecycleMap.get(
152
+ sharedStateSubscriptionHandler
153
+ );
150
154
 
151
155
  // If no status is found, stop here.
152
- if (!activeDeferredDestroyState) {
156
+ if (!activeDeferredLifecycleState) {
153
157
  return;
154
158
  }
155
159
 
156
160
  // Decrement the consumer reference count.
157
- activeDeferredDestroyState.refCount -= 1;
161
+ activeDeferredLifecycleState.refCount -= 1;
158
162
 
159
- // If there are still active consumers, do not destroy the handler.
160
- if (activeDeferredDestroyState.refCount > 0) {
163
+ // If there are still active consumers, keep the handler connected.
164
+ if (activeDeferredLifecycleState.refCount > 0) {
161
165
  return;
162
166
  }
163
167
 
164
168
  // Reset the reference count to zero.
165
- activeDeferredDestroyState.refCount = 0;
166
- // Schedule deferred destruction to allow for potential immediate re-subscriptions.
167
- activeDeferredDestroyState.timeoutId = setTimeout(() => {
169
+ activeDeferredLifecycleState.refCount = 0;
170
+ // Schedule deferred cleanup to allow for potential immediate re-subscriptions.
171
+ activeDeferredLifecycleState.timeoutId = setTimeout(() => {
168
172
  // Check if the handler still has no consumers after the timeout.
169
- const pendingDeferredDestroyState = deferredDestroyMap.get(
173
+ const pendingDeferredLifecycleState = deferredLifecycleMap.get(
170
174
  sharedStateSubscriptionHandler
171
175
  );
172
176
 
173
- // If consumers have reappeared, do not destroy the handler.
174
- if (!pendingDeferredDestroyState || pendingDeferredDestroyState.refCount > 0) {
177
+ // If consumers have reappeared, keep the handler connected.
178
+ if (!pendingDeferredLifecycleState || pendingDeferredLifecycleState.refCount > 0) {
175
179
  return;
176
180
  }
177
181
 
178
- // Clear the pending timeout and destroy the state handler.
179
- pendingDeferredDestroyState.timeoutId = null;
180
- stateSubscriptionHandler.destroy();
181
- // Remove the status from the global map.
182
- deferredDestroyMap.delete(sharedStateSubscriptionHandler);
182
+ // Clear the pending timeout and disconnect the state handler.
183
+ pendingDeferredLifecycleState.timeoutId = null;
184
+
185
+ try {
186
+ stateSubscriptionHandler.disconnect?.();
187
+ } finally {
188
+ if (destroyOnCleanup) {
189
+ try {
190
+ stateSubscriptionHandler.destroy();
191
+ } finally {
192
+ // Remove the status from the global map after final destruction.
193
+ deferredLifecycleMap.delete(sharedStateSubscriptionHandler);
194
+ }
195
+ }
196
+ }
183
197
  }, 0);
184
198
  };
185
199
  },
@@ -51,6 +51,7 @@ type CounterBucketSelection = { bucket: number };
51
51
  type CounterBucketState = { bucket: number };
52
52
  type SetState = { openItems: Set<string> };
53
53
  type SetActions = { toggle: (id: string) => void };
54
+ type NoopActions = { noop: () => void };
54
55
  type CounterSubscribable = {
55
56
  subscribe: (listener: (value: CounterState) => void) => () => void;
56
57
  getSnapshot: () => CounterState;
@@ -200,6 +201,37 @@ class SetNativeStateHandler extends NativeStateHandler<SetState, SetActions> {
200
201
  }
201
202
  }
202
203
 
204
+ class LifecycleNativeStateHandler extends NativeStateHandler<CounterState, NoopActions> {
205
+ constructor(
206
+ private readonly onConnectSpy: () => void,
207
+ private readonly onDisconnectSpy: () => void
208
+ ) {
209
+ super({
210
+ initialState: {
211
+ count: 0,
212
+ },
213
+ });
214
+ }
215
+
216
+ trackSubscription(subscription: { unsubscribe: () => void }) {
217
+ this.subscriptions = [...this.subscriptions, subscription];
218
+ }
219
+
220
+ protected override onConnect(): void {
221
+ this.onConnectSpy();
222
+ }
223
+
224
+ protected override onDisconnect(): void {
225
+ this.onDisconnectSpy();
226
+ }
227
+
228
+ getActions(): NoopActions {
229
+ return {
230
+ noop: () => undefined,
231
+ };
232
+ }
233
+ }
234
+
203
235
  function createCounterSubscribable(initialCount: number) {
204
236
  let listener: ((value: CounterState) => void) | null = null;
205
237
  const unsubscribe = jest.fn(() => {
@@ -269,6 +301,54 @@ describe('Native State Handler', () => {
269
301
  expect(spy).toHaveBeenCalledTimes(1);
270
302
  });
271
303
 
304
+ it('should connect and disconnect side effects with reference counting', () => {
305
+ const onConnectSpy = jest.fn();
306
+ const onDisconnectSpy = jest.fn();
307
+ const handler = new LifecycleNativeStateHandler(onConnectSpy, onDisconnectSpy);
308
+
309
+ expect(handler.getSnapshot()).toStrictEqual({ count: 0 });
310
+ expect(onConnectSpy).not.toHaveBeenCalled();
311
+
312
+ handler.connect();
313
+ handler.connect();
314
+ handler.disconnect();
315
+
316
+ expect(onConnectSpy).toHaveBeenCalledTimes(1);
317
+ expect(onDisconnectSpy).not.toHaveBeenCalled();
318
+
319
+ handler.disconnect();
320
+
321
+ expect(onDisconnectSpy).toHaveBeenCalledTimes(1);
322
+ });
323
+
324
+ it('should clear managed subscriptions on disconnect', () => {
325
+ const handler = new LifecycleNativeStateHandler(jest.fn(), jest.fn());
326
+ const unsubscribeSpy = jest.fn();
327
+
328
+ handler.trackSubscription({ unsubscribe: unsubscribeSpy });
329
+ handler.connect();
330
+ handler.disconnect();
331
+
332
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(1);
333
+ expect(handler.subscriptions).toStrictEqual([]);
334
+ });
335
+
336
+ it('should disconnect and clear managed subscriptions on destroy', () => {
337
+ const onDisconnectSpy = jest.fn();
338
+ const handler = new LifecycleNativeStateHandler(jest.fn(), onDisconnectSpy);
339
+ const unsubscribeSpy = jest.fn();
340
+
341
+ handler.trackSubscription({ unsubscribe: unsubscribeSpy });
342
+ handler.connect();
343
+ handler.connect();
344
+
345
+ handler.destroy();
346
+ handler.disconnect();
347
+
348
+ expect(onDisconnectSpy).toHaveBeenCalledTimes(1);
349
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(1);
350
+ });
351
+
272
352
  it('should call subscriber when state has changed and also on initial subscribe', () => {
273
353
  const spy = jest.fn();
274
354
  const unsubscribe = stateHandler.subscribe(spy);