@veams/status-quo 1.10.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/.turbo/turbo-build.log +14 -12
- package/CHANGELOG.md +2 -0
- package/README.md +43 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -6
- package/dist/index.js.map +1 -1
- package/dist/observable/index.d.ts +5 -0
- package/dist/observable/index.js +6 -0
- package/dist/observable/index.js.map +1 -0
- 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/signals/index.d.ts +5 -0
- package/dist/signals/index.js +6 -0
- package/dist/signals/index.js.map +1 -0
- 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/store/index.d.ts +0 -2
- package/dist/store/index.js +0 -4
- package/dist/store/index.js.map +1 -1
- package/dist/types/types.d.ts +2 -0
- package/package.json +15 -1
- package/src/index.ts +0 -6
- package/src/observable/index.ts +5 -0
- package/src/react/hooks/__tests__/state-selector.spec.tsx +160 -1
- package/src/react/hooks/state-subscription-selector.tsx +54 -40
- package/src/signals/index.ts +5 -0
- package/src/store/__tests__/native-state-handler.spec.ts +80 -0
- package/src/store/base-state-handler.ts +88 -0
- package/src/store/index.ts +0 -4
- package/src/types/types.ts +4 -0
- package/dist/hooks/__tests__/state-provider.spec.d.ts +0 -4
- package/dist/hooks/__tests__/state-provider.spec.js +0 -179
- package/dist/hooks/__tests__/state-provider.spec.js.map +0 -1
- package/dist/hooks/__tests__/state-selector.spec.d.ts +0 -4
- package/dist/hooks/__tests__/state-selector.spec.js +0 -499
- package/dist/hooks/__tests__/state-selector.spec.js.map +0 -1
- package/dist/hooks/__tests__/state-singleton.spec.d.ts +0 -4
- package/dist/hooks/__tests__/state-singleton.spec.js +0 -96
- package/dist/hooks/__tests__/state-singleton.spec.js.map +0 -1
- package/dist/hooks/index.d.ts +0 -6
- package/dist/hooks/index.js +0 -7
- package/dist/hooks/index.js.map +0 -1
- package/dist/hooks/state-actions.d.ts +0 -2
- package/dist/hooks/state-actions.js +0 -5
- package/dist/hooks/state-actions.js.map +0 -1
- package/dist/hooks/state-factory.d.ts +0 -7
- package/dist/hooks/state-factory.js +0 -13
- package/dist/hooks/state-factory.js.map +0 -1
- package/dist/hooks/state-handler.d.ts +0 -2
- package/dist/hooks/state-handler.js +0 -9
- package/dist/hooks/state-handler.js.map +0 -1
- package/dist/hooks/state-provider.d.ts +0 -14
- package/dist/hooks/state-provider.js +0 -24
- package/dist/hooks/state-provider.js.map +0 -1
- package/dist/hooks/state-singleton.d.ts +0 -6
- package/dist/hooks/state-singleton.js +0 -7
- package/dist/hooks/state-singleton.js.map +0 -1
- package/dist/hooks/state-subscription-selector.d.ts +0 -3
- package/dist/hooks/state-subscription-selector.js +0 -63
- package/dist/hooks/state-subscription-selector.js.map +0 -1
- package/dist/hooks/state-subscription.d.ts +0 -9
- package/dist/hooks/state-subscription.js +0 -53
- package/dist/hooks/state-subscription.js.map +0 -1
|
@@ -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);
|
|
@@ -53,6 +53,11 @@ export abstract class BaseStateHandler<S, A> implements StateSubscriptionHandler
|
|
|
53
53
|
// Holds the Redux DevTools instance if enabled.
|
|
54
54
|
protected devTools: DevTools | null = null;
|
|
55
55
|
|
|
56
|
+
// Tracks mounted consumers that have connected this handler.
|
|
57
|
+
private connectCount = 0;
|
|
58
|
+
// Prevents duplicate side effects while the handler is already connected.
|
|
59
|
+
private isConnected = false;
|
|
60
|
+
|
|
56
61
|
// Keeps track of active subscriptions to allow for cleanup.
|
|
57
62
|
subscriptions: ManagedSubscription[] = [];
|
|
58
63
|
// Tracks keyed subscriptions so handlers can replace them by name.
|
|
@@ -127,10 +132,93 @@ export abstract class BaseStateHandler<S, A> implements StateSubscriptionHandler
|
|
|
127
132
|
this.devTools?.send(actionName, nextState);
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Starts external effects for this handler.
|
|
137
|
+
*/
|
|
138
|
+
connect(): void {
|
|
139
|
+
const previousConnectCount = this.connectCount;
|
|
140
|
+
this.connectCount += 1;
|
|
141
|
+
|
|
142
|
+
if (this.isConnected) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.isConnected = true;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
this.onConnect();
|
|
150
|
+
} catch (error) {
|
|
151
|
+
this.connectCount = previousConnectCount;
|
|
152
|
+
this.isConnected = false;
|
|
153
|
+
this.clearManagedSubscriptions();
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Stops external effects once all connected consumers have disconnected.
|
|
160
|
+
*/
|
|
161
|
+
disconnect(): void {
|
|
162
|
+
if (this.connectCount === 0) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.connectCount -= 1;
|
|
167
|
+
|
|
168
|
+
if (this.connectCount > 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!this.isConnected) {
|
|
173
|
+
this.clearManagedSubscriptions();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
this.onDisconnect();
|
|
179
|
+
} finally {
|
|
180
|
+
this.isConnected = false;
|
|
181
|
+
this.clearManagedSubscriptions();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
130
185
|
/**
|
|
131
186
|
* Cleans up all active subscriptions when the handler is destroyed.
|
|
132
187
|
*/
|
|
133
188
|
destroy(): void {
|
|
189
|
+
this.connectCount = 0;
|
|
190
|
+
|
|
191
|
+
if (!this.isConnected) {
|
|
192
|
+
this.clearManagedSubscriptions();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
this.onDisconnect();
|
|
198
|
+
} finally {
|
|
199
|
+
this.isConnected = false;
|
|
200
|
+
this.clearManagedSubscriptions();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Optional hook for subclasses that need to start external effects.
|
|
206
|
+
*/
|
|
207
|
+
protected onConnect(): void {
|
|
208
|
+
// Optional override.
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Optional hook for subclasses that need to stop external effects.
|
|
213
|
+
*/
|
|
214
|
+
protected onDisconnect(): void {
|
|
215
|
+
// Optional override.
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Clears all subscriptions managed through this base handler.
|
|
220
|
+
*/
|
|
221
|
+
protected clearManagedSubscriptions(): void {
|
|
134
222
|
const subscriptions = [...this.subscriptions];
|
|
135
223
|
const namedSubscriptions = [...this.namedSubscriptions.values()];
|
|
136
224
|
|
package/src/store/index.ts
CHANGED
|
@@ -6,10 +6,6 @@
|
|
|
6
6
|
export { BaseStateHandler } from './base-state-handler.js';
|
|
7
7
|
// Export the lightweight state handler using plain JavaScript.
|
|
8
8
|
export { NativeStateHandler } from './native-state-handler.js';
|
|
9
|
-
// Export the state handler powered by RxJS BehaviorSubjects.
|
|
10
|
-
export { ObservableStateHandler } from './observable-state-handler.js';
|
|
11
|
-
// Export the state handler powered by Preact Signals.
|
|
12
|
-
export { SignalStateHandler } from './signal-state-handler.js';
|
|
13
9
|
|
|
14
10
|
/**
|
|
15
11
|
* Export singleton related types and factory function.
|
package/src/types/types.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface StateSubscriptionHandler<V, A> {
|
|
|
13
13
|
subscribe(listener: () => void): () => void;
|
|
14
14
|
// Method to subscribe a listener that receives the updated state value.
|
|
15
15
|
subscribe(listener: (value: V) => void): () => void;
|
|
16
|
+
// Optional method to start external effects after a committed consumer subscribes.
|
|
17
|
+
connect?: () => void;
|
|
18
|
+
// Optional method to stop external effects after the last consumer unsubscribes.
|
|
19
|
+
disconnect?: () => void;
|
|
16
20
|
// Method to retrieve the current state snapshot.
|
|
17
21
|
getSnapshot: () => V;
|
|
18
22
|
// Method to clean up resources associated with the handler.
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import React, { act } from 'react';
|
|
2
|
-
import { createRoot } from 'react-dom/client';
|
|
3
|
-
import { StateProvider, useProvidedStateActions, useProvidedStateHandler, useProvidedStateSubscription, } from '../state-provider.js';
|
|
4
|
-
class TestStateHandler {
|
|
5
|
-
initialState;
|
|
6
|
-
state;
|
|
7
|
-
listeners = new Set();
|
|
8
|
-
destroy = jest.fn();
|
|
9
|
-
constructor(initialState) {
|
|
10
|
-
this.initialState = initialState;
|
|
11
|
-
this.state = initialState;
|
|
12
|
-
}
|
|
13
|
-
subscribe(listener) {
|
|
14
|
-
const typedListener = listener;
|
|
15
|
-
this.listeners.add(typedListener);
|
|
16
|
-
return () => {
|
|
17
|
-
this.listeners.delete(typedListener);
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
getSnapshot = () => {
|
|
21
|
-
return this.state;
|
|
22
|
-
};
|
|
23
|
-
getInitialState = () => {
|
|
24
|
-
return this.initialState;
|
|
25
|
-
};
|
|
26
|
-
getActions = () => {
|
|
27
|
-
return {
|
|
28
|
-
increment: () => {
|
|
29
|
-
this.state = {
|
|
30
|
-
...this.state,
|
|
31
|
-
count: this.state.count + 1,
|
|
32
|
-
};
|
|
33
|
-
this.emitStateChange();
|
|
34
|
-
},
|
|
35
|
-
rename: (label) => {
|
|
36
|
-
this.state = {
|
|
37
|
-
...this.state,
|
|
38
|
-
label,
|
|
39
|
-
};
|
|
40
|
-
this.emitStateChange();
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
};
|
|
44
|
-
emitStateChange() {
|
|
45
|
-
const nextState = this.state;
|
|
46
|
-
this.listeners.forEach((listener) => listener(nextState));
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function CountConsumer({ onRender }) {
|
|
50
|
-
const [count] = useProvidedStateSubscription((state) => state.count);
|
|
51
|
-
onRender(count);
|
|
52
|
-
return React.createElement("span", null, count);
|
|
53
|
-
}
|
|
54
|
-
function FullStateConsumer({ onActionsReady, onRender, }) {
|
|
55
|
-
const [state, actions] = useProvidedStateSubscription();
|
|
56
|
-
onRender(state);
|
|
57
|
-
onActionsReady(actions);
|
|
58
|
-
return React.createElement("span", null, state.label);
|
|
59
|
-
}
|
|
60
|
-
function ActionsOnlyConsumer({ onActionsReady, onRender, }) {
|
|
61
|
-
const actions = useProvidedStateActions();
|
|
62
|
-
onRender();
|
|
63
|
-
onActionsReady(actions);
|
|
64
|
-
return React.createElement("span", null, "actions-only");
|
|
65
|
-
}
|
|
66
|
-
function HandlerConsumer({ onHandlerReady, }) {
|
|
67
|
-
const handler = useProvidedStateHandler();
|
|
68
|
-
onHandlerReady(handler);
|
|
69
|
-
return React.createElement("span", null, "handler");
|
|
70
|
-
}
|
|
71
|
-
function MissingProviderConsumer() {
|
|
72
|
-
useProvidedStateActions();
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
describe('StateProvider', () => {
|
|
76
|
-
let container;
|
|
77
|
-
let root;
|
|
78
|
-
beforeAll(() => {
|
|
79
|
-
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
80
|
-
});
|
|
81
|
-
beforeEach(() => {
|
|
82
|
-
container = document.createElement('div');
|
|
83
|
-
document.body.appendChild(container);
|
|
84
|
-
root = createRoot(container);
|
|
85
|
-
});
|
|
86
|
-
afterEach(() => {
|
|
87
|
-
act(() => {
|
|
88
|
-
root.unmount();
|
|
89
|
-
});
|
|
90
|
-
container.remove();
|
|
91
|
-
});
|
|
92
|
-
afterAll(() => {
|
|
93
|
-
globalThis.IS_REACT_ACT_ENVIRONMENT = false;
|
|
94
|
-
});
|
|
95
|
-
it('should share one handler instance across the provider subtree', () => {
|
|
96
|
-
const stateHandler = new TestStateHandler({
|
|
97
|
-
count: 0,
|
|
98
|
-
label: 'Counter',
|
|
99
|
-
});
|
|
100
|
-
const countRenderSpy = jest.fn();
|
|
101
|
-
const actionsRenderSpy = jest.fn();
|
|
102
|
-
const actionsReadySpy = jest.fn();
|
|
103
|
-
const handlerReadySpy = jest.fn();
|
|
104
|
-
act(() => {
|
|
105
|
-
root.render(React.createElement(StateProvider, { instance: stateHandler },
|
|
106
|
-
React.createElement(CountConsumer, { onRender: countRenderSpy }),
|
|
107
|
-
React.createElement(ActionsOnlyConsumer, { onActionsReady: actionsReadySpy, onRender: actionsRenderSpy }),
|
|
108
|
-
React.createElement(HandlerConsumer, { onHandlerReady: handlerReadySpy })));
|
|
109
|
-
});
|
|
110
|
-
expect(countRenderSpy).toHaveBeenCalledTimes(1);
|
|
111
|
-
expect(countRenderSpy).toHaveBeenLastCalledWith(0);
|
|
112
|
-
expect(actionsRenderSpy).toHaveBeenCalledTimes(1);
|
|
113
|
-
expect(handlerReadySpy).toHaveBeenCalledWith(stateHandler);
|
|
114
|
-
const [[actions]] = actionsReadySpy.mock.calls;
|
|
115
|
-
act(() => {
|
|
116
|
-
actions.rename('Renamed');
|
|
117
|
-
});
|
|
118
|
-
expect(countRenderSpy).toHaveBeenCalledTimes(1);
|
|
119
|
-
expect(actionsRenderSpy).toHaveBeenCalledTimes(1);
|
|
120
|
-
act(() => {
|
|
121
|
-
actions.increment();
|
|
122
|
-
});
|
|
123
|
-
expect(countRenderSpy).toHaveBeenCalledTimes(2);
|
|
124
|
-
expect(countRenderSpy).toHaveBeenLastCalledWith(1);
|
|
125
|
-
expect(actionsRenderSpy).toHaveBeenCalledTimes(1);
|
|
126
|
-
});
|
|
127
|
-
it('should return the full snapshot when no selector is provided', () => {
|
|
128
|
-
const stateHandler = new TestStateHandler({
|
|
129
|
-
count: 2,
|
|
130
|
-
label: 'Counter',
|
|
131
|
-
});
|
|
132
|
-
const renderSpy = jest.fn();
|
|
133
|
-
const actionsReadySpy = jest.fn();
|
|
134
|
-
act(() => {
|
|
135
|
-
root.render(React.createElement(StateProvider, { instance: stateHandler },
|
|
136
|
-
React.createElement(FullStateConsumer, { onActionsReady: actionsReadySpy, onRender: renderSpy })));
|
|
137
|
-
});
|
|
138
|
-
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
139
|
-
expect(renderSpy).toHaveBeenLastCalledWith({ count: 2, label: 'Counter' });
|
|
140
|
-
const [[actions]] = actionsReadySpy.mock.calls;
|
|
141
|
-
act(() => {
|
|
142
|
-
actions.increment();
|
|
143
|
-
});
|
|
144
|
-
expect(renderSpy).toHaveBeenCalledTimes(2);
|
|
145
|
-
expect(renderSpy).toHaveBeenLastCalledWith({ count: 3, label: 'Counter' });
|
|
146
|
-
});
|
|
147
|
-
it('should follow a new instance when the provider instance changes', () => {
|
|
148
|
-
const firstHandler = new TestStateHandler({
|
|
149
|
-
count: 1,
|
|
150
|
-
label: 'First',
|
|
151
|
-
});
|
|
152
|
-
const secondHandler = new TestStateHandler({
|
|
153
|
-
count: 8,
|
|
154
|
-
label: 'Second',
|
|
155
|
-
});
|
|
156
|
-
const renderSpy = jest.fn();
|
|
157
|
-
act(() => {
|
|
158
|
-
root.render(React.createElement(StateProvider, { instance: firstHandler },
|
|
159
|
-
React.createElement(CountConsumer, { onRender: renderSpy })));
|
|
160
|
-
});
|
|
161
|
-
act(() => {
|
|
162
|
-
root.render(React.createElement(StateProvider, { instance: secondHandler },
|
|
163
|
-
React.createElement(CountConsumer, { onRender: renderSpy })));
|
|
164
|
-
});
|
|
165
|
-
expect(renderSpy).toHaveBeenCalledTimes(2);
|
|
166
|
-
expect(renderSpy).toHaveBeenNthCalledWith(1, 1);
|
|
167
|
-
expect(renderSpy).toHaveBeenNthCalledWith(2, 8);
|
|
168
|
-
});
|
|
169
|
-
it('should throw when provider hooks are used outside StateProvider', () => {
|
|
170
|
-
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
171
|
-
expect(() => {
|
|
172
|
-
act(() => {
|
|
173
|
-
root.render(React.createElement(MissingProviderConsumer, null));
|
|
174
|
-
});
|
|
175
|
-
}).toThrow('No StateProvider instance found in the current React tree.');
|
|
176
|
-
consoleErrorSpy.mockRestore();
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
//# sourceMappingURL=state-provider.spec.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"state-provider.spec.js","sourceRoot":"","sources":["../../../src/hooks/__tests__/state-provider.spec.tsx"],"names":[],"mappings":"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"}
|