@veams/status-quo 0.1.0 → 1.1.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 (65) hide show
  1. package/.github/workflows/pages.yml +46 -0
  2. package/.github/workflows/release.yml +33 -0
  3. package/CHANGELOG.md +30 -0
  4. package/README.md +260 -124
  5. package/assets/statusquo-logo.png +0 -0
  6. package/dist/hooks/state-subscription.d.ts +1 -2
  7. package/dist/hooks/state-subscription.js +8 -9
  8. package/dist/hooks/state-subscription.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +2 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/store/__tests__/{state-handler.spec.js → observable-state-handler.spec.js} +14 -14
  13. package/dist/store/__tests__/observable-state-handler.spec.js.map +1 -0
  14. package/dist/store/__tests__/signal-state-handler.spec.js +78 -0
  15. package/dist/store/__tests__/signal-state-handler.spec.js.map +1 -0
  16. package/dist/store/base-state-handler.d.ts +30 -0
  17. package/dist/store/base-state-handler.js +84 -0
  18. package/dist/store/base-state-handler.js.map +1 -0
  19. package/dist/store/index.d.ts +3 -1
  20. package/dist/store/index.js +3 -1
  21. package/dist/store/index.js.map +1 -1
  22. package/dist/store/observable-state-handler.d.ts +26 -0
  23. package/dist/store/observable-state-handler.js +55 -0
  24. package/dist/store/observable-state-handler.js.map +1 -0
  25. package/dist/store/signal-state-handler.d.ts +25 -0
  26. package/dist/store/signal-state-handler.js +49 -0
  27. package/dist/store/signal-state-handler.js.map +1 -0
  28. package/dist/types/types.d.ts +2 -2
  29. package/package.json +22 -11
  30. package/playground/index.html +12 -0
  31. package/playground/src/App.tsx +478 -0
  32. package/playground/src/assets/philosophy-agnostic.svg +18 -0
  33. package/playground/src/assets/philosophy-separation.svg +13 -0
  34. package/playground/src/assets/philosophy-swap.svg +17 -0
  35. package/playground/src/assets/statusquo-logo.png +0 -0
  36. package/playground/src/main.tsx +19 -0
  37. package/playground/src/styles.css +411 -0
  38. package/playground/tsconfig.json +12 -0
  39. package/playground/vite.config.ts +18 -0
  40. package/src/hooks/state-subscription.tsx +21 -14
  41. package/src/index.ts +14 -2
  42. package/src/store/__tests__/{state-handler.spec.ts → observable-state-handler.spec.ts} +15 -15
  43. package/src/store/__tests__/signal-state-handler.spec.ts +97 -0
  44. package/src/store/base-state-handler.ts +119 -0
  45. package/src/store/index.ts +3 -1
  46. package/src/store/observable-state-handler.ts +87 -0
  47. package/src/store/signal-state-handler.ts +76 -0
  48. package/src/types/types.ts +2 -3
  49. package/dist/store/__tests__/state-handler.spec.js.map +0 -1
  50. package/dist/store/state-handler.d.ts +0 -36
  51. package/dist/store/state-handler.js +0 -122
  52. package/dist/store/state-handler.js.map +0 -1
  53. package/dist/types/hooks/index.d.ts +0 -2
  54. package/dist/types/hooks/state-factory.d.ts +0 -2
  55. package/dist/types/hooks/state-singleton.d.ts +0 -2
  56. package/dist/types/hooks/state-subscription.d.ts +0 -3
  57. package/dist/types/index.d.ts +0 -6
  58. package/dist/types/store/dev-tools.d.ts +0 -23
  59. package/dist/types/store/index.d.ts +0 -3
  60. package/dist/types/store/state-handler.d.ts +0 -36
  61. package/dist/types/store/state-singleton.d.ts +0 -5
  62. package/dist/types/types/types.d.ts +0 -7
  63. package/src/store/state-handler.ts +0 -181
  64. /package/dist/store/__tests__/{state-handler.spec.d.ts → observable-state-handler.spec.d.ts} +0 -0
  65. /package/dist/{types/store/__tests__/state-handler.spec.d.ts → store/__tests__/signal-state-handler.spec.d.ts} +0 -0
