@veams/status-quo 1.0.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.
- package/CHANGELOG.md +1 -0
- package/README.md +254 -135
- package/assets/statusquo-logo.png +0 -0
- package/dist/store/base-state-handler.d.ts +4 -0
- package/dist/store/base-state-handler.js +6 -0
- package/dist/store/base-state-handler.js.map +1 -1
- package/package.json +1 -1
- package/playground/src/App.tsx +38 -36
- package/playground/src/assets/statusquo-logo.png +0 -0
- package/playground/src/styles.css +10 -6
- package/src/store/base-state-handler.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
9
9
|
### Added
|
|
10
10
|
- `SignalStateHandler` (signals-backed state handler).
|
|
11
11
|
- `BaseStateHandler` to share devtools and lifecycle APIs.
|
|
12
|
+
- `bindSubscribable` helper for managing external subscriptions.
|
|
12
13
|
- Playground/demo with GitHub Pages deployment.
|
|
13
14
|
|
|
14
15
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,193 +1,312 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @veams/status-quo
|
|
2
|
+
[](https://www.npmjs.com/package/@veams/status-quo)
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
<center>
|
|
5
|
+
<img src="assets/statusquo-logo.png" width="200" alt="StatusQuo Logo" style="margin: 0 auto;">
|
|
6
|
+
</center>
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
The manager to rule your state.
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
This page mirrors the demo content and adds a full API reference.
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
2. [Example](#example)
|
|
12
|
+
## Table of Contents
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
1. [Overview](#overview)
|
|
15
|
+
2. [Philosophy](#philosophy)
|
|
16
|
+
3. [Demo](#demo)
|
|
17
|
+
4. [Quickstart](#quickstart)
|
|
18
|
+
5. [Handlers](#handlers)
|
|
19
|
+
6. [Hooks](#hooks)
|
|
20
|
+
7. [Singletons](#singletons)
|
|
21
|
+
8. [Composition](#composition)
|
|
22
|
+
9. [Devtools](#devtools)
|
|
23
|
+
10. [Cleanup](#cleanup)
|
|
24
|
+
11. [API Reference](#api-reference)
|
|
25
|
+
12. [Migration](#migration)
|
|
13
26
|
|
|
14
|
-
##
|
|
27
|
+
## Overview
|
|
15
28
|
|
|
16
|
-
|
|
17
|
-
1. Use actions and state in your component
|
|
18
|
-
1. When using React, initialize the state handler with a custom hook called `useStateFactory()` (or `useStateSingleton()` for Singleton states)
|
|
29
|
+
StatusQuo is a small, framework-agnostic state layer that focuses on explicit lifecycle, clear action APIs, and a minimal subscription surface. It ships two handler implementations with the same public interface: RxJS-backed observables and signals-backed stores.
|
|
19
30
|
|
|
31
|
+
## Philosophy
|
|
20
32
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
_Please keep in mind that dependencies for the hook needs to be flattened and cannot be used as an object due to how React works._
|
|
33
|
+
- Swap the engine, keep the API. Your UI code stays the same when you switch from RxJS to Signals.
|
|
34
|
+
- Separate view and state. Handlers own transitions and expose actions; views subscribe to snapshots.
|
|
35
|
+
- Framework-agnostic core. Business logic lives outside the UI library; hooks provide the glue.
|
|
25
36
|
|
|
26
37
|
## Demo
|
|
27
38
|
|
|
28
39
|
Live docs and demo:
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
https://veams.github.io/status-quo/
|
|
32
|
-
```
|
|
41
|
+
[https://veams.github.io/status-quo/](https://veams.github.io/status-quo/)
|
|
33
42
|
|
|
34
|
-
##
|
|
43
|
+
## Quickstart
|
|
35
44
|
|
|
36
|
-
|
|
45
|
+
Install:
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
## Example
|
|
47
|
+
```bash
|
|
48
|
+
npm install @veams/status-quo rxjs @preact/signals-core
|
|
49
|
+
```
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
You should start with the abstract class `ObservableStateHandler`:
|
|
51
|
+
Create a store and use it in a component:
|
|
45
52
|
|
|
46
53
|
```ts
|
|
47
|
-
import {
|
|
54
|
+
import { ObservableStateHandler, useStateFactory } from '@veams/status-quo';
|
|
55
|
+
|
|
56
|
+
type CounterState = { count: number };
|
|
48
57
|
|
|
49
|
-
type CounterState = {
|
|
50
|
-
count: number;
|
|
51
|
-
};
|
|
52
58
|
type CounterActions = {
|
|
53
59
|
increase: () => void;
|
|
54
60
|
decrease: () => void;
|
|
55
61
|
};
|
|
56
62
|
|
|
57
|
-
class
|
|
58
|
-
constructor(
|
|
59
|
-
super({ initialState: { count:
|
|
63
|
+
class CounterStore extends ObservableStateHandler<CounterState, CounterActions> {
|
|
64
|
+
constructor() {
|
|
65
|
+
super({ initialState: { count: 0 } });
|
|
60
66
|
}
|
|
61
|
-
|
|
62
|
-
getActions() {
|
|
67
|
+
|
|
68
|
+
getActions(): CounterActions {
|
|
63
69
|
return {
|
|
64
|
-
increase() {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
})
|
|
68
|
-
},
|
|
69
|
-
decrease() {
|
|
70
|
-
const currentState = this.getState();
|
|
71
|
-
|
|
72
|
-
if (currentState.count > 0) {
|
|
73
|
-
this.setState({
|
|
74
|
-
count: currentState - 1
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
70
|
+
increase: () => this.setState({ count: this.getState().count + 1 }),
|
|
71
|
+
decrease: () => this.setState({ count: this.getState().count - 1 }),
|
|
72
|
+
};
|
|
79
73
|
}
|
|
80
74
|
}
|
|
81
75
|
|
|
82
|
-
|
|
83
|
-
return new CounterStateHandler(...args);
|
|
84
|
-
}
|
|
76
|
+
const [state, actions] = useStateFactory(() => new CounterStore(), []);
|
|
85
77
|
```
|
|
86
78
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return (
|
|
97
|
-
<div>
|
|
98
|
-
<h2>Counter: {state}</h2>
|
|
99
|
-
<button onClick={actions.increase}>Increase</button>
|
|
100
|
-
<button onClick={actions.decrease}>Decrease</button>
|
|
101
|
-
</div>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
```
|
|
79
|
+
## Handlers
|
|
80
|
+
|
|
81
|
+
StatusQuo provides two handler implementations with the same public interface:
|
|
82
|
+
|
|
83
|
+
- `ObservableStateHandler` (RxJS-backed)
|
|
84
|
+
- `SignalStateHandler` (Signals-backed)
|
|
85
|
+
|
|
86
|
+
Both are built on `BaseStateHandler`, which provides the shared lifecycle and devtools support.
|
|
105
87
|
|
|
106
|
-
|
|
88
|
+
## Hooks
|
|
107
89
|
|
|
108
|
-
|
|
90
|
+
- `useStateFactory(factory, deps)`
|
|
91
|
+
- Creates a handler instance per component and subscribes to its snapshot.
|
|
92
|
+
- Suitable for per-component or per-instance state.
|
|
93
|
+
- `useStateSingleton(singleton)`
|
|
94
|
+
- Uses a shared singleton handler across components.
|
|
95
|
+
|
|
96
|
+
## Singletons
|
|
109
97
|
|
|
110
98
|
```ts
|
|
111
|
-
import { makeStateSingleton } from '@veams/status-quo';
|
|
99
|
+
import { makeStateSingleton, useStateSingleton } from '@veams/status-quo';
|
|
112
100
|
|
|
113
|
-
|
|
101
|
+
const CounterSingleton = makeStateSingleton(() => new CounterStore());
|
|
114
102
|
|
|
115
|
-
|
|
103
|
+
const [state, actions] = useStateSingleton(CounterSingleton);
|
|
116
104
|
```
|
|
117
105
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
106
|
+
## Composition
|
|
107
|
+
|
|
108
|
+
Use only the slice you need. RxJS makes multi-source composition powerful and declarative with operators like `combineLatest`, `switchMap`, or `debounceTime`. Signals can derive values with `computed` and wire them into a parent store via `bindSubscribable`.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { combineLatest } from 'rxjs';
|
|
112
|
+
|
|
113
|
+
// RxJS: combine handler streams (RxJS shines here)
|
|
114
|
+
class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
|
|
115
|
+
private counter$ = CounterObservableStore.getInstance().getStateAsObservable();
|
|
116
|
+
private card$ = new CardObservableHandler();
|
|
117
|
+
|
|
118
|
+
constructor() {
|
|
119
|
+
super({ initialState: { counter: 0, cardTitle: '' }});
|
|
120
|
+
|
|
121
|
+
this.subscriptions.push(
|
|
122
|
+
combineLatest([
|
|
123
|
+
this.counter$,
|
|
124
|
+
this.card$,
|
|
125
|
+
]).subscribe(([counterState, cardState]) => {
|
|
126
|
+
this.setState({
|
|
127
|
+
counter: counterState,
|
|
128
|
+
cardTitle: cardState.title,
|
|
129
|
+
}, 'sync-combined');
|
|
130
|
+
})
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
// Signals: combine derived values via computed + bindSubscribable
|
|
137
|
+
import { computed } from '@preact/signals-core';
|
|
138
|
+
|
|
139
|
+
class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
|
|
140
|
+
private counter = CounterSignalHandler.getInstance();
|
|
141
|
+
private card = new CardSignalHandler();
|
|
142
|
+
private combined$ = computed(() => ({
|
|
143
|
+
counter: this.counter.getSignal().value,
|
|
144
|
+
cardTitle: this.card.getSignal().value.title,
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
constructor() {
|
|
148
|
+
super({ initialState: { counter: 0, cardTitle: '' }});
|
|
149
|
+
|
|
150
|
+
this.bindSubscribable(
|
|
151
|
+
{ subscribe: this.combined.subscribe.bind(this.combined), getSnapshot: () => this.combined.value },
|
|
152
|
+
(nextState) => this.setState(nextState, 'sync-combined')
|
|
153
|
+
);
|
|
154
|
+
}
|
|
141
155
|
}
|
|
142
156
|
```
|
|
143
157
|
|
|
144
|
-
|
|
158
|
+
## Devtools
|
|
145
159
|
|
|
146
|
-
|
|
147
|
-
You can enable the devtools in an easy way:
|
|
160
|
+
Enable Redux Devtools integration with `options.devTools`:
|
|
148
161
|
|
|
149
162
|
```ts
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
constructor([startCount = 0]) {
|
|
163
|
+
class CounterStore extends ObservableStateHandler<CounterState, CounterActions> {
|
|
164
|
+
constructor() {
|
|
153
165
|
super({
|
|
154
|
-
initialState: { count:
|
|
155
|
-
options: {
|
|
156
|
-
devTools: { enabled: true, namespace: 'Counter' },
|
|
157
|
-
},
|
|
166
|
+
initialState: { count: 0 },
|
|
167
|
+
options: { devTools: { enabled: true, namespace: 'Counter' } },
|
|
158
168
|
});
|
|
159
169
|
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
160
172
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
173
|
+
## Cleanup
|
|
174
|
+
|
|
175
|
+
Handlers expose `subscribe`, `getSnapshot`, and `destroy` for custom integrations:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const unsubscribe = store.subscribe(() => {
|
|
179
|
+
console.log(store.getSnapshot());
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
unsubscribe();
|
|
183
|
+
store.destroy();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## API Reference
|
|
187
|
+
|
|
188
|
+
### `StateSubscriptionHandler<V, A>`
|
|
189
|
+
|
|
190
|
+
Required interface implemented by all handlers.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
interface StateSubscriptionHandler<V, A> {
|
|
194
|
+
subscribe: (listener: () => void) => () => void;
|
|
195
|
+
getSnapshot: () => V;
|
|
196
|
+
destroy: () => void;
|
|
197
|
+
getInitialState: () => V;
|
|
198
|
+
getActions: () => A;
|
|
185
199
|
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### `BaseStateHandler<S, A>`
|
|
203
|
+
|
|
204
|
+
Shared base class for all handlers.
|
|
205
|
+
|
|
206
|
+
Constructor:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
protected constructor(initialState: S)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Public methods:
|
|
213
|
+
|
|
214
|
+
- `getInitialState(): S`
|
|
215
|
+
- `getState(): S`
|
|
216
|
+
- `getSnapshot(): S`
|
|
217
|
+
- `setState(next: Partial<S>, actionName = 'change'): void`
|
|
218
|
+
- `subscribe(listener: () => void): () => void` (abstract)
|
|
219
|
+
- `destroy(): void`
|
|
220
|
+
- `getActions(): A` (abstract)
|
|
221
|
+
|
|
222
|
+
Protected helpers:
|
|
223
|
+
|
|
224
|
+
- `getStateValue(): S` (abstract)
|
|
225
|
+
- `setStateValue(next: S): void` (abstract)
|
|
226
|
+
- `initDevTools(options?: { enabled?: boolean; namespace: string }): void`
|
|
227
|
+
- `bindSubscribable<T>(service: { subscribe: (listener: (value: T) => void) => () => void; getSnapshot?: () => T }, onChange: (value: T) => void): void`
|
|
228
|
+
- Registers the subscription on `this.subscriptions` and invokes `onChange` with the current snapshot when available.
|
|
229
|
+
|
|
230
|
+
### `ObservableStateHandler<S, A>`
|
|
231
|
+
|
|
232
|
+
RxJS-backed handler. Extends `BaseStateHandler`.
|
|
233
|
+
|
|
234
|
+
Constructor:
|
|
186
235
|
|
|
187
|
-
|
|
188
|
-
|
|
236
|
+
```ts
|
|
237
|
+
protected constructor({
|
|
238
|
+
initialState,
|
|
239
|
+
options
|
|
240
|
+
}: {
|
|
241
|
+
initialState: S;
|
|
242
|
+
options?: {
|
|
243
|
+
devTools?: { enabled?: boolean; namespace: string };
|
|
244
|
+
};
|
|
245
|
+
})
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Public methods:
|
|
249
|
+
|
|
250
|
+
- `getStateAsObservable(options?: { useDistinctUntilChanged?: boolean }): Observable<S>`
|
|
251
|
+
- `getStateItemAsObservable(key: keyof S): Observable<S[keyof S]>`
|
|
252
|
+
- `getObservableItem(key: keyof S): Observable<S[keyof S]>`
|
|
253
|
+
- `subscribe(listener: () => void): () => void`
|
|
254
|
+
|
|
255
|
+
Notes:
|
|
256
|
+
- The observable stream uses `distinctUntilChanged` by default (JSON compare).
|
|
257
|
+
- `subscribe` does not fire for the initial value; it only fires on subsequent changes.
|
|
258
|
+
|
|
259
|
+
### `SignalStateHandler<S, A>`
|
|
260
|
+
|
|
261
|
+
Signals-backed handler. Extends `BaseStateHandler`.
|
|
262
|
+
|
|
263
|
+
Constructor:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
protected constructor({
|
|
267
|
+
initialState,
|
|
268
|
+
options
|
|
269
|
+
}: {
|
|
270
|
+
initialState: S;
|
|
271
|
+
options?: {
|
|
272
|
+
devTools?: { enabled?: boolean; namespace: string };
|
|
273
|
+
useDistinctUntilChanged?: boolean;
|
|
274
|
+
};
|
|
275
|
+
})
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Public methods:
|
|
279
|
+
|
|
280
|
+
- `getSignal(): Signal<S>`
|
|
281
|
+
- `subscribe(listener: () => void): () => void`
|
|
282
|
+
|
|
283
|
+
Notes:
|
|
284
|
+
- `useDistinctUntilChanged` defaults to `true` (JSON compare).
|
|
285
|
+
|
|
286
|
+
### `makeStateSingleton`
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
function makeStateSingleton<S, A>(
|
|
290
|
+
factory: () => StateSubscriptionHandler<S, A>
|
|
291
|
+
): {
|
|
292
|
+
getInstance: () => StateSubscriptionHandler<S, A>;
|
|
189
293
|
}
|
|
190
294
|
```
|
|
191
295
|
|
|
192
|
-
|
|
193
|
-
|
|
296
|
+
### Hooks
|
|
297
|
+
|
|
298
|
+
- `useStateFactory<V, A, P extends unknown[]>(factory: (...args: P) => StateSubscriptionHandler<V, A>, params?: P)`
|
|
299
|
+
- Returns `[state, actions]`.
|
|
300
|
+
- `useStateSingleton<V, A>(singleton: StateSingleton<V, A>)`
|
|
301
|
+
- Returns `[state, actions]`.
|
|
302
|
+
|
|
303
|
+
## Migration
|
|
304
|
+
|
|
305
|
+
From pre-1.0 releases:
|
|
306
|
+
|
|
307
|
+
1. Rename `StateHandler` -> `ObservableStateHandler`.
|
|
308
|
+
2. Implement `subscribe()` and `getSnapshot()` on custom handlers.
|
|
309
|
+
3. Replace `getObservable()` usage with `subscribe()` in custom integrations.
|
|
310
|
+
4. Update devtools config:
|
|
311
|
+
- From: `super({ initialState, devTools: { ... } })`
|
|
312
|
+
- To: `super({ initialState, options: { devTools: { ... } } })`
|
|
Binary file
|
|
@@ -19,6 +19,10 @@ export declare abstract class BaseStateHandler<S, A> implements StateSubscriptio
|
|
|
19
19
|
destroy(): void;
|
|
20
20
|
protected abstract getStateValue(): S;
|
|
21
21
|
protected abstract setStateValue(nextState: S): void;
|
|
22
|
+
protected bindSubscribable<T>(service: {
|
|
23
|
+
subscribe: (listener: (value: T) => void) => () => void;
|
|
24
|
+
getSnapshot?: () => T;
|
|
25
|
+
}, onChange: (value: T) => void): void;
|
|
22
26
|
abstract subscribe(listener: () => void): () => void;
|
|
23
27
|
abstract getActions(): A;
|
|
24
28
|
private handleDevToolsEvents;
|
|
@@ -53,6 +53,12 @@ export class BaseStateHandler {
|
|
|
53
53
|
destroy() {
|
|
54
54
|
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
|
|
55
55
|
}
|
|
56
|
+
bindSubscribable(service, onChange) {
|
|
57
|
+
const unsubscribe = service.subscribe(onChange);
|
|
58
|
+
this.subscriptions = [...(this.subscriptions ?? []), { unsubscribe }];
|
|
59
|
+
if (service.getSnapshot)
|
|
60
|
+
onChange(service.getSnapshot());
|
|
61
|
+
}
|
|
56
62
|
handleDevToolsEvents = (message) => {
|
|
57
63
|
if (message.type !== 'DISPATCH') {
|
|
58
64
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-state-handler.js","sourceRoot":"","sources":["../../src/store/base-state-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAU9C,MAAM,sBAAsB,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;AAEtE,MAAM,gBAAgB,GAAG;IACvB,KAAK,EAAE,IAAI;IACX,IAAI,EAAE,IAAI;IACV,OAAO,EAAE,KAAK;IACd,MAAM,EAAE,IAAI;IACZ,MAAM,EAAE,QAAQ;IAChB,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,IAAI;IACV,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,KAAK;IACf,IAAI,EAAE,KAAK;CACZ,CAAC;AAEF,MAAM,OAAgB,gBAAgB;IACjB,YAAY,CAAI;IACzB,QAAQ,GAAoB,IAAI,CAAC;IAE3C,aAAa,GAAuC,EAAE,CAAC;IAEvD,YAAsB,YAAe;QACnC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACnC,CAAC;IAES,YAAY,CAAC,eAAiC;QACtD,MAAM,aAAa,GAAG;YACpB,GAAG,sBAAsB;YACzB,GAAG,eAAe;SACnB,CAAC;QAEF,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;YAC3B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,OAAO;QACT,CAAC;QAED,IAAI,CAAC,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,YAAY,EAAE;YAC9C,IAAI,EAAE,aAAa,CAAC,SAAS;YAC7B,UAAU,EAAE,aAAa,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;YACtE,cAAc,EAAE,IAAI,CAAC,UAAU,EAAE;YACjC,QAAQ,EAAE,gBAAgB;SAC3B,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;IACtD,CAAC;IAED,eAAe;QACb,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC;IAC9B,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC;IACzB,CAAC;IAED,QAAQ,CAAC,QAAoB,EAAE,UAAU,GAAG,QAAQ;QAClD,MAAM,SAAS,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,QAAQ,EAAE,CAAC;QACtD,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;QAC9B,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAC7C,CAAC;IAED,OAAO;QACL,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC,CAAC;IAC3E,CAAC;
|
|
1
|
+
{"version":3,"file":"base-state-handler.js","sourceRoot":"","sources":["../../src/store/base-state-handler.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/playground/src/App.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import 'prismjs/components/prism-bash';
|
|
|
16
16
|
import philosophySwap from './assets/philosophy-swap.svg';
|
|
17
17
|
import philosophySeparation from './assets/philosophy-separation.svg';
|
|
18
18
|
import philosophyAgnostic from './assets/philosophy-agnostic.svg';
|
|
19
|
+
import statusQuoLogo from './assets/statusquo-logo.png';
|
|
19
20
|
|
|
20
21
|
type CounterState = {
|
|
21
22
|
count: number;
|
|
@@ -227,48 +228,49 @@ const CounterSingleton = makeStateSingleton(() => new CounterStore());
|
|
|
227
228
|
|
|
228
229
|
const [state, actions] = useStateSingleton(CounterSingleton);`;
|
|
229
230
|
|
|
230
|
-
const composeSnippet =
|
|
231
|
-
class UserStore extends ObservableStateHandler<UserState, UserActions> { /* ... */ }
|
|
232
|
-
class CartStore extends ObservableStateHandler<CartState, CartActions> { /* ... */ }
|
|
231
|
+
const composeSnippet = `import { combineLatest } from "rxjs";
|
|
233
232
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
private
|
|
233
|
+
// RxJS: combine handler streams (RxJS shines here)
|
|
234
|
+
class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
|
|
235
|
+
private counter$ = CounterObservableStore.getInstance().getStateAsObservable();
|
|
236
|
+
private card$ = new CardObservableHandler();
|
|
237
237
|
|
|
238
238
|
constructor() {
|
|
239
|
-
super({ initialState: {
|
|
240
|
-
|
|
241
|
-
this.subscriptions
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
239
|
+
super({ initialState: { counter: 0, cardTitle: "" }});
|
|
240
|
+
|
|
241
|
+
this.subscriptions.push(
|
|
242
|
+
combineLatest([
|
|
243
|
+
this.counter$,
|
|
244
|
+
this.card$,
|
|
245
|
+
]).subscribe(([counterState, cardState]) => {
|
|
246
|
+
this.setState({
|
|
247
|
+
counter: counterState,
|
|
248
|
+
cardTitle: cardState.title,
|
|
249
|
+
}, "sync-combined");
|
|
250
|
+
})
|
|
251
|
+
)
|
|
249
252
|
}
|
|
250
253
|
|
|
251
|
-
private syncCart() {
|
|
252
|
-
this.setState({ cart: this.cart.getState() }, 'sync-cart');
|
|
253
|
-
}
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
//
|
|
257
|
-
import { computed } from
|
|
258
|
-
|
|
259
|
-
class UserSignalStore extends SignalStateHandler<UserState, UserActions> { /* ... */ }
|
|
256
|
+
// Signals: combine derived values via computed + bindSubscribable
|
|
257
|
+
import { computed } from "@preact/signals-core";
|
|
260
258
|
|
|
261
259
|
class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
|
|
262
|
-
private
|
|
263
|
-
private
|
|
260
|
+
private counter = CounterSignalHandler.getInstance().getSignal();
|
|
261
|
+
private card = new CardSignalHandler();
|
|
262
|
+
private combined = computed(() => ({
|
|
263
|
+
counter: this.counter.getSignal().value,
|
|
264
|
+
cardTitle: this.card.getSignal().value.title,
|
|
265
|
+
}));
|
|
264
266
|
|
|
265
267
|
constructor() {
|
|
266
|
-
super({ initialState: {
|
|
267
|
-
this.subscriptions = [{ unsubscribe: this.user.subscribe(() => this.sync()) }];
|
|
268
|
-
}
|
|
268
|
+
super({ initialState: { counter: 0, cardTitle: "" }});
|
|
269
269
|
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
this.bindSubscribable(
|
|
271
|
+
{ subscribe: this.combined.subscribe.bind(this.combined), getSnapshot: () => this.combined.value },
|
|
272
|
+
(nextState) => this.setState(nextState, "sync-combined")
|
|
273
|
+
);
|
|
272
274
|
}
|
|
273
275
|
}`;
|
|
274
276
|
|
|
@@ -279,8 +281,7 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
|
|
|
279
281
|
return (
|
|
280
282
|
<div className="app">
|
|
281
283
|
<div className="brand-bar">
|
|
282
|
-
<
|
|
283
|
-
<span>Status Quo Demo</span>
|
|
284
|
+
<img src={statusQuoLogo} alt="StatusQuo logo" className="brand-logo" />
|
|
284
285
|
</div>
|
|
285
286
|
<nav className="nav">
|
|
286
287
|
<div className="nav-links">
|
|
@@ -300,7 +301,7 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
|
|
|
300
301
|
<p className="eyebrow">Philosophy</p>
|
|
301
302
|
<h1>State management that stays out of your way</h1>
|
|
302
303
|
<p className="subtext">
|
|
303
|
-
|
|
304
|
+
<span>StatusQuo</span> treats state handlers as small, composable objects with explicit
|
|
304
305
|
lifecycle and a tiny interface. Components subscribe to snapshots, not
|
|
305
306
|
framework‑specific store APIs. That makes it easy to swap the engine under the
|
|
306
307
|
hood—RxJS for observable streams or Preact Signals for ultra‑light reactive state.
|
|
@@ -433,9 +434,10 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
|
|
|
433
434
|
<p className="eyebrow">Compose</p>
|
|
434
435
|
<h2>Combine multiple handlers</h2>
|
|
435
436
|
<p>
|
|
436
|
-
Use only the slice you need.
|
|
437
|
-
|
|
438
|
-
|
|
437
|
+
Use only the slice you need. RxJS makes multi-source composition powerful and
|
|
438
|
+
declarative with operators like `combineLatest`, `switchMap`, or `debounceTime`.
|
|
439
|
+
Signals can derive values with `computed` and wire them into a parent store via
|
|
440
|
+
`bindSubscribable`. This keeps parent stores lean and focused.
|
|
439
441
|
</p>
|
|
440
442
|
</div>
|
|
441
443
|
<pre className="code-block">
|
|
Binary file
|
|
@@ -54,12 +54,10 @@ body {
|
|
|
54
54
|
width: 100%;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
.brand-
|
|
58
|
-
width:
|
|
59
|
-
height:
|
|
60
|
-
|
|
61
|
-
background: var(--accent);
|
|
62
|
-
box-shadow: 0 0 0 4px rgba(47, 107, 255, 0.2);
|
|
57
|
+
.brand-logo {
|
|
58
|
+
width: 260px;
|
|
59
|
+
height: auto;
|
|
60
|
+
display: block;
|
|
63
61
|
}
|
|
64
62
|
|
|
65
63
|
.nav {
|
|
@@ -130,6 +128,12 @@ p {
|
|
|
130
128
|
margin: 0;
|
|
131
129
|
color: var(--muted);
|
|
132
130
|
line-height: 1.6;
|
|
131
|
+
|
|
132
|
+
span {
|
|
133
|
+
font-family: monospace;
|
|
134
|
+
font-weight: 700;
|
|
135
|
+
font-size: .9rem;
|
|
136
|
+
}
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
.muted {
|
|
@@ -78,6 +78,15 @@ export abstract class BaseStateHandler<S, A> implements StateSubscriptionHandler
|
|
|
78
78
|
|
|
79
79
|
protected abstract getStateValue(): S;
|
|
80
80
|
protected abstract setStateValue(nextState: S): void;
|
|
81
|
+
protected bindSubscribable<T>(
|
|
82
|
+
service: { subscribe: (listener: (value: T) => void) => () => void; getSnapshot?: () => T },
|
|
83
|
+
onChange: (value: T) => void
|
|
84
|
+
) {
|
|
85
|
+
const unsubscribe = service.subscribe(onChange);
|
|
86
|
+
this.subscriptions = [...(this.subscriptions ?? []), { unsubscribe }];
|
|
87
|
+
|
|
88
|
+
if (service.getSnapshot) onChange(service.getSnapshot());
|
|
89
|
+
}
|
|
81
90
|
|
|
82
91
|
abstract subscribe(listener: () => void): () => void;
|
|
83
92
|
abstract getActions(): A;
|