@veams/status-quo 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +252 -18
- package/dist/config/status-quo-config.d.ts +21 -0
- package/dist/config/status-quo-config.js +48 -0
- package/dist/config/status-quo-config.js.map +1 -0
- package/dist/hooks/__tests__/state-selector.spec.d.ts +4 -0
- package/dist/hooks/__tests__/state-selector.spec.js +384 -0
- package/dist/hooks/__tests__/state-selector.spec.js.map +1 -0
- package/dist/hooks/__tests__/state-singleton.spec.d.ts +4 -0
- package/dist/hooks/__tests__/state-singleton.spec.js +97 -0
- package/dist/hooks/__tests__/state-singleton.spec.js.map +1 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/state-actions.d.ts +2 -0
- package/dist/hooks/state-actions.js +5 -0
- package/dist/hooks/state-actions.js.map +1 -0
- package/dist/hooks/state-factory.d.ts +5 -0
- package/dist/hooks/state-factory.js +10 -6
- package/dist/hooks/state-factory.js.map +1 -1
- package/dist/hooks/state-handler.d.ts +2 -0
- package/dist/hooks/state-handler.js +9 -0
- package/dist/hooks/state-handler.js.map +1 -0
- package/dist/hooks/state-singleton.d.ts +4 -0
- package/dist/hooks/state-singleton.js +3 -5
- package/dist/hooks/state-singleton.js.map +1 -1
- package/dist/hooks/state-subscription-selector.d.ts +5 -0
- package/dist/hooks/state-subscription-selector.js +29 -0
- package/dist/hooks/state-subscription-selector.js.map +1 -0
- package/dist/hooks/state-subscription.d.ts +8 -1
- package/dist/hooks/state-subscription.js +49 -10
- package/dist/hooks/state-subscription.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/store/__tests__/observable-state-handler.spec.js +68 -5
- package/dist/store/__tests__/observable-state-handler.spec.js.map +1 -1
- package/dist/store/__tests__/signal-state-handler.spec.js +64 -5
- package/dist/store/__tests__/signal-state-handler.spec.js.map +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/observable-state-handler.d.ts +7 -2
- package/dist/store/observable-state-handler.js +17 -22
- package/dist/store/observable-state-handler.js.map +1 -1
- package/dist/store/signal-state-handler.d.ts +3 -1
- package/dist/store/signal-state-handler.js +5 -14
- package/dist/store/signal-state-handler.js.map +1 -1
- package/dist/store/state-singleton.d.ts +4 -1
- package/dist/store/state-singleton.js +11 -2
- package/dist/store/state-singleton.js.map +1 -1
- package/docs/assets/index-BBmpszOW.css +1 -0
- package/docs/assets/index-Cf8El_RO.js +194 -0
- package/docs/assets/statusquo-logo-8GVRbxpc.png +0 -0
- package/docs/index.html +13 -0
- package/package.json +1 -1
- package/playground/src/App.tsx +269 -48
- package/playground/src/styles.css +123 -0
- package/src/config/status-quo-config.ts +76 -0
- package/src/hooks/__tests__/state-selector.spec.tsx +607 -0
- package/src/hooks/__tests__/state-singleton.spec.tsx +151 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/state-actions.tsx +7 -0
- package/src/hooks/state-factory.tsx +32 -6
- package/src/hooks/state-handler.tsx +16 -0
- package/src/hooks/state-singleton.tsx +17 -7
- package/src/hooks/state-subscription-selector.tsx +70 -0
- package/src/hooks/state-subscription.tsx +98 -21
- package/src/index.ts +23 -4
- package/src/store/__tests__/observable-state-handler.spec.ts +98 -11
- package/src/store/__tests__/signal-state-handler.spec.ts +92 -11
- package/src/store/index.ts +1 -1
- package/src/store/observable-state-handler.ts +25 -27
- package/src/store/signal-state-handler.ts +7 -16
- package/src/store/state-singleton.ts +21 -3
|
@@ -1,27 +1,40 @@
|
|
|
1
|
+
import { resetStatusQuoForTests, setupStatusQuo } from '../../config/status-quo-config.js';
|
|
1
2
|
import { SignalStateHandler } from '../signal-state-handler.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import type { DistinctOptions } from '../../config/status-quo-config.js';
|
|
4
|
+
|
|
5
|
+
type TestState = { test: string; test2: string };
|
|
6
|
+
type TestActions = { testAction: () => void };
|
|
7
|
+
type TestSignalHandlerOptions = {
|
|
8
|
+
withDevTools?: boolean;
|
|
9
|
+
distinct?: DistinctOptions<TestState>;
|
|
10
|
+
useDistinctUntilChanged?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
class TestSignalStateHandler extends SignalStateHandler<TestState, TestActions> {
|
|
14
|
+
constructor({ withDevTools, distinct, useDistinctUntilChanged }: TestSignalHandlerOptions = {}) {
|
|
8
15
|
super({
|
|
9
16
|
initialState: {
|
|
10
17
|
test: 'testValue',
|
|
11
18
|
test2: 'testValue2',
|
|
12
19
|
},
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
options: {
|
|
21
|
+
...(withDevTools && {
|
|
15
22
|
devTools: {
|
|
16
23
|
enabled: true,
|
|
17
24
|
namespace: 'TestSignalStateHandler',
|
|
18
25
|
},
|
|
19
|
-
},
|
|
20
|
-
|
|
26
|
+
}),
|
|
27
|
+
...(distinct && {
|
|
28
|
+
distinct,
|
|
29
|
+
}),
|
|
30
|
+
...(typeof useDistinctUntilChanged === 'boolean' && {
|
|
31
|
+
useDistinctUntilChanged,
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
21
34
|
});
|
|
22
35
|
}
|
|
23
36
|
|
|
24
|
-
getActions():
|
|
37
|
+
getActions(): TestActions {
|
|
25
38
|
return {
|
|
26
39
|
testAction: () => {
|
|
27
40
|
this.setState({ test: 'newValue' });
|
|
@@ -34,9 +47,14 @@ describe('Signal State Handler', () => {
|
|
|
34
47
|
let stateHandler: TestSignalStateHandler;
|
|
35
48
|
|
|
36
49
|
beforeEach(() => {
|
|
50
|
+
resetStatusQuoForTests();
|
|
37
51
|
stateHandler = new TestSignalStateHandler();
|
|
38
52
|
});
|
|
39
53
|
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
resetStatusQuoForTests();
|
|
56
|
+
});
|
|
57
|
+
|
|
40
58
|
it('should provide initial state', () => {
|
|
41
59
|
expect(stateHandler.getInitialState()).toStrictEqual({
|
|
42
60
|
test: 'testValue',
|
|
@@ -94,4 +112,67 @@ describe('Signal State Handler', () => {
|
|
|
94
112
|
|
|
95
113
|
expect(spy).toHaveBeenCalledTimes(2);
|
|
96
114
|
});
|
|
115
|
+
|
|
116
|
+
it('should respect global distinct setup when disabled', () => {
|
|
117
|
+
setupStatusQuo({
|
|
118
|
+
distinct: {
|
|
119
|
+
enabled: false,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const handler = new TestSignalStateHandler();
|
|
124
|
+
const spy = jest.fn();
|
|
125
|
+
const unsubscribe = handler.subscribe(spy);
|
|
126
|
+
|
|
127
|
+
handler.setState({ test: 'same' });
|
|
128
|
+
handler.setState({ test: 'same' });
|
|
129
|
+
|
|
130
|
+
unsubscribe();
|
|
131
|
+
|
|
132
|
+
expect(spy).toHaveBeenCalledTimes(2);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should respect global custom distinct comparator from setupStatusQuo', () => {
|
|
136
|
+
setupStatusQuo({
|
|
137
|
+
distinct: {
|
|
138
|
+
comparator: (previous: TestState, next: TestState) => {
|
|
139
|
+
return previous.test === next.test;
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const handler = new TestSignalStateHandler();
|
|
145
|
+
const spy = jest.fn();
|
|
146
|
+
const unsubscribe = handler.subscribe(spy);
|
|
147
|
+
|
|
148
|
+
handler.setState({ test2: 'newValue2' });
|
|
149
|
+
handler.setState({ test: 'newValue' });
|
|
150
|
+
|
|
151
|
+
unsubscribe();
|
|
152
|
+
|
|
153
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should prefer per-handler distinct options over global setup', () => {
|
|
157
|
+
setupStatusQuo({
|
|
158
|
+
distinct: {
|
|
159
|
+
enabled: false,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const handler = new TestSignalStateHandler({
|
|
164
|
+
distinct: {
|
|
165
|
+
enabled: true,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
const spy = jest.fn();
|
|
169
|
+
const unsubscribe = handler.subscribe(spy);
|
|
170
|
+
|
|
171
|
+
handler.setState({ test: 'same' });
|
|
172
|
+
handler.setState({ test: 'same' });
|
|
173
|
+
|
|
174
|
+
unsubscribe();
|
|
175
|
+
|
|
176
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
177
|
+
});
|
|
97
178
|
});
|
package/src/store/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { BaseStateHandler } from './base-state-handler.js';
|
|
2
2
|
export { ObservableStateHandler } from './observable-state-handler.js';
|
|
3
3
|
export { SignalStateHandler } from './signal-state-handler.js';
|
|
4
|
-
export type { StateSingleton } from './state-singleton.js';
|
|
4
|
+
export type { StateSingleton, StateSingletonOptions } from './state-singleton.js';
|
|
5
5
|
export { makeStateSingleton } from './state-singleton.js';
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { BehaviorSubject, distinctUntilKeyChanged, map
|
|
1
|
+
import { BehaviorSubject, distinctUntilChanged, distinctUntilKeyChanged, map } from 'rxjs';
|
|
2
2
|
|
|
3
3
|
import { BaseStateHandler } from './base-state-handler.js';
|
|
4
|
+
import { resolveDistinctOptions } from '../config/status-quo-config.js';
|
|
4
5
|
|
|
5
6
|
import type { Observable } from 'rxjs';
|
|
7
|
+
import type { DistinctOptions } from '../config/status-quo-config.js';
|
|
6
8
|
|
|
7
9
|
type ObservableStateHandlerProps<S> = {
|
|
8
10
|
initialState: S;
|
|
@@ -11,29 +13,21 @@ type ObservableStateHandlerProps<S> = {
|
|
|
11
13
|
enabled?: boolean;
|
|
12
14
|
namespace: string;
|
|
13
15
|
};
|
|
16
|
+
distinct?: DistinctOptions<S>;
|
|
17
|
+
useDistinctUntilChanged?: boolean;
|
|
14
18
|
};
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
type StateObservableOptions = { useDistinctUntilChanged?: boolean };
|
|
18
22
|
|
|
19
|
-
function distinctUntilChangedAsJson<T>() {
|
|
20
|
-
return pipe<Observable<T>, Observable<T>>(
|
|
21
|
-
distinctUntilChanged((a, b) => {
|
|
22
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
23
|
-
})
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const pipeMap = {
|
|
28
|
-
useDistinctUntilChanged: distinctUntilChangedAsJson(),
|
|
29
|
-
};
|
|
30
|
-
|
|
31
23
|
export abstract class ObservableStateHandler<S, A> extends BaseStateHandler<S, A> {
|
|
32
24
|
private readonly state$: BehaviorSubject<S>;
|
|
25
|
+
private readonly distinctOptions: ReturnType<typeof resolveDistinctOptions<S>>;
|
|
33
26
|
|
|
34
27
|
protected constructor({ initialState, options }: ObservableStateHandlerProps<S>) {
|
|
35
28
|
super(initialState);
|
|
36
29
|
this.state$ = new BehaviorSubject<S>(initialState);
|
|
30
|
+
this.distinctOptions = resolveDistinctOptions(options?.distinct, options?.useDistinctUntilChanged);
|
|
37
31
|
this.initDevTools(options?.devTools);
|
|
38
32
|
}
|
|
39
33
|
|
|
@@ -52,26 +46,30 @@ export abstract class ObservableStateHandler<S, A> extends BaseStateHandler<S, A
|
|
|
52
46
|
);
|
|
53
47
|
}
|
|
54
48
|
|
|
55
|
-
getStateAsObservable(
|
|
56
|
-
|
|
57
|
-
useDistinctUntilChanged
|
|
49
|
+
getStateAsObservable(options: StateObservableOptions = {}) {
|
|
50
|
+
const useDistinctUntilChanged =
|
|
51
|
+
options.useDistinctUntilChanged ?? this.distinctOptions.enabled;
|
|
52
|
+
|
|
53
|
+
if (!useDistinctUntilChanged) {
|
|
54
|
+
return this.state$;
|
|
58
55
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.map((enabledOptions) => pipeMap[enabledOptions as keyof StateObservableOptions])
|
|
66
|
-
.reduce((stateObservable$, operator) => {
|
|
67
|
-
return stateObservable$.pipe(operator) as BehaviorSubject<S>;
|
|
68
|
-
}, this.state$);
|
|
56
|
+
|
|
57
|
+
return this.state$.pipe(
|
|
58
|
+
distinctUntilChanged((previous, next) => {
|
|
59
|
+
return this.distinctOptions.comparator(previous, next);
|
|
60
|
+
})
|
|
61
|
+
) as Observable<S>;
|
|
69
62
|
}
|
|
70
63
|
|
|
71
|
-
|
|
64
|
+
getObservable(key: keyof S) {
|
|
72
65
|
return this.getStateItemAsObservable(key);
|
|
73
66
|
}
|
|
74
67
|
|
|
68
|
+
/** @deprecated Use getObservable instead. */
|
|
69
|
+
getObservableItem(key: keyof S) {
|
|
70
|
+
return this.getObservable(key);
|
|
71
|
+
}
|
|
72
|
+
|
|
75
73
|
subscribe(listener: () => void) {
|
|
76
74
|
let initialized = false;
|
|
77
75
|
const subscription = this.getStateAsObservable().subscribe(() => {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { signal } from '@preact/signals-core';
|
|
2
2
|
|
|
3
3
|
import { BaseStateHandler } from './base-state-handler.js';
|
|
4
|
+
import { resolveDistinctOptions } from '../config/status-quo-config.js';
|
|
4
5
|
|
|
5
6
|
import type { Signal } from '@preact/signals-core';
|
|
7
|
+
import type { DistinctOptions } from '../config/status-quo-config.js';
|
|
6
8
|
|
|
7
9
|
type SignalStateHandlerProps<S> = {
|
|
8
10
|
initialState: S;
|
|
@@ -11,34 +13,23 @@ type SignalStateHandlerProps<S> = {
|
|
|
11
13
|
enabled?: boolean;
|
|
12
14
|
namespace: string;
|
|
13
15
|
};
|
|
16
|
+
distinct?: DistinctOptions<S>;
|
|
14
17
|
useDistinctUntilChanged?: boolean;
|
|
15
18
|
};
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
type Listener = () => void;
|
|
19
22
|
|
|
20
|
-
const defaultOptions = {
|
|
21
|
-
useDistinctUntilChanged: true,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
function isEqualAsJson(a: unknown, b: unknown) {
|
|
25
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
23
|
export abstract class SignalStateHandler<S, A> extends BaseStateHandler<S, A> {
|
|
29
24
|
private readonly state: Signal<S>;
|
|
30
25
|
private readonly listeners = new Map<Listener, S>();
|
|
31
|
-
private readonly
|
|
26
|
+
private readonly distinctOptions: ReturnType<typeof resolveDistinctOptions<S>>;
|
|
32
27
|
|
|
33
|
-
protected constructor({ initialState, options
|
|
28
|
+
protected constructor({ initialState, options }: SignalStateHandlerProps<S>) {
|
|
34
29
|
super(initialState);
|
|
35
|
-
const mergedOptions = {
|
|
36
|
-
...defaultOptions,
|
|
37
|
-
...options,
|
|
38
|
-
};
|
|
39
30
|
|
|
40
31
|
this.state = signal<S>(initialState);
|
|
41
|
-
this.
|
|
32
|
+
this.distinctOptions = resolveDistinctOptions(options?.distinct, options?.useDistinctUntilChanged);
|
|
42
33
|
this.initDevTools(options?.devTools);
|
|
43
34
|
}
|
|
44
35
|
|
|
@@ -65,7 +56,7 @@ export abstract class SignalStateHandler<S, A> extends BaseStateHandler<S, A> {
|
|
|
65
56
|
|
|
66
57
|
private notify(nextState: S) {
|
|
67
58
|
for (const [listener, lastSnapshot] of this.listeners.entries()) {
|
|
68
|
-
if (this.
|
|
59
|
+
if (this.distinctOptions.enabled && this.distinctOptions.comparator(lastSnapshot, nextState)) {
|
|
69
60
|
continue;
|
|
70
61
|
}
|
|
71
62
|
|
|
@@ -4,12 +4,20 @@ export interface StateSingleton<V, A> {
|
|
|
4
4
|
getInstance: () => StateSubscriptionHandler<V, A>;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
export interface StateSingletonOptions {
|
|
8
|
+
destroyOnNoConsumers?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
export function makeStateSingleton<S, A>(
|
|
8
|
-
stateHandlerFactory: () => StateSubscriptionHandler<S, A
|
|
12
|
+
stateHandlerFactory: () => StateSubscriptionHandler<S, A>,
|
|
13
|
+
{ destroyOnNoConsumers = true }: StateSingletonOptions = {}
|
|
9
14
|
): StateSingleton<S, A> {
|
|
10
15
|
let instance: StateSubscriptionHandler<S, A> | null = null;
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
const singleton: StateSingleton<S, A> & {
|
|
17
|
+
destroyInstance: () => void;
|
|
18
|
+
destroyOnNoConsumers: boolean;
|
|
19
|
+
} = {
|
|
20
|
+
destroyOnNoConsumers,
|
|
13
21
|
getInstance() {
|
|
14
22
|
if (!instance) {
|
|
15
23
|
instance = stateHandlerFactory();
|
|
@@ -17,5 +25,15 @@ export function makeStateSingleton<S, A>(
|
|
|
17
25
|
|
|
18
26
|
return instance;
|
|
19
27
|
},
|
|
28
|
+
destroyInstance() {
|
|
29
|
+
if (!instance) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
instance.destroy();
|
|
34
|
+
instance = null;
|
|
35
|
+
},
|
|
20
36
|
};
|
|
37
|
+
|
|
38
|
+
return singleton;
|
|
21
39
|
}
|