@@ -0,0 +1,411 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap');
2
+
3
+ :root {
4
+ color-scheme: light;
5
+ --bg: #f3f2ee;
6
+ --bg-strong: #e6e1d8;
7
+ --card: #ffffff;
8
+ --ink: #1b1b1f;
9
+ --muted: #5c5c66;
10
+ --accent: #2f6bff;
11
+ --accent-soft: #c9d8ff;
12
+ --accent-2: #ff9f1c;
13
+ --shadow: 0 18px 45px -30px rgba(0, 0, 0, 0.45);
14
+ --radius-lg: 28px;
15
+ --radius-md: 18px;
16
+ --radius-sm: 12px;
17
+ --panel-dark: #0e1116;
18
+ font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
19
+ }
20
+
21
+ * {
22
+ box-sizing: border-box;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ min-height: 100vh;
28
+ color: var(--ink);
29
+ background:
30
+ radial-gradient(circle at 20% 20%, rgba(47, 107, 255, 0.18), transparent 55%),
31
+ radial-gradient(circle at 80% 0%, rgba(255, 159, 28, 0.2), transparent 50%),
32
+ linear-gradient(160deg, var(--bg), var(--bg-strong));
33
+ }
34
+
35
+ #root {
36
+ min-height: 100vh;
37
+ }
38
+
39
+ .app {
40
+ max-width: 1100px;
41
+ margin: 0 auto;
42
+ padding: 56px 28px 72px;
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: 40px;
46
+ }
47
+
48
+ .brand-bar {
49
+ display: inline-flex;
50
+ align-items: center;
51
+ gap: 10px;
52
+ font-weight: 600;
53
+ justify-content: center;
54
+ width: 100%;
55
+ }
56
+
57
+ .brand-logo {
58
+ width: 260px;
59
+ height: auto;
60
+ display: block;
61
+ }
62
+
63
+ .nav {
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ padding: 10px 6px;
68
+ }
69
+
70
+ .nav-links {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ gap: 10px;
74
+ flex-wrap: wrap;
75
+ padding: 6px 12px;
76
+ background: rgba(255, 255, 255, 0.7);
77
+ border-radius: 999px;
78
+ border: 1px solid rgba(27, 27, 31, 0.1);
79
+ box-shadow: var(--shadow);
80
+ }
81
+
82
+ .nav-links a {
83
+ text-decoration: none;
84
+ color: var(--ink);
85
+ font-weight: 500;
86
+ padding: 6px 12px;
87
+ border-radius: 999px;
88
+ transition: background 0.2s ease, color 0.2s ease;
89
+ }
90
+
91
+ .nav-links a:hover {
92
+ background: rgba(47, 107, 255, 0.12);
93
+ color: var(--accent);
94
+ }
95
+
96
+ .hero {
97
+ display: grid;
98
+ gap: 16px;
99
+ padding: 32px 34px;
100
+ background: rgba(255, 255, 255, 0.75);
101
+ border-radius: var(--radius-lg);
102
+ box-shadow: var(--shadow);
103
+ border: 1px solid rgba(27, 27, 31, 0.08);
104
+ backdrop-filter: blur(14px);
105
+ animation: fade-up 0.6s ease both;
106
+ }
107
+
108
+ .hero.intro {
109
+ gap: 18px;
110
+ }
111
+
112
+ h1 {
113
+ font-size: clamp(2rem, 1.6vw + 1.4rem, 2.8rem);
114
+ line-height: 1.15;
115
+ margin: 0;
116
+ }
117
+
118
+ h2 {
119
+ margin: 0;
120
+ font-size: 1.4rem;
121
+ }
122
+
123
+ h3 {
124
+ margin: 0 0 8px;
125
+ }
126
+
127
+ p {
128
+ margin: 0;
129
+ color: var(--muted);
130
+ line-height: 1.6;
131
+
132
+ span {
133
+ font-family: monospace;
134
+ font-weight: 700;
135
+ font-size: .9rem;
136
+ }
137
+ }
138
+
139
+ .muted {
140
+ color: var(--muted);
141
+ }
142
+
143
+ .eyebrow {
144
+ font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, monospace;
145
+ text-transform: uppercase;
146
+ letter-spacing: 0.18em;
147
+ font-size: 0.7rem;
148
+ color: var(--muted);
149
+ margin-bottom: 10px;
150
+ }
151
+
152
+ .subtext {
153
+ max-width: 600px;
154
+ }
155
+
156
+ .hero .subtext {
157
+ margin-top: 12px;
158
+ }
159
+
160
+ .hero .subtext + .subtext {
161
+ margin-top: 10px;
162
+ }
163
+
164
+ .grid {
165
+ display: grid;
166
+ gap: 24px;
167
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
168
+ }
169
+
170
+ .doc-section {
171
+ display: grid;
172
+ gap: 18px;
173
+ padding: 28px;
174
+ border-radius: var(--radius-md);
175
+ background: rgba(255, 255, 255, 0.85);
176
+ border: 1px solid rgba(27, 27, 31, 0.08);
177
+ box-shadow: var(--shadow);
178
+ }
179
+
180
+ .doc-copy {
181
+ display: grid;
182
+ gap: 10px;
183
+ }
184
+
185
+ .doc-snippets {
186
+ display: grid;
187
+ gap: 12px;
188
+ }
189
+
190
+ .singleton-grid {
191
+ display: grid;
192
+ gap: 20px;
193
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
194
+ }
195
+
196
+ .singleton-card {
197
+ background: #fff;
198
+ border-radius: var(--radius-md);
199
+ padding: 22px;
200
+ border: 1px solid rgba(27, 27, 31, 0.08);
201
+ box-shadow: var(--shadow);
202
+ display: grid;
203
+ gap: 14px;
204
+ }
205
+
206
+ .singleton-count {
207
+ font-size: 2.4rem;
208
+ font-weight: 700;
209
+ color: var(--ink);
210
+ background: var(--bg-strong);
211
+ border-radius: var(--radius-sm);
212
+ padding: 10px 16px;
213
+ width: fit-content;
214
+ }
215
+
216
+ .singleton-count.highlight {
217
+ background: rgba(47, 107, 255, 0.16);
218
+ color: var(--accent);
219
+ }
220
+
221
+ .singleton-hint {
222
+ font-size: 0.85rem;
223
+ color: var(--muted);
224
+ }
225
+
226
+ .guide {
227
+ display: grid;
228
+ gap: 20px;
229
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
230
+ }
231
+
232
+ .philosophy-grid {
233
+ display: grid;
234
+ gap: 20px;
235
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
236
+ }
237
+
238
+ .philosophy-card {
239
+ background: #fff;
240
+ border-radius: var(--radius-md);
241
+ border: 1px solid rgba(27, 27, 31, 0.08);
242
+ box-shadow: var(--shadow);
243
+ overflow: hidden;
244
+ display: grid;
245
+ gap: 16px;
246
+ }
247
+
248
+ .philosophy-card img {
249
+ width: 100%;
250
+ height: 190px;
251
+ object-fit: cover;
252
+ background: var(--panel-dark);
253
+ }
254
+
255
+ .philosophy-card div {
256
+ padding: 0 20px 22px;
257
+ display: grid;
258
+ gap: 8px;
259
+ }
260
+
261
+ .guide-card {
262
+ background: rgba(255, 255, 255, 0.85);
263
+ border-radius: var(--radius-md);
264
+ padding: 22px;
265
+ border: 1px solid rgba(27, 27, 31, 0.08);
266
+ box-shadow: var(--shadow);
267
+ display: grid;
268
+ gap: 16px;
269
+ }
270
+
271
+ .code-block.compact {
272
+ font-size: 0.75rem;
273
+ padding: 12px 14px;
274
+ }
275
+
276
+ .card {
277
+ background: var(--card);
278
+ border-radius: var(--radius-md);
279
+ padding: 24px;
280
+ box-shadow: var(--shadow);
281
+ border: 1px solid rgba(27, 27, 31, 0.08);
282
+ display: flex;
283
+ flex-direction: column;
284
+ gap: 20px;
285
+ animation: fade-up 0.6s ease both;
286
+ }
287
+
288
+ .card:nth-child(2) {
289
+ animation-delay: 0.1s;
290
+ }
291
+
292
+ .card-header {
293
+ display: flex;
294
+ align-items: center;
295
+ justify-content: space-between;
296
+ gap: 12px;
297
+ }
298
+
299
+ .count-chip {
300
+ background: var(--accent-soft);
301
+ color: var(--accent);
302
+ border-radius: 999px;
303
+ padding: 6px 14px;
304
+ font-weight: 600;
305
+ font-size: 1rem;
306
+ }
307
+
308
+ .count-display {
309
+ display: grid;
310
+ gap: 8px;
311
+ padding: 16px;
312
+ border-radius: var(--radius-sm);
313
+ background: linear-gradient(120deg, rgba(47, 107, 255, 0.1), rgba(255, 159, 28, 0.08));
314
+ }
315
+
316
+ .code-block {
317
+ margin: 0;
318
+ padding: 16px 18px;
319
+ border-radius: var(--radius-sm);
320
+ background: #12131a;
321
+ color: #f0f2ff;
322
+ font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, monospace;
323
+ font-size: 0.8rem;
324
+ line-height: 1.5;
325
+ overflow-x: auto;
326
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
327
+ }
328
+
329
+ .code-block code {
330
+ font-family: inherit;
331
+ }
332
+
333
+ .count-label {
334
+ font-size: 0.85rem;
335
+ color: var(--muted);
336
+ }
337
+
338
+ .count-value {
339
+ font-size: 2.6rem;
340
+ font-weight: 700;
341
+ }
342
+
343
+ .actions {
344
+ display: grid;
345
+ grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
346
+ gap: 10px;
347
+ }
348
+
349
+ .btn {
350
+ border: 1px solid rgba(27, 27, 31, 0.18);
351
+ border-radius: 999px;
352
+ padding: 10px 14px;
353
+ font: inherit;
354
+ background: #fff;
355
+ cursor: pointer;
356
+ transition: transform 0.2s ease, box-shadow 0.2s ease, border 0.2s ease;
357
+ }
358
+
359
+ .btn:hover {
360
+ transform: translateY(-1px);
361
+ box-shadow: 0 10px 20px -14px rgba(27, 27, 31, 0.4);
362
+ }
363
+
364
+ .btn.primary {
365
+ background: var(--accent);
366
+ color: #fff;
367
+ border-color: transparent;
368
+ }
369
+
370
+ .btn.ghost {
371
+ background: transparent;
372
+ color: var(--ink);
373
+ }
374
+
375
+ @keyframes fade-up {
376
+ from {
377
+ opacity: 0;
378
+ transform: translateY(12px);
379
+ }
380
+ to {
381
+ opacity: 1;
382
+ transform: translateY(0);
383
+ }
384
+ }
385
+
386
+ @media (max-width: 720px) {
387
+ .app {
388
+ padding: 40px 20px 60px;
389
+ }
390
+
391
+ .nav {
392
+ padding: 0;
393
+ }
394
+
395
+ .nav-links {
396
+ border-radius: var(--radius-md);
397
+ }
398
+
399
+ .doc-section {
400
+ padding: 22px;
401
+ }
402
+
403
+ .hero {
404
+ padding: 26px;
405
+ }
406
+
407
+ .footer-card {
408
+ flex-direction: column;
409
+ align-items: flex-start;
410
+ }
411
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "noEmit": true,
6
+ "baseUrl": ".",
7
+ "paths": {
8
+ "@veams/status-quo": ["../src/index.ts"]
9
+ }
10
+ },
11
+ "include": ["src"]
12
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import { fileURLToPath, URL } from 'node:url';
4
+
5
+ export default defineConfig({
6
+ root: fileURLToPath(new URL('.', import.meta.url)),
7
+ base: './',
8
+ plugins: [react()],
9
+ resolve: {
10
+ alias: {
11
+ '@veams/status-quo': fileURLToPath(new URL('../src/index.ts', import.meta.url)),
12
+ },
13
+ },
14
+ build: {
15
+ outDir: fileURLToPath(new URL('../docs', import.meta.url)),
16
+ emptyOutDir: true,
17
+ },
18
+ });
@@ -1,25 +1,32 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useCallback, useSyncExternalStore } from 'react';
2
2
 
