@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.
Files changed (94) hide show
  1. package/.turbo/turbo-test.log +115 -15
  2. package/CHANGELOG.md +2 -0
  3. package/README.md +51 -7
  4. package/dist/config/status-quo-config.d.ts +22 -1
  5. package/dist/config/status-quo-config.js +46 -2
  6. package/dist/config/status-quo-config.js.map +1 -1
  7. package/dist/index.d.ts +12 -2
  8. package/dist/index.js +22 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/react/hooks/__tests__/state-provider.spec.js +2 -2
  11. package/dist/react/hooks/__tests__/state-provider.spec.js.map +1 -1
  12. package/dist/react/hooks/index.d.ts +6 -3
  13. package/dist/react/hooks/index.js +12 -3
  14. package/dist/react/hooks/index.js.map +1 -1
  15. package/dist/react/hooks/state-actions.d.ts +9 -1
  16. package/dist/react/hooks/state-actions.js +21 -2
  17. package/dist/react/hooks/state-actions.js.map +1 -1
  18. package/dist/react/hooks/state-factory.d.ts +7 -0
  19. package/dist/react/hooks/state-factory.js +23 -1
  20. package/dist/react/hooks/state-factory.js.map +1 -1
  21. package/dist/react/hooks/state-handler.d.ts +4 -0
  22. package/dist/react/hooks/state-handler.js +18 -1
  23. package/dist/react/hooks/state-handler.js.map +1 -1
  24. package/dist/react/hooks/state-provider.d.ts +18 -9
  25. package/dist/react/hooks/state-provider.js +25 -17
  26. package/dist/react/hooks/state-provider.js.map +1 -1
  27. package/dist/react/hooks/state-singleton.d.ts +8 -2
  28. package/dist/react/hooks/state-singleton.js +21 -3
  29. package/dist/react/hooks/state-singleton.js.map +1 -1
  30. package/dist/react/hooks/state-subscription-selector.d.ts +4 -0
  31. package/dist/react/hooks/state-subscription-selector.js +64 -8
  32. package/dist/react/hooks/state-subscription-selector.js.map +1 -1
  33. package/dist/react/hooks/state-subscription.d.ts +12 -0
  34. package/dist/react/hooks/state-subscription.js +49 -1
  35. package/dist/react/hooks/state-subscription.js.map +1 -1
  36. package/dist/react/index.d.ts +4 -1
  37. package/dist/react/index.js +5 -1
  38. package/dist/react/index.js.map +1 -1
  39. package/dist/store/__tests__/native-state-handler.spec.d.ts +1 -0
  40. package/dist/store/__tests__/native-state-handler.spec.js +210 -0
  41. package/dist/store/__tests__/native-state-handler.spec.js.map +1 -0
  42. package/dist/store/__tests__/observable-state-handler.spec.d.ts +7 -0
  43. package/dist/store/__tests__/observable-state-handler.spec.js.map +1 -1
  44. package/dist/store/base-state-handler.d.ts +42 -0
  45. package/dist/store/base-state-handler.js +73 -10
  46. package/dist/store/base-state-handler.js.map +1 -1
  47. package/dist/store/dev-tools.d.ts +42 -17
  48. package/dist/store/dev-tools.js +24 -8
  49. package/dist/store/dev-tools.js.map +1 -1
  50. package/dist/store/index.d.ts +7 -0
  51. package/dist/store/index.js +9 -0
  52. package/dist/store/index.js.map +1 -1
  53. package/dist/store/native-state-handler.d.ts +44 -0
  54. package/dist/store/native-state-handler.js +62 -0
  55. package/dist/store/native-state-handler.js.map +1 -0
  56. package/dist/store/observable-state-handler.d.ts +34 -0
  57. package/dist/store/observable-state-handler.js +45 -1
  58. package/dist/store/observable-state-handler.js.map +1 -1
  59. package/dist/store/signal-state-handler.d.ts +26 -0
  60. package/dist/store/signal-state-handler.js +35 -0
  61. package/dist/store/signal-state-handler.js.map +1 -1
  62. package/dist/store/state-singleton.d.ts +14 -0
  63. package/dist/store/state-singleton.js +20 -1
  64. package/dist/store/state-singleton.js.map +1 -1
  65. package/dist/types/types.d.ts +9 -0
  66. package/dist/types/types.js +3 -0
  67. package/dist/types/types.js.map +1 -1
  68. package/dist/utils/selector-cache.d.ts +17 -0
  69. package/dist/utils/selector-cache.js +28 -1
  70. package/dist/utils/selector-cache.js.map +1 -1
  71. package/package.json +12 -1
  72. package/src/config/status-quo-config.ts +64 -1
  73. package/src/index.ts +29 -0
  74. package/src/react/hooks/__tests__/state-provider.spec.tsx +2 -2
  75. package/src/react/hooks/index.ts +13 -8
  76. package/src/react/hooks/state-actions.tsx +23 -2
  77. package/src/react/hooks/state-factory.tsx +34 -0
  78. package/src/react/hooks/state-handler.tsx +15 -0
  79. package/src/react/hooks/state-provider.tsx +36 -40
  80. package/src/react/hooks/state-singleton.tsx +37 -7
  81. package/src/react/hooks/state-subscription-selector.tsx +85 -7
  82. package/src/react/hooks/state-subscription.tsx +75 -0
  83. package/src/react/index.ts +16 -1
  84. package/src/store/__tests__/native-state-handler.spec.ts +291 -0
  85. package/src/store/__tests__/observable-state-handler.spec.ts +8 -0
  86. package/src/store/base-state-handler.ts +89 -12
  87. package/src/store/dev-tools.ts +72 -27
  88. package/src/store/index.ts +16 -0
  89. package/src/store/native-state-handler.ts +98 -0
  90. package/src/store/observable-state-handler.ts +57 -0
  91. package/src/store/signal-state-handler.ts +47 -1
  92. package/src/store/state-singleton.ts +30 -0
  93. package/src/types/types.ts +16 -0
  94. 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);
@@ -1,44 +1,89 @@
1
- declare global {
2
- interface Window {
3
- __REDUX_DEVTOOLS_EXTENSION__?: {
4
- connect: (opts: Record<string, unknown>) => DevTools;
5
- };
6
- }
7
- }
1
+ /**
2
+ * Redux DevTools extension types and constants.
3
+ */
8
4
 
9
- export type MessagePayload = {
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
- actionId: number;
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
- id: string;
17
- source: '@devtools-extension';
18
- };
22
+ }
19
23
 
20
- export type DevTools = {
21
- init: (state: unknown) => void;
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: (cb: (message: MessagePayload) => void) => void;
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
- export function withDevTools<S>(initialState: S, options = {}): DevTools | null {
27
- if (typeof window === 'undefined') {
28
- return null;
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 (!window.__REDUX_DEVTOOLS_EXTENSION__) {
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 = window.__REDUX_DEVTOOLS_EXTENSION__.connect(options);
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 devTools;
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
  }
@@ -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';