@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.
- package/CHANGELOG.md +2 -0
- package/README.md +21 -2
- package/dist/react/hooks/__tests__/state-selector.spec.js +103 -0
- package/dist/react/hooks/__tests__/state-selector.spec.js.map +1 -1
- package/dist/react/hooks/state-subscription-selector.js +46 -34
- package/dist/react/hooks/state-subscription-selector.js.map +1 -1
- package/dist/store/__tests__/native-state-handler.spec.js +62 -0
- package/dist/store/__tests__/native-state-handler.spec.js.map +1 -1
- package/dist/store/base-state-handler.d.ts +22 -0
- package/dist/store/base-state-handler.js +76 -0
- package/dist/store/base-state-handler.js.map +1 -1
- package/dist/types/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/react/hooks/__tests__/state-selector.spec.tsx +160 -1
- package/src/react/hooks/state-subscription-selector.tsx +54 -40
- package/src/store/__tests__/native-state-handler.spec.ts +80 -0
- package/src/store/base-state-handler.ts +88 -0
- package/src/types/types.ts +4 -0
|
@@ -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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"}
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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
|
@@ -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
|
|
23
|
+
* Tracks the reference count and deferred lifecycle cleanup status of a state handler.
|
|
24
24
|
*/
|
|
25
|
-
type
|
|
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
|
|
55
|
-
const
|
|
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
|
|
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
|
|
61
|
+
function getDeferredLifecycleState(
|
|
62
62
|
stateSubscriptionHandler: SharedStateSubscriptionHandler
|
|
63
|
-
):
|
|
63
|
+
): DeferredLifecycle {
|
|
64
64
|
// Retrieve the existing status from the map.
|
|
65
|
-
const existingState =
|
|
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:
|
|
73
|
+
const nextState: DeferredLifecycle = {
|
|
74
74
|
refCount: 0,
|
|
75
75
|
timeoutId: null,
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
-
|
|
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
|
|
116
|
+
// Access the deferred lifecycle status for this handler.
|
|
117
117
|
const sharedStateSubscriptionHandler =
|
|
118
118
|
stateSubscriptionHandler as unknown as SharedStateSubscriptionHandler;
|
|
119
|
-
const
|
|
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
|
-
|
|
124
|
+
deferredLifecycleState.refCount += 1;
|
|
122
125
|
|
|
123
|
-
// If a pending
|
|
124
|
-
if (
|
|
125
|
-
clearTimeout(
|
|
126
|
-
|
|
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
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
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 (!
|
|
156
|
+
if (!activeDeferredLifecycleState) {
|
|
153
157
|
return;
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
// Decrement the consumer reference count.
|
|
157
|
-
|
|
161
|
+
activeDeferredLifecycleState.refCount -= 1;
|
|
158
162
|
|
|
159
|
-
// If there are still active consumers,
|
|
160
|
-
if (
|
|
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
|
-
|
|
166
|
-
// Schedule deferred
|
|
167
|
-
|
|
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
|
|
173
|
+
const pendingDeferredLifecycleState = deferredLifecycleMap.get(
|
|
170
174
|
sharedStateSubscriptionHandler
|
|
171
175
|
);
|
|
172
176
|
|
|
173
|
-
// If consumers have reappeared,
|
|
174
|
-
if (!
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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);
|