@coherent.js/state 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/dist/index.js +6 -0
- package/dist/index.js.map +2 -2
- package/package.json +4 -3
- package/types/index.d.ts +520 -316
package/types/index.d.ts
CHANGED
|
@@ -3,256 +3,261 @@
|
|
|
3
3
|
* @module @coherent.js/state
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { CoherentNode, ComponentState } from '@coherent.js/core';
|
|
7
|
-
|
|
8
6
|
// ============================================================================
|
|
9
|
-
//
|
|
7
|
+
// Reactive State
|
|
10
8
|
// ============================================================================
|
|
11
9
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
setState(partial: Partial<T> | ((state: T) => Partial<T>)): void;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Subscribe to state changes
|
|
29
|
-
* @returns Unsubscribe function
|
|
30
|
-
*/
|
|
31
|
-
subscribe(listener: (state: T, prevState: T) => void): () => void;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Destroy the store and clean up subscriptions
|
|
35
|
-
*/
|
|
36
|
-
destroy(): void;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Store creation options
|
|
41
|
-
* @template T - The shape of the state object
|
|
42
|
-
*/
|
|
43
|
-
export interface StoreOptions<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
44
|
-
/** Initial state */
|
|
45
|
-
initialState: T;
|
|
46
|
-
/** Persistence configuration */
|
|
47
|
-
persist?: {
|
|
48
|
-
/** Storage key */
|
|
49
|
-
key: string;
|
|
50
|
-
/** Storage adapter (localStorage, sessionStorage, or custom) */
|
|
51
|
-
storage?: Storage;
|
|
52
|
-
/** Custom serialization */
|
|
53
|
-
serialize?: (state: T) => string;
|
|
54
|
-
/** Custom deserialization */
|
|
55
|
-
deserialize?: (value: string) => T;
|
|
56
|
-
/** Debounce persistence writes (ms) */
|
|
57
|
-
debounce?: number;
|
|
58
|
-
};
|
|
59
|
-
/** Enable devtools integration */
|
|
60
|
-
devtools?: boolean;
|
|
61
|
-
/** Store name for debugging */
|
|
62
|
-
name?: string;
|
|
63
|
-
/** Middleware functions */
|
|
64
|
-
middleware?: Array<StoreMiddleware<T>>;
|
|
10
|
+
/** Called with the new and previous value; `unwatch` stops further calls. */
|
|
11
|
+
export type Watcher<T = unknown> = (
|
|
12
|
+
newValue: T,
|
|
13
|
+
oldValue: T | undefined,
|
|
14
|
+
unwatch: () => void
|
|
15
|
+
) => void;
|
|
16
|
+
|
|
17
|
+
export interface ObservableOptions {
|
|
18
|
+
/** Notify even when the new value is `===` the old one; defaults to `true` */
|
|
19
|
+
deep?: boolean;
|
|
20
|
+
/** Invoke watchers on subscribe; defaults to `true` */
|
|
21
|
+
immediate?: boolean;
|
|
22
|
+
[option: string]: unknown;
|
|
65
23
|
}
|
|
66
24
|
|
|
67
25
|
/**
|
|
68
|
-
*
|
|
26
|
+
* A single reactive value.
|
|
27
|
+
*
|
|
28
|
+
* Read and write through the `value` accessor — assigning notifies watchers
|
|
29
|
+
* and invalidates any computed that read it.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* const count = observable(0);
|
|
33
|
+
* count.watch(next => console.log(next));
|
|
34
|
+
* count.value = 1;
|
|
35
|
+
* ```
|
|
69
36
|
*/
|
|
70
|
-
export
|
|
71
|
-
|
|
72
|
-
action: string,
|
|
73
|
-
payload?: unknown
|
|
74
|
-
) => T | void;
|
|
75
|
-
|
|
76
|
-
// ============================================================================
|
|
77
|
-
// Selector Types
|
|
78
|
-
// ============================================================================
|
|
37
|
+
export class Observable<T = unknown> {
|
|
38
|
+
constructor(value: T, options?: ObservableOptions);
|
|
79
39
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
40
|
+
/** The current value; assigning notifies watchers */
|
|
41
|
+
get value(): T;
|
|
42
|
+
set value(newValue: T);
|
|
83
43
|
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
* @template T - Store state type
|
|
87
|
-
* @template P - Payload type (void for no payload)
|
|
88
|
-
*/
|
|
89
|
-
export type Action<T, P = void> = P extends void
|
|
90
|
-
? () => Partial<T>
|
|
91
|
-
: (payload: P) => Partial<T>;
|
|
44
|
+
/** Subscribe to changes; returns an unwatch function */
|
|
45
|
+
watch(callback: Watcher<T>, options?: { immediate?: boolean }): () => void;
|
|
92
46
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// ============================================================================
|
|
47
|
+
/** Remove one observer */
|
|
48
|
+
unwatch(observer: (newValue: T, oldValue: T | undefined) => void): void;
|
|
96
49
|
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
*/
|
|
100
|
-
export interface Observer<T = unknown> {
|
|
101
|
-
(value: T, oldValue: T): void;
|
|
50
|
+
/** Remove every observer and computed dependent */
|
|
51
|
+
unwatchAll(): void;
|
|
102
52
|
}
|
|
103
53
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
constructor(initialValue: T);
|
|
109
|
-
|
|
110
|
-
/** Get current value */
|
|
111
|
-
get(): T;
|
|
54
|
+
/** Raised by the reactive primitives. */
|
|
55
|
+
export class StateError extends Error {
|
|
56
|
+
constructor(message: string, options?: Record<string, unknown>);
|
|
57
|
+
}
|
|
112
58
|
|
|
113
|
-
|
|
114
|
-
|
|
59
|
+
/** Sink for errors thrown inside watchers and computed getters. */
|
|
60
|
+
export const globalErrorHandler: {
|
|
61
|
+
handle(error: unknown, context?: Record<string, unknown>): void;
|
|
62
|
+
};
|
|
115
63
|
|
|
116
|
-
|
|
117
|
-
|
|
64
|
+
export interface ReactiveStateOptions extends ObservableOptions {
|
|
65
|
+
/** Run registered middleware on `set()` */
|
|
66
|
+
enableMiddleware?: boolean;
|
|
67
|
+
/** Record mutations so `undo()` and `getHistory()` work */
|
|
68
|
+
enableHistory?: boolean;
|
|
69
|
+
/** Cap on retained history entries */
|
|
70
|
+
maxHistorySize?: number;
|
|
71
|
+
}
|
|
118
72
|
|
|
119
|
-
|
|
120
|
-
|
|
73
|
+
/** What `set` middleware may return to alter or veto a write. */
|
|
74
|
+
export interface MiddlewareResult {
|
|
75
|
+
cancelled?: boolean;
|
|
76
|
+
value?: unknown;
|
|
77
|
+
}
|
|
121
78
|
|
|
122
|
-
|
|
123
|
-
|
|
79
|
+
/** Payload handed to a `subscribe()` listener. */
|
|
80
|
+
export interface StateChange<T = unknown> {
|
|
81
|
+
key: string;
|
|
82
|
+
newValue: T;
|
|
83
|
+
oldValue: T | undefined;
|
|
84
|
+
state: Record<string, unknown>;
|
|
124
85
|
}
|
|
125
86
|
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
computed?: Record<string, (state: T) => unknown>;
|
|
134
|
-
/** Property watchers */
|
|
135
|
-
watchers?: Record<string, Observer<unknown>>;
|
|
136
|
-
/** Middleware functions */
|
|
137
|
-
middleware?: Array<(state: T, action: string, payload?: unknown) => T | void>;
|
|
87
|
+
/** One recorded mutation. */
|
|
88
|
+
export interface HistoryEntry {
|
|
89
|
+
action: 'set' | 'delete' | 'clear' | 'batch';
|
|
90
|
+
key: string | null;
|
|
91
|
+
oldValue: unknown;
|
|
92
|
+
newValue: unknown;
|
|
93
|
+
timestamp?: number;
|
|
138
94
|
}
|
|
139
95
|
|
|
140
96
|
/**
|
|
141
|
-
*
|
|
97
|
+
* A keyed collection of observables, with computed properties, watchers,
|
|
98
|
+
* middleware and optional undo history.
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* const state = createReactiveState({ count: 0 });
|
|
102
|
+
* state.computed('doubled', () => state.get('count') * 2);
|
|
103
|
+
* state.watch('count', next => console.log(next));
|
|
104
|
+
* state.set('count', 1);
|
|
105
|
+
* ```
|
|
142
106
|
*/
|
|
143
|
-
export class ReactiveState
|
|
144
|
-
constructor(options
|
|
107
|
+
export class ReactiveState {
|
|
108
|
+
constructor(initialState?: Record<string, unknown>, options?: ReactiveStateOptions);
|
|
145
109
|
|
|
146
|
-
/**
|
|
147
|
-
get<
|
|
110
|
+
/** Current value of a key, or `undefined` */
|
|
111
|
+
get<T = unknown>(key: string): T | undefined;
|
|
148
112
|
|
|
149
|
-
/**
|
|
150
|
-
set
|
|
113
|
+
/** Write a key; `false` when middleware cancelled the write */
|
|
114
|
+
set(key: string, value: unknown, options?: ReactiveStateOptions): boolean;
|
|
151
115
|
|
|
152
|
-
/**
|
|
153
|
-
|
|
116
|
+
/** Whether the key exists */
|
|
117
|
+
has(key: string): boolean;
|
|
154
118
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
119
|
+
/** Drop a key and its watchers; `false` when it was absent */
|
|
120
|
+
delete(key: string): boolean;
|
|
157
121
|
|
|
158
|
-
/**
|
|
159
|
-
|
|
122
|
+
/** Drop every key */
|
|
123
|
+
clear(): void;
|
|
160
124
|
|
|
161
|
-
/**
|
|
162
|
-
|
|
125
|
+
/** Define a computed property derived from other keys */
|
|
126
|
+
computed(key: string, getter: () => unknown, options?: ObservableOptions): void;
|
|
163
127
|
|
|
164
|
-
/**
|
|
165
|
-
|
|
128
|
+
/** Current value of a computed property, or `undefined` */
|
|
129
|
+
getComputed<T = unknown>(key: string): T | undefined;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Watch one key, or a getter expression. Returns an unwatch function, and
|
|
133
|
+
* throws {@link StateError} for a key that does not exist.
|
|
134
|
+
*/
|
|
135
|
+
watch<T = unknown>(
|
|
136
|
+
key: string | (() => T),
|
|
137
|
+
callback: Watcher<T>,
|
|
138
|
+
options?: { immediate?: boolean }
|
|
139
|
+
): () => void;
|
|
140
|
+
|
|
141
|
+
/** Apply several writes with history suppressed until the batch ends */
|
|
142
|
+
batch<T>(updates: ((state: this) => T) | Record<string, unknown>): T | undefined;
|
|
143
|
+
|
|
144
|
+
/** Subscribe to one or more keys; returns an unsubscribe function */
|
|
145
|
+
subscribe(
|
|
146
|
+
keys: string | string[],
|
|
147
|
+
callback: (change: StateChange) => void,
|
|
148
|
+
options?: { immediate?: boolean }
|
|
149
|
+
): () => void;
|
|
150
|
+
|
|
151
|
+
/** Register middleware consulted on every `set()` */
|
|
152
|
+
use(middleware: (action: string, context: Record<string, unknown>) => MiddlewareResult | void): void;
|
|
153
|
+
|
|
154
|
+
/** Most recent mutations, newest first */
|
|
155
|
+
getHistory(limit?: number): HistoryEntry[];
|
|
156
|
+
|
|
157
|
+
/** Revert the most recent mutation; `false` when history is empty */
|
|
158
|
+
undo(): boolean;
|
|
159
|
+
|
|
160
|
+
/** Snapshot every key as a plain object */
|
|
161
|
+
toObject(): Record<string, unknown>;
|
|
162
|
+
|
|
163
|
+
/** Snapshot every computed property as a plain object */
|
|
164
|
+
getComputedValues(): Record<string, unknown>;
|
|
165
|
+
|
|
166
|
+
/** Counts of keys, computeds, watchers, history entries and middleware */
|
|
167
|
+
getStats(): {
|
|
168
|
+
stateKeys: number;
|
|
169
|
+
computedKeys: number;
|
|
170
|
+
watcherKeys: number;
|
|
171
|
+
historyLength: number;
|
|
172
|
+
middlewareCount: number;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/** Release every watcher and drop all state */
|
|
176
|
+
destroy(): void;
|
|
166
177
|
}
|
|
167
178
|
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
): ReactiveState<T>;
|
|
179
|
+
/** Create a {@link ReactiveState}. */
|
|
180
|
+
export function createReactiveState(
|
|
181
|
+
initialState?: Record<string, unknown>,
|
|
182
|
+
options?: ReactiveStateOptions
|
|
183
|
+
): ReactiveState;
|
|
174
184
|
|
|
175
|
-
/**
|
|
176
|
-
|
|
177
|
-
*/
|
|
178
|
-
export function observable<T = unknown>(initialValue: T): Observable<T>;
|
|
185
|
+
/** Create an {@link Observable}. */
|
|
186
|
+
export function observable<T = unknown>(value: T, options?: ObservableOptions): Observable<T>;
|
|
179
187
|
|
|
180
188
|
/**
|
|
181
|
-
* Create a
|
|
189
|
+
* Create a read-only observable derived from other observables. Dependencies
|
|
190
|
+
* are tracked automatically; assigning to `value` throws {@link StateError}.
|
|
182
191
|
*/
|
|
183
192
|
export function computed<T = unknown>(
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
getter: () => T,
|
|
194
|
+
options?: ObservableOptions
|
|
186
195
|
): Observable<T>;
|
|
187
196
|
|
|
188
|
-
/**
|
|
189
|
-
|
|
190
|
-
|
|
197
|
+
/** An observable with a `toggle()` helper. */
|
|
198
|
+
export type ToggleObservable = Observable<boolean> & { toggle(): void };
|
|
199
|
+
|
|
200
|
+
/** An observable with counter helpers. */
|
|
201
|
+
export type CounterObservable = Observable<number> & {
|
|
202
|
+
increment(by?: number): void;
|
|
203
|
+
decrement(by?: number): void;
|
|
204
|
+
reset(): void;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** An observable with immutable array helpers. */
|
|
208
|
+
export type ArrayObservable<T = unknown> = Observable<T[]> & {
|
|
209
|
+
push(...items: T[]): void;
|
|
210
|
+
pop(): T | undefined;
|
|
211
|
+
filter(predicate: (item: T, index: number, array: T[]) => boolean): void;
|
|
212
|
+
clear(): void;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/** Observable factories for common shapes. */
|
|
191
216
|
export const stateUtils: {
|
|
192
|
-
/**
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
|
|
217
|
+
/** A boolean observable that can flip itself */
|
|
218
|
+
toggle(initialValue?: boolean): ToggleObservable;
|
|
219
|
+
/** A numeric observable with increment/decrement/reset */
|
|
220
|
+
counter(initialValue?: number): CounterObservable;
|
|
221
|
+
/** An array observable whose helpers replace rather than mutate */
|
|
222
|
+
array<T = unknown>(initialArray?: T[]): ArrayObservable<T>;
|
|
223
|
+
/** A deeply reactive state container, not an observable */
|
|
224
|
+
object(initialObject?: Record<string, unknown>): ReactiveState;
|
|
200
225
|
};
|
|
201
226
|
|
|
202
227
|
// ============================================================================
|
|
203
|
-
// SSR
|
|
228
|
+
// SSR State Manager
|
|
204
229
|
// ============================================================================
|
|
205
230
|
|
|
206
|
-
/**
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Simple state interface
|
|
222
|
-
*/
|
|
223
|
-
export interface State<T = unknown> {
|
|
224
|
-
/** Get current state */
|
|
225
|
-
get(): T;
|
|
226
|
-
/** Set new state */
|
|
227
|
-
set(value: T): void;
|
|
228
|
-
/** Update with partial */
|
|
229
|
-
update(partial: Partial<T>): void;
|
|
230
|
-
/** Subscribe to changes */
|
|
231
|
-
subscribe(listener: (state: T) => void): () => void;
|
|
232
|
-
/** Reset to initial state */
|
|
233
|
-
reset(): void;
|
|
231
|
+
/** A per-render key/value store. */
|
|
232
|
+
export interface StateContainer {
|
|
233
|
+
get<T = unknown>(key: string): T | undefined;
|
|
234
|
+
/** Chainable */
|
|
235
|
+
set(key: string, value: unknown): StateContainer;
|
|
236
|
+
has(key: string): boolean;
|
|
237
|
+
/** `false` when the key was absent */
|
|
238
|
+
delete(key: string): boolean;
|
|
239
|
+
/** Chainable */
|
|
240
|
+
clear(): StateContainer;
|
|
241
|
+
/** Snapshot as a plain object */
|
|
242
|
+
toObject(): Record<string, unknown>;
|
|
234
243
|
}
|
|
235
244
|
|
|
236
245
|
/**
|
|
237
|
-
* Create a
|
|
246
|
+
* Create a state container scoped to one request or render.
|
|
238
247
|
*/
|
|
239
|
-
export function createState<
|
|
240
|
-
initialState: T,
|
|
241
|
-
options?: StateManagerOptions<T>
|
|
242
|
-
): State<T>;
|
|
248
|
+
export function createState(initialState?: Record<string, unknown>): StateContainer;
|
|
243
249
|
|
|
244
250
|
/**
|
|
245
|
-
*
|
|
251
|
+
* Process-wide store shared across renders. Prefer
|
|
252
|
+
* {@link StateContainer | request state} for anything request-specific.
|
|
246
253
|
*/
|
|
247
254
|
export const globalStateManager: {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
|
|
254
|
-
/** Clear state (optionally by key) */
|
|
255
|
-
clear(key?: string): void;
|
|
255
|
+
set(key: string, value: unknown): void;
|
|
256
|
+
get<T = unknown>(key: string): T | undefined;
|
|
257
|
+
has(key: string): boolean;
|
|
258
|
+
clear(): void;
|
|
259
|
+
/** A fresh container isolated from the global store */
|
|
260
|
+
createRequestState(): StateContainer;
|
|
256
261
|
};
|
|
257
262
|
|
|
258
263
|
// ============================================================================
|
|
@@ -260,163 +265,362 @@ export const globalStateManager: {
|
|
|
260
265
|
// ============================================================================
|
|
261
266
|
|
|
262
267
|
/**
|
|
263
|
-
*
|
|
264
|
-
|
|
265
|
-
export interface ContextValue<T = unknown> {
|
|
266
|
-
value: T;
|
|
267
|
-
subscribers: Set<(value: T) => void>;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Provide a context value
|
|
268
|
+
* Push a context value, remembering the previous one so
|
|
269
|
+
* {@link restoreContext} can unwind it.
|
|
272
270
|
*/
|
|
273
|
-
export function provideContext
|
|
271
|
+
export function provideContext(key: string, value: unknown): void;
|
|
274
272
|
|
|
275
273
|
/**
|
|
276
|
-
*
|
|
274
|
+
* Wrap children in a context. The returned function provides the context,
|
|
275
|
+
* renders, then restores the previous value.
|
|
277
276
|
*/
|
|
278
|
-
export function createContextProvider<
|
|
277
|
+
export function createContextProvider<C = unknown, R = unknown>(
|
|
279
278
|
key: string,
|
|
280
|
-
value:
|
|
281
|
-
|
|
279
|
+
value: unknown,
|
|
280
|
+
children: C
|
|
281
|
+
): (renderFunction?: (children: C) => R) => R | C;
|
|
282
282
|
|
|
283
|
-
/**
|
|
284
|
-
|
|
285
|
-
*/
|
|
286
|
-
export function useContext<T = unknown>(key: string, defaultValue?: T): T;
|
|
283
|
+
/** Pop one context value, restoring what {@link provideContext} replaced. */
|
|
284
|
+
export function restoreContext(key: string): void;
|
|
287
285
|
|
|
288
286
|
/**
|
|
289
|
-
*
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Clear all context stacks
|
|
287
|
+
* Unwind every tracked context to the value it held before its first
|
|
288
|
+
* `provideContext()`. Call between renders so one request's contexts do not
|
|
289
|
+
* leak into the next.
|
|
295
290
|
*/
|
|
296
291
|
export function clearAllContexts(): void;
|
|
297
292
|
|
|
293
|
+
/** Read the current context value, or `undefined`. */
|
|
294
|
+
export function useContext<T = unknown>(key: string): T | undefined;
|
|
295
|
+
|
|
298
296
|
// ============================================================================
|
|
299
297
|
// Persistent State
|
|
300
298
|
// ============================================================================
|
|
301
299
|
|
|
302
|
-
/**
|
|
303
|
-
|
|
304
|
-
|
|
300
|
+
/** Where persistent state is written. */
|
|
301
|
+
export type StorageKind = 'localStorage' | 'sessionStorage' | 'indexedDB' | 'memory';
|
|
302
|
+
|
|
303
|
+
/** Storage backend contract. */
|
|
305
304
|
export interface PersistenceAdapter {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
/** Remove item from storage */
|
|
311
|
-
removeItem(key: string): Promise<void> | void;
|
|
305
|
+
get(key: string): Promise<string | null> | string | null;
|
|
306
|
+
set(key: string, value: string): Promise<boolean> | boolean;
|
|
307
|
+
remove(key: string): Promise<boolean> | boolean;
|
|
308
|
+
clear(): Promise<boolean> | boolean;
|
|
312
309
|
}
|
|
313
310
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
serialize?: (state:
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
311
|
+
export interface PersistentStateOptions {
|
|
312
|
+
/** Backend; defaults to `'localStorage'` */
|
|
313
|
+
storage?: StorageKind;
|
|
314
|
+
/** Storage key; defaults to `'coherent-state'` */
|
|
315
|
+
key?: string;
|
|
316
|
+
/** Coalesce writes; defaults to `true` */
|
|
317
|
+
debounce?: boolean;
|
|
318
|
+
/** Debounce window in ms; defaults to `300` */
|
|
319
|
+
debounceDelay?: number;
|
|
320
|
+
serialize?: (state: Record<string, unknown>) => string;
|
|
321
|
+
deserialize?: (data: string) => Record<string, unknown>;
|
|
322
|
+
/** Persist only these keys */
|
|
323
|
+
include?: string[] | null;
|
|
324
|
+
/** Persist everything except these keys */
|
|
325
|
+
exclude?: string[] | null;
|
|
326
|
+
/** Obfuscate the payload with `encryptionKey` */
|
|
327
|
+
encrypt?: boolean;
|
|
328
|
+
encryptionKey?: string | null;
|
|
329
|
+
onSave?: ((state: Record<string, unknown>) => void) | null;
|
|
330
|
+
onLoad?: ((state: Record<string, unknown>) => void) | null;
|
|
331
|
+
onError?: ((error: unknown) => void) | null;
|
|
332
|
+
/** Tag stored payloads with `version` and run `migrate` on mismatch */
|
|
333
|
+
versioning?: boolean;
|
|
334
|
+
version?: string;
|
|
335
|
+
migrate?: ((state: Record<string, unknown>, from: string) => Record<string, unknown>) | null;
|
|
336
|
+
/** Discard stored state older than this many ms */
|
|
337
|
+
ttl?: number | null;
|
|
338
|
+
/** Mirror updates to other tabs over BroadcastChannel */
|
|
339
|
+
crossTab?: boolean;
|
|
328
340
|
}
|
|
329
341
|
|
|
330
|
-
/**
|
|
331
|
-
|
|
332
|
-
*/
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
342
|
+
/** A state container backed by storage. */
|
|
343
|
+
export interface PersistentState {
|
|
344
|
+
/** One key, or a copy of the whole state */
|
|
345
|
+
getState<T = unknown>(key: string): T | undefined;
|
|
346
|
+
getState(): Record<string, unknown>;
|
|
347
|
+
/** Merge updates, persisting unless `persist` is `false` */
|
|
348
|
+
setState(
|
|
349
|
+
updates:
|
|
350
|
+
| Record<string, unknown>
|
|
351
|
+
| ((state: Record<string, unknown>) => Record<string, unknown>),
|
|
352
|
+
persist?: boolean
|
|
353
|
+
): void;
|
|
354
|
+
/** Restore the initial values */
|
|
355
|
+
resetState(persist?: boolean): void;
|
|
356
|
+
/** Subscribe to changes; returns an unsubscribe function */
|
|
357
|
+
subscribe(
|
|
358
|
+
listener: (state: Record<string, unknown>, oldState: Record<string, unknown>) => void
|
|
359
|
+
): () => void;
|
|
360
|
+
/** Force an immediate write */
|
|
361
|
+
persist(): Promise<void>;
|
|
362
|
+
/** Reload from storage; `false` when nothing was stored */
|
|
363
|
+
restore(): Promise<boolean>;
|
|
364
|
+
/** Remove the stored payload */
|
|
365
|
+
clearStorage(): Promise<void>;
|
|
366
|
+
load(): Promise<Record<string, unknown> | null>;
|
|
367
|
+
save(): Promise<void>;
|
|
368
|
+
readonly adapter: PersistenceAdapter;
|
|
369
|
+
}
|
|
336
370
|
|
|
337
371
|
/**
|
|
338
|
-
*
|
|
372
|
+
* Create a state container that persists to storage. Unless the backend is
|
|
373
|
+
* `'memory'`, stored state is restored on creation.
|
|
339
374
|
*/
|
|
340
|
-
export function
|
|
375
|
+
export function createPersistentState(
|
|
376
|
+
initialState?: Record<string, unknown>,
|
|
377
|
+
options?: PersistentStateOptions
|
|
378
|
+
): PersistentState;
|
|
341
379
|
|
|
342
|
-
/**
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
380
|
+
/** {@link createPersistentState} backed by localStorage. */
|
|
381
|
+
export function withLocalStorage(
|
|
382
|
+
initialState?: Record<string, unknown>,
|
|
383
|
+
key?: string,
|
|
384
|
+
options?: PersistentStateOptions
|
|
385
|
+
): PersistentState;
|
|
346
386
|
|
|
347
|
-
/**
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
387
|
+
/** {@link createPersistentState} backed by sessionStorage. */
|
|
388
|
+
export function withSessionStorage(
|
|
389
|
+
initialState?: Record<string, unknown>,
|
|
390
|
+
key?: string,
|
|
391
|
+
options?: PersistentStateOptions
|
|
392
|
+
): PersistentState;
|
|
393
|
+
|
|
394
|
+
/** {@link createPersistentState} backed by IndexedDB. */
|
|
395
|
+
export function withIndexedDB(
|
|
396
|
+
initialState?: Record<string, unknown>,
|
|
397
|
+
key?: string,
|
|
398
|
+
options?: PersistentStateOptions
|
|
399
|
+
): PersistentState;
|
|
355
400
|
|
|
356
401
|
// ============================================================================
|
|
357
402
|
// Validated State
|
|
358
403
|
// ============================================================================
|
|
359
404
|
|
|
360
|
-
/**
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
405
|
+
/** JSON-Schema-style constraints. */
|
|
406
|
+
export interface StateSchema {
|
|
407
|
+
type?: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null';
|
|
408
|
+
properties?: Record<string, StateSchema>;
|
|
409
|
+
required?: string[];
|
|
410
|
+
enum?: unknown[];
|
|
411
|
+
minLength?: number;
|
|
412
|
+
maxLength?: number;
|
|
413
|
+
pattern?: string;
|
|
414
|
+
format?: 'email' | 'url' | 'uuid' | 'date' | 'date-time' | (string & {});
|
|
415
|
+
minimum?: number;
|
|
416
|
+
maximum?: number;
|
|
417
|
+
exclusiveMinimum?: number;
|
|
418
|
+
exclusiveMaximum?: number;
|
|
419
|
+
multipleOf?: number;
|
|
420
|
+
items?: StateSchema;
|
|
421
|
+
minItems?: number;
|
|
422
|
+
maxItems?: number;
|
|
423
|
+
uniqueItems?: boolean;
|
|
424
|
+
/** Custom check run after the built-in constraints */
|
|
425
|
+
validate?: (value: unknown) => boolean | string;
|
|
426
|
+
[keyword: string]: unknown;
|
|
365
427
|
}
|
|
366
428
|
|
|
367
|
-
/**
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
429
|
+
/** One validation failure. */
|
|
430
|
+
export interface ValidationError {
|
|
431
|
+
path: string;
|
|
432
|
+
message: string;
|
|
433
|
+
[detail: string]: unknown;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export interface ValidationResult {
|
|
437
|
+
valid: boolean;
|
|
438
|
+
errors: ValidationError[];
|
|
439
|
+
/** The value after coercion, when `coerce` is set */
|
|
440
|
+
value?: unknown;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Returns `true` when valid, or a message describing the failure. */
|
|
444
|
+
export type Validator<T = unknown> = (value: T) => boolean | string;
|
|
445
|
+
|
|
446
|
+
export interface ValidatedStateOptions {
|
|
447
|
+
/** Schema applied to the whole state, and per key via `properties` */
|
|
448
|
+
schema?: StateSchema | null;
|
|
449
|
+
/** Extra per-key validators */
|
|
450
|
+
validators?: Record<string, Validator | Validator[]>;
|
|
451
|
+
/** Reject writes that fail validation */
|
|
377
452
|
strict?: boolean;
|
|
453
|
+
/** Convert values to the declared type where possible */
|
|
454
|
+
coerce?: boolean;
|
|
455
|
+
onError?: ((errors: ValidationError[]) => void) | null;
|
|
456
|
+
/** Validate on write; defaults to `true` */
|
|
457
|
+
validateOnSet?: boolean;
|
|
458
|
+
/** Validate on read; defaults to `false` */
|
|
459
|
+
validateOnGet?: boolean;
|
|
460
|
+
required?: string[];
|
|
461
|
+
/** Permit keys the schema does not mention; defaults to `true` */
|
|
462
|
+
allowUnknown?: boolean;
|
|
378
463
|
}
|
|
379
464
|
|
|
380
|
-
/**
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
465
|
+
/** A state container that validates writes. */
|
|
466
|
+
export interface ValidatedState {
|
|
467
|
+
getState<T = unknown>(key: string): T | undefined;
|
|
468
|
+
getState(): Record<string, unknown>;
|
|
469
|
+
setState(
|
|
470
|
+
updates:
|
|
471
|
+
| Record<string, unknown>
|
|
472
|
+
| ((state: Record<string, unknown>) => Record<string, unknown>)
|
|
473
|
+
): void;
|
|
474
|
+
/** Subscribe to changes; returns an unsubscribe function */
|
|
475
|
+
subscribe(
|
|
476
|
+
listener: (state: Record<string, unknown>, oldState: Record<string, unknown>) => void
|
|
477
|
+
): () => void;
|
|
478
|
+
/** Current errors, keyed by path */
|
|
479
|
+
getErrors(): Record<string, ValidationError[]>;
|
|
388
480
|
isValid(): boolean;
|
|
389
|
-
/**
|
|
390
|
-
|
|
481
|
+
/** Validate one key */
|
|
482
|
+
validateField(key: string, value: unknown): ValidationResult;
|
|
483
|
+
/** Validate the whole state */
|
|
484
|
+
validate(): ValidationResult;
|
|
391
485
|
}
|
|
392
486
|
|
|
393
|
-
/**
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
): ValidatedState<T>;
|
|
487
|
+
/** Create a {@link ValidatedState}. */
|
|
488
|
+
export function createValidatedState(
|
|
489
|
+
initialState?: Record<string, unknown>,
|
|
490
|
+
options?: ValidatedStateOptions
|
|
491
|
+
): ValidatedState;
|
|
399
492
|
|
|
400
|
-
/**
|
|
401
|
-
* Built-in validators
|
|
402
|
-
*/
|
|
493
|
+
/** Ready-made validators, some of them parameterized factories. */
|
|
403
494
|
export const validators: {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
/** Minimum value */
|
|
411
|
-
min(value: number, message?: string): ValidationRule;
|
|
412
|
-
/** Maximum value */
|
|
413
|
-
max(value: number, message?: string): ValidationRule;
|
|
414
|
-
/** Pattern matching */
|
|
415
|
-
pattern(regex: RegExp, message?: string): ValidationRule;
|
|
416
|
-
/** Email format */
|
|
417
|
-
email(message?: string): ValidationRule;
|
|
418
|
-
/** URL format */
|
|
419
|
-
url(message?: string): ValidationRule;
|
|
420
|
-
/** Custom validator */
|
|
421
|
-
custom(fn: (value: unknown) => boolean | string): ValidationRule;
|
|
495
|
+
email: Validator<unknown>;
|
|
496
|
+
url: Validator<unknown>;
|
|
497
|
+
required: Validator<unknown>;
|
|
498
|
+
range(min: number, max: number): Validator<unknown>;
|
|
499
|
+
length(min: number, max: number): Validator<unknown>;
|
|
500
|
+
pattern(pattern: RegExp | string): Validator<unknown>;
|
|
422
501
|
};
|
|
502
|
+
|
|
503
|
+
// ============================================================================
|
|
504
|
+
// State Patterns
|
|
505
|
+
// ============================================================================
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* A field check for {@link FormState}. Returns the error message, or a falsy
|
|
509
|
+
* value when the field passes — the opposite of {@link Validator}, which
|
|
510
|
+
* returns `true` for valid.
|
|
511
|
+
*/
|
|
512
|
+
export type FieldCheck<V = unknown, T = Record<string, unknown>> = (
|
|
513
|
+
value: V,
|
|
514
|
+
allValues: T
|
|
515
|
+
) => string | false | null | undefined;
|
|
516
|
+
|
|
517
|
+
/** Form values, errors and submission flags in one container. */
|
|
518
|
+
export class FormState<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
519
|
+
constructor(initialValues?: T, options?: ReactiveStateOptions);
|
|
520
|
+
|
|
521
|
+
setValue<K extends keyof T & string>(field: K, value: T[K]): void;
|
|
522
|
+
getValue<K extends keyof T & string>(field: K): T[K];
|
|
523
|
+
setError(field: keyof T & string, error: string | null): void;
|
|
524
|
+
/** Register a check run whenever the field changes */
|
|
525
|
+
addValidator<K extends keyof T & string>(field: K, validator: FieldCheck<T[K], T>): void;
|
|
526
|
+
/** Run every validator; `true` when all pass */
|
|
527
|
+
validateAll(): boolean;
|
|
528
|
+
/**
|
|
529
|
+
* Validate, then await `onSubmit`. Resolves `false` when validation fails or
|
|
530
|
+
* `onSubmit` throws, recording the error under `_form`.
|
|
531
|
+
*/
|
|
532
|
+
submit(onSubmit: (values: T) => unknown): Promise<boolean>;
|
|
533
|
+
/** Restore the initial values and drop errors */
|
|
534
|
+
reset(): void;
|
|
535
|
+
|
|
536
|
+
watchValues(callback: Watcher<T>): () => void;
|
|
537
|
+
watchErrors(callback: Watcher<Record<string, string>>): () => void;
|
|
538
|
+
watchSubmitting(callback: Watcher<boolean>): () => void;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export interface ListStateOptions extends ReactiveStateOptions {
|
|
542
|
+
/** Items per page; defaults to `10` */
|
|
543
|
+
pageSize?: number;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** A list with filtering, sorting and pagination. */
|
|
547
|
+
export class ListState<T = unknown> {
|
|
548
|
+
constructor(initialItems?: T[], options?: ListStateOptions);
|
|
549
|
+
|
|
550
|
+
addItem(item: T): void;
|
|
551
|
+
/** Remove by index, or by the first item matching a predicate */
|
|
552
|
+
removeItem(indexOrPredicate: number | ((item: T) => boolean)): void;
|
|
553
|
+
updateItem(indexOrPredicate: number | ((item: T) => boolean), updates: Partial<T>): void;
|
|
554
|
+
filter(filters: Record<string, unknown>): void;
|
|
555
|
+
sort(sortBy: string, order?: 'asc' | 'desc'): void;
|
|
556
|
+
setPage(page: number): void;
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Replace the items with whatever `loader` resolves to, toggling `loading`.
|
|
560
|
+
* Resolves `[]` and records `error` when the loader throws.
|
|
561
|
+
*/
|
|
562
|
+
load(loader: (filters: Record<string, unknown>) => T[] | Promise<T[]>): Promise<T[]>;
|
|
563
|
+
|
|
564
|
+
/** Items matching the current filters */
|
|
565
|
+
get filteredItems(): T[];
|
|
566
|
+
/** {@link ListState.filteredItems} in the current sort order */
|
|
567
|
+
get sortedItems(): T[];
|
|
568
|
+
/** The current page of {@link ListState.sortedItems} */
|
|
569
|
+
get paginatedItems(): T[];
|
|
570
|
+
/** Page count for the current filters and page size */
|
|
571
|
+
get totalPages(): number;
|
|
572
|
+
|
|
573
|
+
watchItems(callback: Watcher<T[]>): () => void;
|
|
574
|
+
watchLoading(callback: Watcher<boolean>): () => void;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** A modal whose `open()` resolves with whatever `close()` is passed. */
|
|
578
|
+
export class ModalState<D = unknown, R = unknown> {
|
|
579
|
+
constructor(initialState?: Record<string, unknown>);
|
|
580
|
+
|
|
581
|
+
/** Open with data; resolves once closed */
|
|
582
|
+
open(data?: D): Promise<R | null>;
|
|
583
|
+
/** Close, resolving the pending `open()` */
|
|
584
|
+
close(result?: R | null): void;
|
|
585
|
+
setLoading(loading: boolean): void;
|
|
586
|
+
setError(error: unknown): void;
|
|
587
|
+
|
|
588
|
+
watchOpen(callback: Watcher<boolean>): () => void;
|
|
589
|
+
watchData(callback: Watcher<D | null>): () => void;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Client-side route, params and history. */
|
|
593
|
+
export class RouterState {
|
|
594
|
+
constructor(initialRoute?: string, options?: ReactiveStateOptions);
|
|
595
|
+
|
|
596
|
+
addRoute(path: string, handler: unknown): void;
|
|
597
|
+
navigate(path: string, params?: Record<string, unknown>, query?: Record<string, unknown>): void;
|
|
598
|
+
back(): void;
|
|
599
|
+
forward(): void;
|
|
600
|
+
|
|
601
|
+
watchRoute(callback: Watcher<string>): () => void;
|
|
602
|
+
watchParams(callback: Watcher<Record<string, unknown>>): () => void;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Create a {@link FormState}. */
|
|
606
|
+
export function createFormState<T extends Record<string, unknown> = Record<string, unknown>>(
|
|
607
|
+
initialValues?: T,
|
|
608
|
+
options?: ReactiveStateOptions
|
|
609
|
+
): FormState<T>;
|
|
610
|
+
|
|
611
|
+
/** Create a {@link ListState}. */
|
|
612
|
+
export function createListState<T = unknown>(
|
|
613
|
+
initialItems?: T[],
|
|
614
|
+
options?: ListStateOptions
|
|
615
|
+
): ListState<T>;
|
|
616
|
+
|
|
617
|
+
/** Create a {@link ModalState}. */
|
|
618
|
+
export function createModalState<D = unknown, R = unknown>(
|
|
619
|
+
initialState?: Record<string, unknown>
|
|
620
|
+
): ModalState<D, R>;
|
|
621
|
+
|
|
622
|
+
/** Create a {@link RouterState}. */
|
|
623
|
+
export function createRouterState(
|
|
624
|
+
initialRoute?: string,
|
|
625
|
+
options?: ReactiveStateOptions
|
|
626
|
+
): RouterState;
|