3
3
  import type { StateSubscriptionHandler } from '../types/types.js';
4
- import type { SetStateAction } from 'react';
4
+ type Listener = () => void;
5
5
 
6
6
  export function useStateSubscription<V, A>(
7
7
  stateSubscriptionHandler: StateSubscriptionHandler<V, A>
8
8
  ) {
9
- const [state, setSubscriptionState] = useState<SetStateAction<V>>(
10
- stateSubscriptionHandler.getInitialState()
9
+ const subscribe = useCallback(
10
+ (listener: Listener) => {
11
+ const unsubscribe = stateSubscriptionHandler.subscribe(listener);
12
+
13
+ return () => {
14
+ unsubscribe();
15
+ stateSubscriptionHandler.destroy();
16
+ };
17
+ },
18
+ [stateSubscriptionHandler]
11
19
  );
12
20
 
13
- useEffect(() => {
14
- const state$ = stateSubscriptionHandler.getObservable().subscribe((data) => {
15
- setSubscriptionState(data);
16
- });
21
+ const getSnapshot = useCallback(
22
+ () => stateSubscriptionHandler.getSnapshot(),
23
+ [stateSubscriptionHandler]
24
+ );
17
25
 
18
- return () => {
19
- state$.unsubscribe();
20
- return stateSubscriptionHandler.destroy();
21
- };
22
- }, [stateSubscriptionHandler]);
26
+ const getServerSnapshot = useCallback(
27
+ () => stateSubscriptionHandler.getInitialState(),
28
+ [stateSubscriptionHandler]
29
+ );
23
30
 
24
- return state;
31
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
25
32
  }
package/src/index.ts CHANGED
@@ -1,9 +1,21 @@
1
1
  import { useStateFactory, useStateSingleton } from './hooks/index.js';
2
- import { makeStateSingleton, StateHandler } from './store/index.js';
2
+ import {
3
+ BaseStateHandler,
4
+ makeStateSingleton,
5
+ ObservableStateHandler,
6
+ SignalStateHandler,
7
+ } from './store/index.js';
3
8
 
4
9
  import type { StateSingleton } from './store/index.js';
5
10
  import type { StateSubscriptionHandler } from './types/types.js';
6
11
 
7
- export { makeStateSingleton, StateHandler, useStateFactory, useStateSingleton };
12
+ export {
13
+ BaseStateHandler,
14
+ makeStateSingleton,
15
+ ObservableStateHandler,
16
+ SignalStateHandler,
17
+ useStateFactory,
18
+ useStateSingleton,
19
+ };
8
20
 
9
21
  export type { StateSingleton, StateSubscriptionHandler };
@@ -1,8 +1,8 @@
1
1
  import { lastValueFrom, Subject, take } from 'rxjs';
2
2
 
3
- import { StateHandler } from '../state-handler.js';
3
+ import { ObservableStateHandler } from '../observable-state-handler.js';
4
4
 
5
- class TestStateHandler extends StateHandler<
5
+ class TestObservableStateHandler extends ObservableStateHandler<
6
6
  { test: string; test2: string },
7
7
  { testAction: () => void }
8
8
  > {
@@ -13,18 +13,16 @@ class TestStateHandler extends StateHandler<
13
13
  test2: 'testValue2',
14
14
  },
15
15
  ...(withDevTools && {
16
- devTools: {
17
- enabled: true,
18
- namespace: 'TestStateHandler',
16
+ options: {
17
+ devTools: {
18
+ enabled: true,
19
+ namespace: 'TestObservableStateHandler',
20
+ },
19
21
  },
20
22
  }),
21
23
  });
