@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +252 -18
  3. package/dist/config/status-quo-config.d.ts +21 -0
  4. package/dist/config/status-quo-config.js +48 -0
  5. package/dist/config/status-quo-config.js.map +1 -0
  6. package/dist/hooks/__tests__/state-selector.spec.d.ts +4 -0
  7. package/dist/hooks/__tests__/state-selector.spec.js +384 -0
  8. package/dist/hooks/__tests__/state-selector.spec.js.map +1 -0
  9. package/dist/hooks/__tests__/state-singleton.spec.d.ts +4 -0
  10. package/dist/hooks/__tests__/state-singleton.spec.js +97 -0
  11. package/dist/hooks/__tests__/state-singleton.spec.js.map +1 -0
  12. package/dist/hooks/index.d.ts +3 -0
  13. package/dist/hooks/index.js +3 -0
  14. package/dist/hooks/index.js.map +1 -1
  15. package/dist/hooks/state-actions.d.ts +2 -0
  16. package/dist/hooks/state-actions.js +5 -0
  17. package/dist/hooks/state-actions.js.map +1 -0
  18. package/dist/hooks/state-factory.d.ts +5 -0
  19. package/dist/hooks/state-factory.js +10 -6
  20. package/dist/hooks/state-factory.js.map +1 -1
  21. package/dist/hooks/state-handler.d.ts +2 -0
  22. package/dist/hooks/state-handler.js +9 -0
  23. package/dist/hooks/state-handler.js.map +1 -0
  24. package/dist/hooks/state-singleton.d.ts +4 -0
  25. package/dist/hooks/state-singleton.js +3 -5
  26. package/dist/hooks/state-singleton.js.map +1 -1
  27. package/dist/hooks/state-subscription-selector.d.ts +5 -0
  28. package/dist/hooks/state-subscription-selector.js +29 -0
  29. package/dist/hooks/state-subscription-selector.js.map +1 -0
  30. package/dist/hooks/state-subscription.d.ts +8 -1
  31. package/dist/hooks/state-subscription.js +49 -10
  32. package/dist/hooks/state-subscription.js.map +1 -1
  33. package/dist/index.d.ts +7 -5
  34. package/dist/index.js +4 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/store/__tests__/observable-state-handler.spec.js +68 -5
  37. package/dist/store/__tests__/observable-state-handler.spec.js.map +1 -1
  38. package/dist/store/__tests__/signal-state-handler.spec.js +64 -5
  39. package/dist/store/__tests__/signal-state-handler.spec.js.map +1 -1
  40. package/dist/store/index.d.ts +1 -1
  41. package/dist/store/observable-state-handler.d.ts +7 -2
  42. package/dist/store/observable-state-handler.js +17 -22
  43. package/dist/store/observable-state-handler.js.map +1 -1
  44. package/dist/store/signal-state-handler.d.ts +3 -1
  45. package/dist/store/signal-state-handler.js +5 -14
  46. package/dist/store/signal-state-handler.js.map +1 -1
  47. package/dist/store/state-singleton.d.ts +4 -1
  48. package/dist/store/state-singleton.js +11 -2
  49. package/dist/store/state-singleton.js.map +1 -1
  50. package/docs/assets/index-BBmpszOW.css +1 -0
  51. package/docs/assets/index-Cf8El_RO.js +194 -0
  52. package/docs/assets/statusquo-logo-8GVRbxpc.png +0 -0
  53. package/docs/index.html +13 -0
  54. package/package.json +1 -1
  55. package/playground/src/App.tsx +269 -48
  56. package/playground/src/styles.css +123 -0
  57. package/src/config/status-quo-config.ts +76 -0
  58. package/src/hooks/__tests__/state-selector.spec.tsx +607 -0
  59. package/src/hooks/__tests__/state-singleton.spec.tsx +151 -0
  60. package/src/hooks/index.ts +3 -0
  61. package/src/hooks/state-actions.tsx +7 -0
  62. package/src/hooks/state-factory.tsx +32 -6
  63. package/src/hooks/state-handler.tsx +16 -0
  64. package/src/hooks/state-singleton.tsx +17 -7
  65. package/src/hooks/state-subscription-selector.tsx +70 -0
  66. package/src/hooks/state-subscription.tsx +98 -21
  67. package/src/index.ts +23 -4
  68. package/src/store/__tests__/observable-state-handler.spec.ts +98 -11
  69. package/src/store/__tests__/signal-state-handler.spec.ts +92 -11
  70. package/src/store/index.ts +1 -1
  71. package/src/store/observable-state-handler.ts +25 -27
  72. package/src/store/signal-state-handler.ts +7 -16
  73. 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
- class TestSignalStateHandler extends SignalStateHandler<
4
- { test: string; test2: string },
5
- { testAction: () => void }
6
- > {
7
- constructor(withDevTools?: boolean) {
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
- ...(withDevTools && {
14
- options: {
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(): { testAction: () => void } {
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
  });
@@ -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, pipe, distinctUntilChanged } from 'rxjs';
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
- options: StateObservableOptions = {
57
- useDistinctUntilChanged: true,
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
- // Unfortunately we cannot add pipe operators conditionally in an easy manner.
61
- // That's why we use a simple object to attach operators to a new state observable via reduce().
62
- // This way we can easily extend our default operators map.
63
- return Object.keys(options)
64
- .filter((optionKey) => options[optionKey as keyof StateObservableOptions] === true)
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
- getObservableItem(key: keyof S) {
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 useDistinctUntilChanged: boolean;
26
+ private readonly distinctOptions: ReturnType<typeof resolveDistinctOptions<S>>;
32
27
 
33
- protected constructor({ initialState, options = defaultOptions }: SignalStateHandlerProps<S>) {
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.useDistinctUntilChanged = mergedOptions.useDistinctUntilChanged ?? true;
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.useDistinctUntilChanged && isEqualAsJson(nextState, lastSnapshot)) {
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
- return {
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
  }