@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
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { resetStatusQuoForTests, setupStatusQuo } from '../../config/status-quo-config.js';
|
|
2
|
+
import { NativeStateHandler } from '../native-state-handler.js';
|
|
3
|
+
import { makeStateSingleton } from '../state-singleton.js';
|
|
4
|
+
|
|
5
|
+
import type { DistinctOptions } from '../../config/status-quo-config.js';
|
|
6
|
+
|
|
7
|
+
type TestState = { test: string; test2: string };
|
|
8
|
+
type TestActions = { testAction: () => void };
|
|
9
|
+
type TestNativeHandlerOptions = {
|
|
10
|
+
withDevTools?: boolean;
|
|
11
|
+
distinct?: DistinctOptions<TestState>;
|
|
12
|
+
useDistinctUntilChanged?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
class TestNativeStateHandler extends NativeStateHandler<TestState, TestActions> {
|
|
16
|
+
constructor({ withDevTools, distinct, useDistinctUntilChanged }: TestNativeHandlerOptions = {}) {
|
|
17
|
+
super({
|
|
18
|
+
initialState: {
|
|
19
|
+
test: 'testValue',
|
|
20
|
+
test2: 'testValue2',
|
|
21
|
+
},
|
|
22
|
+
options: {
|
|
23
|
+
...(withDevTools && {
|
|
24
|
+
devTools: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
namespace: 'TestNativeStateHandler',
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
...(distinct && {
|
|
30
|
+
distinct,
|
|
31
|
+
}),
|
|
32
|
+
...(typeof useDistinctUntilChanged === 'boolean' && {
|
|
33
|
+
useDistinctUntilChanged,
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getActions(): TestActions {
|
|
40
|
+
return {
|
|
41
|
+
testAction: () => {
|
|
42
|
+
this.setState({ test: 'newValue' });
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type CounterState = { count: number };
|
|
49
|
+
type CounterActions = { increase: () => void };
|
|
50
|
+
type CounterBucketSelection = { bucket: number };
|
|
51
|
+
type CounterBucketState = { bucket: number };
|
|
52
|
+
|
|
53
|
+
class CounterNativeStateHandler extends NativeStateHandler<CounterState, CounterActions> {
|
|
54
|
+
constructor(initialCount = 0) {
|
|
55
|
+
super({
|
|
56
|
+
initialState: {
|
|
57
|
+
count: initialCount,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getActions(): CounterActions {
|
|
63
|
+
return {
|
|
64
|
+
increase: () => {
|
|
65
|
+
this.setState({ count: this.getState().count + 1 }, 'increase');
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class CounterNativeBridgeStateHandler extends NativeStateHandler<
|
|
72
|
+
CounterState,
|
|
73
|
+
{ noop: () => void }
|
|
74
|
+
> {
|
|
75
|
+
constructor(
|
|
76
|
+
counterSingleton: ReturnType<typeof makeStateSingleton<CounterState, CounterActions>>,
|
|
77
|
+
onCounterSync: (counterState: CounterState) => void
|
|
78
|
+
) {
|
|
79
|
+
super({
|
|
80
|
+
initialState: {
|
|
81
|
+
count: 0,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const counterStateHandler = counterSingleton.getInstance();
|
|
86
|
+
|
|
87
|
+
this.bindSubscribable<CounterState, CounterState>(
|
|
88
|
+
counterStateHandler,
|
|
89
|
+
(nextCounterState) => {
|
|
90
|
+
onCounterSync(nextCounterState);
|
|
91
|
+
this.setState({ count: nextCounterState.count }, 'sync-counter');
|
|
92
|
+
},
|
|
93
|
+
(counterState) => counterState
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getActions(): { noop: () => void } {
|
|
98
|
+
return {
|
|
99
|
+
noop: () => undefined,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class CounterNativeBucketBridgeStateHandler extends NativeStateHandler<
|
|
105
|
+
CounterBucketState,
|
|
106
|
+
{ noop: () => void }
|
|
107
|
+
> {
|
|
108
|
+
constructor(
|
|
109
|
+
counterSingleton: ReturnType<typeof makeStateSingleton<CounterState, CounterActions>>,
|
|
110
|
+
onCounterSync: (selection: CounterBucketSelection) => void
|
|
111
|
+
) {
|
|
112
|
+
super({
|
|
113
|
+
initialState: {
|
|
114
|
+
bucket: -1,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const counterStateHandler = counterSingleton.getInstance();
|
|
119
|
+
|
|
120
|
+
this.bindSubscribable<CounterState, CounterBucketSelection>(
|
|
121
|
+
counterStateHandler,
|
|
122
|
+
(nextSelection) => {
|
|
123
|
+
onCounterSync(nextSelection);
|
|
124
|
+
this.setState({ bucket: nextSelection.bucket }, 'sync-counter-bucket');
|
|
125
|
+
},
|
|
126
|
+
(counterState) => ({
|
|
127
|
+
bucket: Math.floor(counterState.count / 2),
|
|
128
|
+
}),
|
|
129
|
+
(current, next) => current.bucket === next.bucket
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getActions(): { noop: () => void } {
|
|
134
|
+
return {
|
|
135
|
+
noop: () => undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
describe('Native State Handler', () => {
|
|
141
|
+
let stateHandler: TestNativeStateHandler;
|
|
142
|
+
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
resetStatusQuoForTests();
|
|
145
|
+
stateHandler = new TestNativeStateHandler();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
afterEach(() => {
|
|
149
|
+
resetStatusQuoForTests();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should provide initial state', () => {
|
|
153
|
+
expect(stateHandler.getInitialState()).toStrictEqual({
|
|
154
|
+
test: 'testValue',
|
|
155
|
+
test2: 'testValue2',
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should provide current state', () => {
|
|
160
|
+
expect(stateHandler.getState()).toStrictEqual({
|
|
161
|
+
test: 'testValue',
|
|
162
|
+
test2: 'testValue2',
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should support state changing via setter and merge state object on first level', () => {
|
|
167
|
+
const expected = {
|
|
168
|
+
test: 'change',
|
|
169
|
+
test2: 'testValue2',
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
stateHandler.setState(expected);
|
|
173
|
+
|
|
174
|
+
expect(stateHandler.getState()).toStrictEqual(expected);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should support additional subscriptions handling', () => {
|
|
178
|
+
const spy = jest.fn();
|
|
179
|
+
const subscription = { unsubscribe: spy };
|
|
180
|
+
|
|
181
|
+
stateHandler.subscriptions = [subscription];
|
|
182
|
+
|
|
183
|
+
stateHandler.destroy();
|
|
184
|
+
|
|
185
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should call subscriber when state has changed and also on initial subscribe', () => {
|
|
189
|
+
const spy = jest.fn();
|
|
190
|
+
const unsubscribe = stateHandler.subscribe(spy);
|
|
191
|
+
|
|
192
|
+
stateHandler.setState({
|
|
193
|
+
test: 'test',
|
|
194
|
+
});
|
|
195
|
+
stateHandler.setState({
|
|
196
|
+
test: 'test2',
|
|
197
|
+
});
|
|
198
|
+
stateHandler.setState({
|
|
199
|
+
test: 'test2',
|
|
200
|
+
});
|
|
201
|
+
stateHandler.setState({
|
|
202
|
+
test: 'test2',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
unsubscribe();
|
|
206
|
+
|
|
207
|
+
expect(spy).toHaveBeenCalledTimes(3); // initial + change + change
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should respect global distinct setup when disabled', () => {
|
|
211
|
+
setupStatusQuo({
|
|
212
|
+
distinct: {
|
|
213
|
+
enabled: false,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const handler = new TestNativeStateHandler();
|
|
218
|
+
const spy = jest.fn();
|
|
219
|
+
const unsubscribe = handler.subscribe(spy);
|
|
220
|
+
|
|
221
|
+
handler.setState({ test: 'same' });
|
|
222
|
+
handler.setState({ test: 'same' });
|
|
223
|
+
|
|
224
|
+
unsubscribe();
|
|
225
|
+
|
|
226
|
+
expect(spy).toHaveBeenCalledTimes(3); // initial + change + change
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should respect global custom distinct comparator from setupStatusQuo', () => {
|
|
230
|
+
setupStatusQuo({
|
|
231
|
+
distinct: {
|
|
232
|
+
comparator: (previous: TestState, next: TestState) => {
|
|
233
|
+
return previous.test === next.test;
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const handler = new TestNativeStateHandler();
|
|
239
|
+
const spy = jest.fn();
|
|
240
|
+
const unsubscribe = handler.subscribe(spy);
|
|
241
|
+
|
|
242
|
+
handler.setState({ test2: 'newValue2' }); // test remains same -> skipped
|
|
243
|
+
handler.setState({ test: 'newValue' }); // test changed -> notified
|
|
244
|
+
|
|
245
|
+
unsubscribe();
|
|
246
|
+
|
|
247
|
+
expect(spy).toHaveBeenCalledTimes(2); // initial + one change
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should notify another state handler for each singleton counter update', () => {
|
|
251
|
+
const counterSingleton = makeStateSingleton(() => new CounterNativeStateHandler(0), {
|
|
252
|
+
destroyOnNoConsumers: false,
|
|
253
|
+
});
|
|
254
|
+
const syncSpy = jest.fn();
|
|
255
|
+
const bridgeStateHandler = new CounterNativeBridgeStateHandler(counterSingleton, syncSpy);
|
|
256
|
+
const counterStateHandler = counterSingleton.getInstance();
|
|
257
|
+
|
|
258
|
+
counterStateHandler.getActions().increase();
|
|
259
|
+
counterStateHandler.getActions().increase();
|
|
260
|
+
|
|
261
|
+
expect(syncSpy).toHaveBeenCalledTimes(3);
|
|
262
|
+
expect(syncSpy).toHaveBeenNthCalledWith(1, { count: 0 });
|
|
263
|
+
expect(syncSpy).toHaveBeenNthCalledWith(2, { count: 1 });
|
|
264
|
+
expect(syncSpy).toHaveBeenNthCalledWith(3, { count: 2 });
|
|
265
|
+
expect(bridgeStateHandler.getState()).toStrictEqual({ count: 2 });
|
|
266
|
+
|
|
267
|
+
bridgeStateHandler.destroy();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should support selector + equality filtering for bindSubscribable', () => {
|
|
271
|
+
const counterSingleton = makeStateSingleton(() => new CounterNativeStateHandler(0), {
|
|
272
|
+
destroyOnNoConsumers: false,
|
|
273
|
+
});
|
|
274
|
+
const syncSpy = jest.fn();
|
|
275
|
+
const bridgeStateHandler = new CounterNativeBucketBridgeStateHandler(counterSingleton, syncSpy);
|
|
276
|
+
const counterStateHandler = counterSingleton.getInstance();
|
|
277
|
+
|
|
278
|
+
counterStateHandler.getActions().increase(); // count 1 -> bucket 0 (no change)
|
|
279
|
+
counterStateHandler.getActions().increase(); // count 2 -> bucket 1
|
|
280
|
+
counterStateHandler.getActions().increase(); // count 3 -> bucket 1 (no change)
|
|
281
|
+
counterStateHandler.getActions().increase(); // count 4 -> bucket 2
|
|
282
|
+
|
|
283
|
+
expect(syncSpy).toHaveBeenCalledTimes(3);
|
|
284
|
+
expect(syncSpy).toHaveBeenNthCalledWith(1, { bucket: 0 });
|
|
285
|
+
expect(syncSpy).toHaveBeenNthCalledWith(2, { bucket: 1 });
|
|
286
|
+
expect(syncSpy).toHaveBeenNthCalledWith(3, { bucket: 2 });
|
|
287
|
+
expect(bridgeStateHandler.getState()).toStrictEqual({ bucket: 2 });
|
|
288
|
+
|
|
289
|
+
bridgeStateHandler.destroy();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -5,6 +5,14 @@ import { ObservableStateHandler } from '../observable-state-handler.js';
|
|
|
5
5
|
|
|
6
6
|
import type { DevToolsOptions, DistinctOptions } from '../../config/status-quo-config.js';
|
|
7
7
|
|
|
8
|
+
declare global {
|
|
9
|
+
interface Window {
|
|
10
|
+
__REDUX_DEVTOOLS_EXTENSION__?: {
|
|
11
|
+
connect: (options: any) => any;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
type TestState = { test: string; test2: string };
|
|
9
17
|
type TestActions = { testAction: () => void };
|
|
10
18
|
type TestObservableHandlerOptions = {
|
|
@@ -1,50 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import internal configuration and utility functions.
|
|
3
|
+
*/
|
|
1
4
|
import { resolveDevToolsOptions } from '../config/status-quo-config.js';
|
|
2
5
|
import { createSelectorCache, selectWithCache } from '../utils/selector-cache.js';
|
|
3
6
|
import { withDevTools } from './dev-tools.js';
|
|
4
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Import necessary types for state management and Redux DevTools integration.
|
|
10
|
+
*/
|
|
5
11
|
import type { StateSubscriptionHandler } from '../types/types.js';
|
|
6
12
|
import type { EqualityFn, Selector } from '../utils/selector-cache.js';
|
|
7
13
|
import type { DevTools, MessagePayload } from './dev-tools.js';
|
|
8
14
|
import type { DevToolsOptions } from '../config/status-quo-config.js';
|
|
9
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Interface for objects that can be subscribed to.
|
|
18
|
+
*/
|
|
10
19
|
type Subscribable<T> = {
|
|
20
|
+
// Method to subscribe to changes.
|
|
11
21
|
subscribe: (listener: (value: T) => void) => () => void;
|
|
22
|
+
// Optional method to get the current snapshot of the state.
|
|
12
23
|
getSnapshot?: () => T;
|
|
13
24
|
};
|
|
14
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Configuration for Redux DevTools features enabled in this handler.
|
|
28
|
+
*/
|
|
15
29
|
const devToolsFeatures = {
|
|
16
|
-
pause: true,
|
|
17
|
-
lock: true,
|
|
18
|
-
persist: false,
|
|
19
|
-
export: true,
|
|
20
|
-
import: 'custom',
|
|
21
|
-
jump: true,
|
|
22
|
-
skip: true,
|
|
23
|
-
reorder: true,
|
|
24
|
-
dispatch: false,
|
|
25
|
-
test: false,
|
|
26
|
-
};
|
|
27
|
-
|
|
30
|
+
pause: true, // Allow pausing the recording of actions.
|
|
31
|
+
lock: true, // Allow locking the state.
|
|
32
|
+
persist: false, // Do not persist state across reloads by default.
|
|
33
|
+
export: true, // Allow exporting the state/actions.
|
|
34
|
+
import: 'custom', // Use custom import logic.
|
|
35
|
+
jump: true, // Allow jumping to specific states.
|
|
36
|
+
skip: true, // Allow skipping specific actions.
|
|
37
|
+
reorder: true, // Allow reordering actions.
|
|
38
|
+
dispatch: false, // Do not allow dispatching actions from DevTools.
|
|
39
|
+
test: false, // Do not generate tests.
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Abstract base class for all state handlers in the system.
|
|
44
|
+
* Implements core logic for initialization, DevTools integration, and subscriptions.
|
|
45
|
+
*/
|
|
28
46
|
export abstract class BaseStateHandler<S, A> implements StateSubscriptionHandler<S, A> {
|
|
47
|
+
// Stores the initial state passed during construction.
|
|
29
48
|
protected readonly initialState: S;
|
|
49
|
+
// Holds the Redux DevTools instance if enabled.
|
|
30
50
|
protected devTools: DevTools | null = null;
|
|
31
51
|
|
|
52
|
+
// Keeps track of active subscriptions to allow for cleanup.
|
|
32
53
|
subscriptions: Array<{ unsubscribe: () => void }> = [];
|
|
33
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Initializes the handler with the given initial state.
|
|
57
|
+
*/
|
|
34
58
|
protected constructor(initialState: S) {
|
|
35
59
|
this.initialState = initialState;
|
|
36
60
|
}
|
|
37
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Sets up Redux DevTools integration based on the provided options.
|
|
64
|
+
*/
|
|
38
65
|
protected initDevTools(devToolsOptions?: DevToolsOptions) {
|
|
66
|
+
// Resolve the final DevTools configuration.
|
|
39
67
|
const resolvedOptions = resolveDevToolsOptions(devToolsOptions);
|
|
40
68
|
|
|
69
|
+
// If DevTools is disabled, stop here.
|
|
41
70
|
if (!resolvedOptions.enabled) {
|
|
42
71
|
this.devTools = null;
|
|
43
72
|
return;
|
|
44
73
|
}
|
|
45
74
|
|
|
75
|
+
// Determine the namespace for the DevTools instance.
|
|
46
76
|
const namespace = devToolsOptions?.namespace ?? this.getDevToolsNamespace();
|
|
47
77
|
|
|
78
|
+
// Connect to the Redux DevTools extension.
|
|
48
79
|
this.devTools = withDevTools(this.initialState, {
|
|
49
80
|
name: namespace,
|
|
50
81
|
instanceId: namespace.toLowerCase().replaceAll(' ', '-'),
|
|
@@ -52,38 +83,67 @@ export abstract class BaseStateHandler<S, A> implements StateSubscriptionHandler
|
|
|
52
83
|
features: devToolsFeatures,
|
|
53
84
|
});
|
|
54
85
|
|
|
86
|
+
// Subscribe to events coming from the DevTools extension (e.g., time travel).
|
|
55
87
|
this.devTools?.subscribe(this.handleDevToolsEvents);
|
|
56
88
|
}
|
|
57
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Returns the initial state of the handler.
|
|
92
|
+
*/
|
|
58
93
|
getInitialState() {
|
|
59
94
|
return this.initialState;
|
|
60
95
|
}
|
|
61
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Returns the current state.
|
|
99
|
+
*/
|
|
62
100
|
getState() {
|
|
63
101
|
return this.getStateValue();
|
|
64
102
|
}
|
|
65
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Alias for getState, often used by React's useSyncExternalStore.
|
|
106
|
+
*/
|
|
66
107
|
getSnapshot() {
|
|
67
108
|
return this.getState();
|
|
68
109
|
}
|
|
69
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Updates the state by merging the partial new state into the current one.
|
|
113
|
+
* Also sends the update to DevTools if enabled.
|
|
114
|
+
*/
|
|
70
115
|
setState(newState: Partial<S>, actionName = 'change') {
|
|
116
|
+
// Merge current state with the new partial state.
|
|
71
117
|
const nextState = { ...this.getState(), ...newState };
|
|
118
|
+
// Update the underlying state value (implemented by subclasses).
|
|
72
119
|
this.setStateValue(nextState);
|
|
120
|
+
// Notify DevTools of the state change.
|
|
73
121
|
this.devTools?.send(actionName, nextState);
|
|
74
122
|
}
|
|
75
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Cleans up all active subscriptions when the handler is destroyed.
|
|
126
|
+
*/
|
|
76
127
|
destroy(): void {
|
|
128
|
+
// Execute the unsubscribe function for each tracked subscription.
|
|
77
129
|
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
|
|
78
130
|
}
|
|
79
131
|
|
|
132
|
+
// Abstract methods to be implemented by concrete handlers for specific state engines (RxJS, Signals, etc.).
|
|
80
133
|
protected abstract getStateValue(): S;
|
|
81
134
|
protected abstract setStateValue(nextState: S): void;
|
|
82
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Returns a default namespace for DevTools based on the class name.
|
|
138
|
+
*/
|
|
83
139
|
protected getDevToolsNamespace() {
|
|
84
140
|
return this.constructor.name || 'Store';
|
|
85
141
|
}
|
|
86
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Binds an external subscribable source to this handler's state.
|
|
145
|
+
* Useful for bridging different state systems.
|
|
146
|
+
*/
|
|
87
147
|
protected bindSubscribable<T, Sel>(
|
|
88
148
|
service: Subscribable<T>,
|
|
89
149
|
onChange: (value: Sel) => void,
|
|
@@ -97,12 +157,17 @@ export abstract class BaseStateHandler<S, A> implements StateSubscriptionHandler
|
|
|
97
157
|
selector?: Selector<T, Sel>,
|
|
98
158
|
isEqual: EqualityFn<Sel> = Object.is
|
|
99
159
|
) {
|
|
160
|
+
// Default to identity selector if none is provided.
|
|
100
161
|
const selectorFn = (selector ?? ((value: T) => value as unknown as Sel));
|
|
162
|
+
// Create a cache for selector results to avoid unnecessary updates.
|
|
101
163
|
const selectorCache = createSelectorCache<Sel>();
|
|
164
|
+
// Flag to track if we received an initial value synchronously during subscription.
|
|
102
165
|
let receivedSyncValue = false;
|
|
103
166
|
|
|
167
|
+
// Internal function to handle value changes from the external source.
|
|
104
168
|
const notifySelectedValue = (value: T) => {
|
|
105
169
|
receivedSyncValue = true;
|
|
170
|
+
// Extract the selected value and check if it has actually changed.
|
|
106
171
|
const { value: nextSelection, hasChanged } = selectWithCache(
|
|
107
172
|
selectorCache,
|
|
108
173
|
value,
|
|
@@ -110,6 +175,7 @@ export abstract class BaseStateHandler<S, A> implements StateSubscriptionHandler
|
|
|
110
175
|
isEqual
|
|
111
176
|
);
|
|
112
177
|
|
|
178
|
+
// Only trigger the callback if the selected value changed.
|
|
113
179
|
if (!hasChanged) {
|
|
114
180
|
return;
|
|
115
181
|
}
|
|
@@ -117,32 +183,43 @@ export abstract class BaseStateHandler<S, A> implements StateSubscriptionHandler
|
|
|
117
183
|
onChange(nextSelection);
|
|
118
184
|
};
|
|
119
185
|
|
|
186
|
+
// Subscribe to the external source.
|
|
120
187
|
const unsubscribe = service.subscribe(notifySelectedValue);
|
|
188
|
+
// Track the subscription for later cleanup.
|
|
121
189
|
this.subscriptions = [...(this.subscriptions ?? []), { unsubscribe }];
|
|
122
190
|
|
|
191
|
+
// If the source has a getSnapshot method and we haven't received a value yet, pull it manually.
|
|
123
192
|
if (service.getSnapshot && !receivedSyncValue) notifySelectedValue(service.getSnapshot());
|
|
124
193
|
}
|
|
125
194
|
|
|
195
|
+
// Abstract methods that must be defined by all concrete state handlers.
|
|
126
196
|
abstract subscribe(listener: () => void): () => void;
|
|
127
197
|
abstract subscribe(listener: (value: S) => void): () => void;
|
|
128
198
|
abstract getActions(): A;
|
|
129
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Handles messages dispatched from the Redux DevTools extension.
|
|
202
|
+
*/
|
|
130
203
|
private handleDevToolsEvents = (message: MessagePayload) => {
|
|
204
|
+
// We only care about DISPATCH type messages (e.g., reset, jump).
|
|
131
205
|
if (message.type !== 'DISPATCH') {
|
|
132
206
|
return;
|
|
133
207
|
}
|
|
134
208
|
|
|
135
209
|
switch (message.payload.type) {
|
|
210
|
+
// Revert state to the initial state.
|
|
136
211
|
case 'RESET':
|
|
137
212
|
this.setStateValue(this.getInitialState());
|
|
138
213
|
this.devTools?.init(this.getInitialState());
|
|
139
214
|
break;
|
|
140
215
|
|
|
216
|
+
// Commit the current state in DevTools.
|
|
141
217
|
case 'COMMIT':
|
|
142
218
|
this.setStateValue(this.getState());
|
|
143
219
|
this.devTools?.init(this.getState());
|
|
144
220
|
break;
|
|
145
221
|
|
|
222
|
+
// Handle time travel actions.
|
|
146
223
|
case 'JUMP_TO_STATE':
|
|
147
224
|
case 'JUMP_TO_ACTION':
|
|
148
225
|
this.setStateValue(JSON.parse(message.state) as S);
|
package/src/store/dev-tools.ts
CHANGED
|
@@ -1,44 +1,89 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
connect: (opts: Record<string, unknown>) => DevTools;
|
|
5
|
-
};
|
|
6
|
-
}
|
|
7
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Redux DevTools extension types and constants.
|
|
3
|
+
*/
|
|
8
4
|
|
|
9
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Interface for messages dispatched from the Redux DevTools extension.
|
|
7
|
+
*/
|
|
8
|
+
export interface MessagePayload {
|
|
9
|
+
// Type of the message (e.g., 'DISPATCH').
|
|
10
10
|
type: string;
|
|
11
|
+
// Details about the message, including action type and state.
|
|
11
12
|
payload: {
|
|
13
|
+
// Specific type of the dispatch action (e.g., 'RESET', 'JUMP_TO_STATE').
|
|
12
14
|
type: string;
|
|
13
|
-
|
|
15
|
+
// Index of the state in the history.
|
|
16
|
+
stateIndex?: number;
|
|
17
|
+
// Unique ID for the action.
|
|
18
|
+
id?: string;
|
|
14
19
|
};
|
|
20
|
+
// Current state of the store in the DevTools as a JSON string.
|
|
15
21
|
state: string;
|
|
16
|
-
|
|
17
|
-
source: '@devtools-extension';
|
|
18
|
-
};
|
|
22
|
+
}
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Interface for the Redux DevTools instance returned by the extension.
|
|
26
|
+
*/
|
|
27
|
+
export interface DevTools {
|
|
28
|
+
// Method to send a new state update to the DevTools extension.
|
|
22
29
|
send: (action: string, state: unknown) => void;
|
|
23
|
-
subscribe
|
|
24
|
-
|
|
30
|
+
// Method to subscribe to changes coming from the DevTools extension.
|
|
31
|
+
subscribe: (listener: (message: MessagePayload) => void) => () => void;
|
|
32
|
+
// Method to initialize the state in the DevTools extension.
|
|
33
|
+
init: (state: unknown) => void;
|
|
34
|
+
}
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Interface for Redux DevTools connection options.
|
|
38
|
+
*/
|
|
39
|
+
export interface DevToolsConnectionOptions {
|
|
40
|
+
// Name to be displayed for the store in the DevTools window.
|
|
41
|
+
name?: string;
|
|
42
|
+
// Unique ID for the DevTools instance.
|
|
43
|
+
instanceId?: string;
|
|
44
|
+
// Action creators that will be available in the DevTools UI.
|
|
45
|
+
actionCreators?: unknown;
|
|
46
|
+
// Configuration for specific DevTools features to enable or disable.
|
|
47
|
+
features?: {
|
|
48
|
+
pause?: boolean;
|
|
49
|
+
lock?: boolean;
|
|
50
|
+
persist?: boolean;
|
|
51
|
+
export?: boolean | 'custom';
|
|
52
|
+
import?: boolean | 'custom';
|
|
53
|
+
jump?: boolean;
|
|
54
|
+
skip?: boolean;
|
|
55
|
+
reorder?: boolean;
|
|
56
|
+
dispatch?: boolean;
|
|
57
|
+
test?: boolean;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Connects the state handler to the Redux DevTools browser extension.
|
|
63
|
+
* Returns a DevTools object to communicate with the extension.
|
|
64
|
+
*/
|
|
65
|
+
export function withDevTools(initialState: unknown, options: DevToolsConnectionOptions): DevTools | null {
|
|
66
|
+
// Check if the Redux DevTools extension is available in the browser.
|
|
67
|
+
const extension = (globalThis as any)?.__REDUX_DEVTOOLS_EXTENSION__;
|
|
30
68
|
|
|
31
|
-
|
|
32
|
-
if (!
|
|
33
|
-
console.error('Status Quo :: Devtools Extension is not installed!');
|
|
69
|
+
// If the extension is not found, we cannot connect.
|
|
70
|
+
if (!extension) {
|
|
34
71
|
return null;
|
|
35
72
|
}
|
|
36
73
|
|
|
37
|
-
|
|
38
|
-
const devTools =
|
|
74
|
+
// Connect to the extension and get an instance.
|
|
75
|
+
const devTools = extension.connect(options);
|
|
39
76
|
|
|
77
|
+
// Initialize the DevTools instance with the initial state.
|
|
40
78
|
devTools.init(initialState);
|
|
41
79
|
|
|
42
|
-
|
|
43
|
-
return
|
|
80
|
+
// Return an interface to interact with the extension.
|
|
81
|
+
return {
|
|
82
|
+
// Send a new action and state to the DevTools.
|
|
83
|
+
send: (action: string, state: unknown) => devTools.send(action, state),
|
|
84
|
+
// Subscribe to events coming from the DevTools.
|
|
85
|
+
subscribe: (listener: (message: MessagePayload) => void) => devTools.subscribe(listener),
|
|
86
|
+
// Re-initialize the state in the extension.
|
|
87
|
+
init: (state: unknown) => devTools.init(state),
|
|
88
|
+
};
|
|
44
89
|
}
|
package/src/store/index.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export core state handler classes and factory functions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Export the abstract base class for all state handlers.
|
|
1
6
|
export { BaseStateHandler } from './base-state-handler.js';
|
|
7
|
+
// Export the lightweight state handler using plain JavaScript.
|
|
8
|
+
export { NativeStateHandler } from './native-state-handler.js';
|
|
9
|
+
// Export the state handler powered by RxJS BehaviorSubjects.
|
|
2
10
|
export { ObservableStateHandler } from './observable-state-handler.js';
|
|
11
|
+
// Export the state handler powered by Preact Signals.
|
|
3
12
|
export { SignalStateHandler } from './signal-state-handler.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Export singleton related types and factory function.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Export the StateSingleton and StateSingletonOptions interfaces for external typing.
|
|
4
19
|
export type { StateSingleton, StateSingletonOptions } from './state-singleton.js';
|
|
20
|
+
// Export the factory function to create singleton state handler instances.
|
|
5
21
|
export { makeStateSingleton } from './state-singleton.js';
|