22
24
  }
23
25
 
24
- getObservable() {
25
- return this.getStateAsObservable();
26
- }
27
-
28
26
  getActions(): { testAction: () => void } {
29
27
  return {
30
28
  testAction: () => {
@@ -34,11 +32,11 @@ class TestStateHandler extends StateHandler<
34
32
  }
35
33
  }
36
34
 
37
- describe('State Handler', () => {
38
- let stateHandler: TestStateHandler;
35
+ describe('Observable State Handler', () => {
36
+ let stateHandler: TestObservableStateHandler;
39
37
 
40
38
  beforeEach(() => {
41
- stateHandler = new TestStateHandler();
39
+ stateHandler = new TestObservableStateHandler();
42
40
  });
43
41
 
44
42
  it('should provide initial state', () => {
@@ -63,7 +61,7 @@ describe('State Handler', () => {
63
61
 
64
62
  stateHandler.setState(expected);
65
63
 
66
- const state = await lastValueFrom(stateHandler.getObservable().pipe(take(1)));
64
+ const state = await lastValueFrom(stateHandler.getStateAsObservable().pipe(take(1)));
67
65
 
68
66
  expect(state).toStrictEqual(expected);
69
67
  expect(stateHandler.getState()).toStrictEqual(expected);
@@ -89,7 +87,7 @@ describe('State Handler', () => {
89
87
  it('should only call subscriber when object state has changed', async () => {
90
88
  const spy = jest.fn();
91
89
 
92
- stateHandler.getObservable().subscribe(spy);
90
+ const unsubscribe = stateHandler.subscribe(spy);
93
91
  stateHandler.setState({
94
92
  test: 'test',
95
93
  });
@@ -103,6 +101,8 @@ describe('State Handler', () => {
103
101
  test: 'test2',
104
102
  });
105
103
 
106
- expect(spy).toHaveBeenCalledTimes(3); // 1. testValue (Initial value), 2. test (first setter), 3. test2 (second setter)
104
+ unsubscribe();
105
+
106
+ expect(spy).toHaveBeenCalledTimes(2); // 1. test (first setter), 2. test2 (second setter)
107
107
  });
108
108
  });
@@ -0,0 +1,97 @@
1
+ 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) {
8
+ super({
9
+ initialState: {
10
+ test: 'testValue',
11
+ test2: 'testValue2',
12
+ },
13
+ ...(withDevTools && {
14
+ options: {
15
+ devTools: {
16
+ enabled: true,
17
+ namespace: 'TestSignalStateHandler',
18
+ },
19
+ },
20
+ }),
21
+ });
22
+ }
23
+
24
+ getActions(): { testAction: () => void } {
25
+ return {
26
+ testAction: () => {
27
+ this.setState({ test: 'newValue' });
28
+ },
29
+ };
30
+ }
31
+ }
32
+
33
+ describe('Signal State Handler', () => {
34
+ let stateHandler: TestSignalStateHandler;
35
+
36
+ beforeEach(() => {
37
+ stateHandler = new TestSignalStateHandler();
38
+ });
39
+
40
+ it('should provide initial state', () => {
41
+ expect(stateHandler.getInitialState()).toStrictEqual({
42
+ test: 'testValue',
43
+ test2: 'testValue2',
44
+ });
45
+ });
46
+
47
+ it('should provide current state', () => {
48
+ expect(stateHandler.getState()).toStrictEqual({
49
+ test: 'testValue',
50
+ test2: 'testValue2',
51
+ });
52
+ });
53
+
54
+ it('should support state changing via setter and merge state object on first level', () => {
55
+ const expected = {
56
+ test: 'change',
57
+ test2: 'testValue2',
58
+ };
59
+
60
+ stateHandler.setState(expected);
61
+
62
+ expect(stateHandler.getState()).toStrictEqual(expected);
63
+ });
64
+
65
+ it('should support additional subscriptions handling', () => {
66
+ const spy = jest.fn();
67
+ const subscription = { unsubscribe: spy };
68
+
69
+ stateHandler.subscriptions = [subscription];
70
+
71
+ stateHandler.destroy();
72
+
73
+ expect(spy).toHaveBeenCalledTimes(1);
74
+ });
75
+
76
+ it('should only call subscriber when object state has changed', () => {
77
+ const spy = jest.fn();
78
+ const unsubscribe = stateHandler.subscribe(spy);
79
+
80
+ stateHandler.setState({
81
+ test: 'test',
82
+ });
83
+ stateHandler.setState({
84
+ test: 'test2',
85
+ });
86
+ stateHandler.setState({
87
+ test: 'test2',
88
+ });
89
+ stateHandler.setState({
90
+ test: 'test2',
91
+ });
92
+
93
+ unsubscribe();
94
+
95
+ expect(spy).toHaveBeenCalledTimes(2);
96
+ });
97
+ });