@veams/status-quo 1.7.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-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/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 +64 -8
- 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/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 +85 -7
- 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 and selecting a specific value from the state.
|
|
3
|
+
* Uses useSyncExternalStore to ensure consistent state reads and avoid unnecessary re-renders.
|
|
4
|
+
*/
|
|
1
5
|
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
2
6
|
|
|
3
7
|
import { createSelectorCache, selectWithCache } from '../../utils/selector-cache.js';
|
|
@@ -5,33 +9,67 @@ import { createSelectorCache, selectWithCache } from '../../utils/selector-cache
|
|
|
5
9
|
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
6
10
|
import type { EqualityFn, Selector } from '../../utils/selector-cache.js';
|
|
7
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Type signature for listener functions.
|
|
14
|
+
*/
|
|
8
15
|
type Listener = () => void;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Represents a state subscription handler with unknown state and actions.
|
|
19
|
+
*/
|
|
9
20
|
type SharedStateSubscriptionHandler = StateSubscriptionHandler<unknown, unknown>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Tracks the reference count and deferred destruction status of a state handler.
|
|
24
|
+
*/
|
|
10
25
|
type DeferredDestroy = {
|
|
26
|
+
// Number of active consumers of the state handler.
|
|
11
27
|
refCount: number;
|
|
28
|
+
// ID of the timeout for deferred destruction.
|
|
12
29
|
timeoutId: ReturnType<typeof setTimeout> | null;
|
|
13
30
|
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Cache entry for a selected state snapshot.
|
|
34
|
+
*/
|
|
14
35
|
type SnapshotCacheEntry<Source, Selected> = {
|
|
36
|
+
// The selected value derived from the source snapshot.
|
|
15
37
|
selectedSnapshot: Selected;
|
|
38
|
+
// The source snapshot from which the value was selected.
|
|
16
39
|
sourceSnapshot: Source;
|
|
40
|
+
// A version number to track state changes.
|
|
17
41
|
version: number;
|
|
18
42
|
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Cache entry for a selected server state snapshot (SSR).
|
|
46
|
+
*/
|
|
19
47
|
type ServerSnapshotCacheEntry<Source, Selected> = {
|
|
48
|
+
// The selected value derived from the source snapshot.
|
|
20
49
|
selectedSnapshot: Selected;
|
|
50
|
+
// The source snapshot from which the value was selected.
|
|
21
51
|
sourceSnapshot: Source;
|
|
22
52
|
};
|
|
23
53
|
|
|
54
|
+
// Global map to track deferred destruction status for each state handler instance.
|
|
24
55
|
const deferredDestroyMap = new WeakMap<SharedStateSubscriptionHandler, DeferredDestroy>();
|
|
25
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Returns the deferred destruction status for a given state handler instance.
|
|
59
|
+
* Initializes the status if it does not already exist.
|
|
60
|
+
*/
|
|
26
61
|
function getDeferredDestroyState(
|
|
27
62
|
stateSubscriptionHandler: SharedStateSubscriptionHandler
|
|
28
63
|
): DeferredDestroy {
|
|
64
|
+
// Retrieve the existing status from the map.
|
|
29
65
|
const existingState = deferredDestroyMap.get(stateSubscriptionHandler);
|
|
30
66
|
|
|
67
|
+
// If status already exists, return it.
|
|
31
68
|
if (existingState) {
|
|
32
69
|
return existingState;
|
|
33
70
|
}
|
|
34
71
|
|
|
72
|
+
// Create and store a new status for the handler.
|
|
35
73
|
const nextState: DeferredDestroy = {
|
|
36
74
|
refCount: 0,
|
|
37
75
|
timeoutId: null,
|
|
@@ -42,78 +80,105 @@ function getDeferredDestroyState(
|
|
|
42
80
|
return nextState;
|
|
43
81
|
}
|
|
44
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Custom hook to select and subscribe to a specific piece of state from a handler.
|
|
85
|
+
* Efficiently manages subscriptions and selector results.
|
|
86
|
+
*/
|
|
45
87
|
export function useStateSubscriptionSelector<V, A, Sel>(
|
|
88
|
+
// The state handler instance to subscribe to.
|
|
46
89
|
stateSubscriptionHandler: StateSubscriptionHandler<V, A>,
|
|
90
|
+
// Selector function to derive a value from the state.
|
|
47
91
|
selector: Selector<V, Sel>,
|
|
92
|
+
// Equality function to compare selected values for changes.
|
|
48
93
|
isEqual: EqualityFn<Sel> = Object.is,
|
|
94
|
+
// Whether to automatically destroy the handler instance on component unmount.
|
|
49
95
|
destroyOnCleanup = true
|
|
50
96
|
) {
|
|
97
|
+
// Cache for the selector results to ensure referential stability.
|
|
51
98
|
const selectorCacheRef = useRef<ReturnType<typeof createSelectorCache<Sel>> | null>(null);
|
|
52
|
-
// Tracks store notifications so getSnapshot can reuse the same selected value
|
|
53
|
-
// within one store version. This keeps useSyncExternalStore reads referentially stable.
|
|
99
|
+
// Tracks store notifications so getSnapshot can reuse the same selected value within one store version.
|
|
54
100
|
const snapshotVersionRef = useRef(0);
|
|
55
101
|
// Client-side cache for selected snapshots per source snapshot/version pair.
|
|
56
102
|
const snapshotCacheRef = useRef<SnapshotCacheEntry<V, Sel> | null>(null);
|
|
57
103
|
// Separate cache for the server snapshot function used by SSR/hydration paths.
|
|
58
104
|
const serverSnapshotCacheRef = useRef<ServerSnapshotCacheEntry<V, Sel> | null>(null);
|
|
59
105
|
|
|
106
|
+
// Initialize the selector cache if it doesn't already exist.
|
|
60
107
|
if (!selectorCacheRef.current) {
|
|
61
108
|
selectorCacheRef.current = createSelectorCache<Sel>();
|
|
62
109
|
}
|
|
63
110
|
|
|
64
111
|
const selectorCache = selectorCacheRef.current;
|
|
65
112
|
|
|
113
|
+
// Subscription function to be used by useSyncExternalStore.
|
|
66
114
|
const subscribe = useCallback(
|
|
67
115
|
(listener: Listener) => {
|
|
116
|
+
// Access the deferred destruction status for this handler.
|
|
68
117
|
const sharedStateSubscriptionHandler =
|
|
69
118
|
stateSubscriptionHandler as unknown as SharedStateSubscriptionHandler;
|
|
70
119
|
const deferredDestroyState = getDeferredDestroyState(sharedStateSubscriptionHandler);
|
|
120
|
+
// Increment the consumer reference count.
|
|
71
121
|
deferredDestroyState.refCount += 1;
|
|
72
122
|
|
|
123
|
+
// If a pending destruction timeout is scheduled, cancel it.
|
|
73
124
|
if (deferredDestroyState.timeoutId) {
|
|
74
125
|
clearTimeout(deferredDestroyState.timeoutId);
|
|
75
126
|
deferredDestroyState.timeoutId = null;
|
|
76
127
|
}
|
|
77
128
|
|
|
129
|
+
// Subscribe to the state handler.
|
|
78
130
|
const unsubscribe = stateSubscriptionHandler.subscribe(() => {
|
|
79
|
-
// Invalidate the selected snapshot cache before notifying React.
|
|
80
|
-
// Any next getSnapshot call should recompute from the new store state.
|
|
131
|
+
// Invalidate the selected snapshot cache before notifying React of a change.
|
|
81
132
|
snapshotVersionRef.current += 1;
|
|
82
133
|
snapshotCacheRef.current = null;
|
|
134
|
+
// Notify React to re-trigger a getSnapshot call.
|
|
83
135
|
listener();
|
|
84
136
|
});
|
|
85
137
|
|
|
138
|
+
// Return an unsubscribe function to be called by React.
|
|
86
139
|
return () => {
|
|
140
|
+
// Execute the handler's unsubscribe method.
|
|
87
141
|
unsubscribe();
|
|
88
142
|
|
|
143
|
+
// If automatic cleanup is disabled, stop here.
|
|
89
144
|
if (!destroyOnCleanup) {
|
|
90
145
|
return;
|
|
91
146
|
}
|
|
92
147
|
|
|
148
|
+
// Retrieve the current destruction status.
|
|
93
149
|
const activeDeferredDestroyState = deferredDestroyMap.get(sharedStateSubscriptionHandler);
|
|
94
150
|
|
|
151
|
+
// If no status is found, stop here.
|
|
95
152
|
if (!activeDeferredDestroyState) {
|
|
96
153
|
return;
|
|
97
154
|
}
|
|
98
155
|
|
|
156
|
+
// Decrement the consumer reference count.
|
|
99
157
|
activeDeferredDestroyState.refCount -= 1;
|
|
100
158
|
|
|
159
|
+
// If there are still active consumers, do not destroy the handler.
|
|
101
160
|
if (activeDeferredDestroyState.refCount > 0) {
|
|
102
161
|
return;
|
|
103
162
|
}
|
|
104
163
|
|
|
164
|
+
// Reset the reference count to zero.
|
|
105
165
|
activeDeferredDestroyState.refCount = 0;
|
|
166
|
+
// Schedule deferred destruction to allow for potential immediate re-subscriptions.
|
|
106
167
|
activeDeferredDestroyState.timeoutId = setTimeout(() => {
|
|
168
|
+
// Check if the handler still has no consumers after the timeout.
|
|
107
169
|
const pendingDeferredDestroyState = deferredDestroyMap.get(
|
|
108
170
|
sharedStateSubscriptionHandler
|
|
109
171
|
);
|
|
110
172
|
|
|
173
|
+
// If consumers have reappeared, do not destroy the handler.
|
|
111
174
|
if (!pendingDeferredDestroyState || pendingDeferredDestroyState.refCount > 0) {
|
|
112
175
|
return;
|
|
113
176
|
}
|
|
114
177
|
|
|
178
|
+
// Clear the pending timeout and destroy the state handler.
|
|
115
179
|
pendingDeferredDestroyState.timeoutId = null;
|
|
116
180
|
stateSubscriptionHandler.destroy();
|
|
181
|
+
// Remove the status from the global map.
|
|
117
182
|
deferredDestroyMap.delete(sharedStateSubscriptionHandler);
|
|
118
183
|
}, 0);
|
|
119
184
|
};
|
|
@@ -121,6 +186,7 @@ export function useStateSubscriptionSelector<V, A, Sel>(
|
|
|
121
186
|
[destroyOnCleanup, stateSubscriptionHandler]
|
|
122
187
|
);
|
|
123
188
|
|
|
189
|
+
// Helper to execute selection using the cache.
|
|
124
190
|
const selectSnapshot = useCallback(
|
|
125
191
|
(snapshot: V) => {
|
|
126
192
|
return selectWithCache(selectorCache, snapshot, selector, isEqual).value;
|
|
@@ -128,31 +194,36 @@ export function useStateSubscriptionSelector<V, A, Sel>(
|
|
|
128
194
|
[isEqual, selector, selectorCache]
|
|
129
195
|
);
|
|
130
196
|
|
|
197
|
+
// Reference to track changes in selection strategy.
|
|
131
198
|
const selectorCacheControlRef = useRef(selectSnapshot);
|
|
132
199
|
|
|
200
|
+
// If the selector or equality function changes, clear all caches.
|
|
133
201
|
if (selectorCacheControlRef.current !== selectSnapshot) {
|
|
134
|
-
// Selector/equality changes define a new selection strategy, so clear all caches.
|
|
135
202
|
selectorCacheControlRef.current = selectSnapshot;
|
|
136
203
|
snapshotVersionRef.current = 0;
|
|
137
204
|
snapshotCacheRef.current = null;
|
|
138
205
|
serverSnapshotCacheRef.current = null;
|
|
139
206
|
}
|
|
140
207
|
|
|
208
|
+
// Snapshot retrieval function to be used by useSyncExternalStore.
|
|
141
209
|
const getSnapshot = useCallback(
|
|
142
210
|
() => {
|
|
211
|
+
// Retrieve the current source state from the handler.
|
|
143
212
|
const sourceSnapshot = stateSubscriptionHandler.getSnapshot();
|
|
213
|
+
// Access the current version and cached value.
|
|
144
214
|
const version = snapshotVersionRef.current;
|
|
145
215
|
const cachedSnapshot = snapshotCacheRef.current;
|
|
146
216
|
|
|
217
|
+
// If the cached selection is still valid for this version and source state, return it.
|
|
147
218
|
if (
|
|
148
219
|
cachedSnapshot &&
|
|
149
220
|
cachedSnapshot.version === version &&
|
|
150
221
|
Object.is(cachedSnapshot.sourceSnapshot, sourceSnapshot)
|
|
151
222
|
) {
|
|
152
|
-
// Same source snapshot in the same store version: return the exact same selected reference.
|
|
153
223
|
return cachedSnapshot.selectedSnapshot;
|
|
154
224
|
}
|
|
155
225
|
|
|
226
|
+
// Compute a new selection and update the cache.
|
|
156
227
|
const selectedSnapshot = selectSnapshot(sourceSnapshot);
|
|
157
228
|
snapshotCacheRef.current = {
|
|
158
229
|
selectedSnapshot,
|
|
@@ -160,31 +231,38 @@ export function useStateSubscriptionSelector<V, A, Sel>(
|
|
|
160
231
|
version,
|
|
161
232
|
};
|
|
162
233
|
|
|
234
|
+
// Return the new selection.
|
|
163
235
|
return selectedSnapshot;
|
|
164
236
|
},
|
|
165
237
|
[selectSnapshot, stateSubscriptionHandler]
|
|
166
238
|
);
|
|
167
239
|
|
|
240
|
+
// Server snapshot retrieval function to be used for SSR/hydration.
|
|
168
241
|
const getServerSnapshot = useCallback(
|
|
169
242
|
() => {
|
|
243
|
+
// Retrieve the initial source state from the handler.
|
|
170
244
|
const sourceSnapshot = stateSubscriptionHandler.getInitialState();
|
|
245
|
+
// Access the current cached server snapshot.
|
|
171
246
|
const cachedSnapshot = serverSnapshotCacheRef.current;
|
|
172
247
|
|
|
248
|
+
// If the cached server selection is still valid for the initial state, return it.
|
|
173
249
|
if (cachedSnapshot && Object.is(cachedSnapshot.sourceSnapshot, sourceSnapshot)) {
|
|
174
|
-
// Keep server snapshot reads stable for hydration by reusing cached selection.
|
|
175
250
|
return cachedSnapshot.selectedSnapshot;
|
|
176
251
|
}
|
|
177
252
|
|
|
253
|
+
// Compute a new server selection and update its cache.
|
|
178
254
|
const selectedSnapshot = selectSnapshot(sourceSnapshot);
|
|
179
255
|
serverSnapshotCacheRef.current = {
|
|
180
256
|
selectedSnapshot,
|
|
181
257
|
sourceSnapshot,
|
|
182
258
|
};
|
|
183
259
|
|
|
260
|
+
// Return the initial selection result.
|
|
184
261
|
return selectedSnapshot;
|
|
185
262
|
},
|
|
186
263
|
[selectSnapshot, stateSubscriptionHandler]
|
|
187
264
|
);
|
|
188
265
|
|
|
266
|
+
// Use the useSyncExternalStore hook to integrate the state with React's rendering lifecycle.
|
|
189
267
|
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
190
268
|
}
|
|
@@ -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';
|