@coherent.js/state 1.1.0 → 2.0.0-rc.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/types/index.d.ts CHANGED
@@ -15,10 +15,18 @@ export type Watcher<T = unknown> = (
15
15
  ) => void;
16
16
 
17
17
  export interface ObservableOptions {
18
- /** Notify even when the new value is `===` the old one; defaults to `true` */
18
+ /**
19
+ * Re-assigning the same object still notifies, since it may have been
20
+ * mutated in place; defaults to `true`. Identical primitives never notify.
21
+ */
19
22
  deep?: boolean;
20
23
  /** Invoke watchers on subscribe; defaults to `true` */
21
24
  immediate?: boolean;
25
+ /**
26
+ * Receives errors thrown by watchers (and middleware); defaults to
27
+ * {@link globalErrorHandler}. The other watchers still run.
28
+ */
29
+ onError?: (error: unknown, context: { type: string; [key: string]: unknown }) => void;
22
30
  [option: string]: unknown;
23
31
  }
24
32
 
@@ -26,7 +34,8 @@ export interface ObservableOptions {
26
34
  * A single reactive value.
27
35
  *
28
36
  * Read and write through the `value` accessor — assigning notifies watchers
29
- * and invalidates any computed that read it.
37
+ * and invalidates any computed that read it. Watchers run after the write
38
+ * (after the outermost {@link batch}), each in isolation.
30
39
  *
31
40
  * ```ts
32
41
  * const count = observable(0);
@@ -41,16 +50,25 @@ export class Observable<T = unknown> {
41
50
  get value(): T;
42
51
  set value(newValue: T);
43
52
 
53
+ /** Read the value without recording a computed dependency */
54
+ peek(): T;
55
+
44
56
  /** Subscribe to changes; returns an unwatch function */
45
57
  watch(callback: Watcher<T>, options?: { immediate?: boolean }): () => void;
46
58
 
47
- /** Remove one observer */
48
- unwatch(observer: (newValue: T, oldValue: T | undefined) => void): void;
59
+ /** Remove a watcher, by the callback passed to {@link watch} */
60
+ unwatch(callback: Watcher<T>): void;
49
61
 
50
- /** Remove every observer and computed dependent */
62
+ /** Remove every watcher */
51
63
  unwatchAll(): void;
52
64
  }
53
65
 
66
+ /**
67
+ * Defer watcher notifications until `fn` returns; each watcher then runs
68
+ * once, with the final value. Batches nest. `fn` must be synchronous.
69
+ */
70
+ export function batch<T>(fn: () => T): T;
71
+
54
72
  /** Raised by the reactive primitives. */
55
73
  export class StateError extends Error {
56
74
  constructor(message: string, options?: Record<string, unknown>);
@@ -107,30 +125,37 @@ export interface HistoryEntry {
107
125
  export class ReactiveState {
108
126
  constructor(initialState?: Record<string, unknown>, options?: ReactiveStateOptions);
109
127
 
110
- /** Current value of a key, or `undefined` */
128
+ /** Current value of a key or dot path (`'user.name'`), or `undefined` */
111
129
  get<T = unknown>(key: string): T | undefined;
112
130
 
113
- /** Write a key; `false` when middleware cancelled the write */
131
+ /**
132
+ * Write a key; `false` when middleware cancelled the write. A dot path
133
+ * writes a copy of the parent object, notifying watchers of both.
134
+ */
114
135
  set(key: string, value: unknown, options?: ReactiveStateOptions): boolean;
115
136
 
116
- /** Whether the key exists */
137
+ /** Whether the key (or dot path) exists */
117
138
  has(key: string): boolean;
118
139
 
119
- /** Drop a key and its watchers; `false` when it was absent */
140
+ /**
141
+ * Drop a key (or dot path) and the key's watchers; computed properties that
142
+ * read it update. `false` when it was absent.
143
+ */
120
144
  delete(key: string): boolean;
121
145
 
122
146
  /** Drop every key */
123
147
  clear(): void;
124
148
 
125
149
  /** Define a computed property derived from other keys */
126
- computed(key: string, getter: () => unknown, options?: ObservableOptions): void;
150
+ computed<T = unknown>(key: string, getter: () => T, options?: ObservableOptions): Observable<T>;
127
151
 
128
152
  /** Current value of a computed property, or `undefined` */
129
153
  getComputed<T = unknown>(key: string): T | undefined;
130
154
 
131
155
  /**
132
- * Watch one key, or a getter expression. Returns an unwatch function, and
133
- * throws {@link StateError} for a key that does not exist.
156
+ * Watch one key, a dot path, or a getter expression (re-evaluated when what
157
+ * it reads changes). Returns an unwatch function, and throws
158
+ * {@link StateError} for a key that does not exist.
134
159
  */
135
160
  watch<T = unknown>(
136
161
  key: string | (() => T),
@@ -138,7 +163,10 @@ export class ReactiveState {
138
163
  options?: { immediate?: boolean }
139
164
  ): () => void;
140
165
 
141
- /** Apply several writes with history suppressed until the batch ends */
166
+ /**
167
+ * Apply several writes as one: watchers run once afterwards, with the final
168
+ * values, and history records a single entry
169
+ */
142
170
  batch<T>(updates: ((state: this) => T) | Record<string, unknown>): T | undefined;
143
171
 
144
172
  /** Subscribe to one or more keys; returns an unsubscribe function */
@@ -187,7 +215,9 @@ export function observable<T = unknown>(value: T, options?: ObservableOptions):
187
215
 
188
216
  /**
189
217
  * Create a read-only observable derived from other observables. Dependencies
190
- * are tracked automatically; assigning to `value` throws {@link StateError}.
218
+ * are tracked automatically and it recomputes lazily; assigning to `value`,
219
+ * or reading it from its own getter (directly or through others), throws
220
+ * {@link StateError}.
191
221
  */
192
222
  export function computed<T = unknown>(
193
223
  getter: () => T,
@@ -265,32 +295,59 @@ export const globalStateManager: {
265
295
  // ============================================================================
266
296
 
267
297
  /**
268
- * Push a context value, remembering the previous one so
269
- * {@link restoreContext} can unwind it.
298
+ * Run `fn` in a fresh, isolated context scope — one per request or render.
299
+ * On Node (AsyncLocalStorage) the scope follows `fn`'s async work, so wrap
300
+ * each request, and each consumer of a streaming render, in it. `values`
301
+ * seeds the scope.
302
+ */
303
+ export function runWithContext<T>(fn: () => T, values?: Record<string, unknown>): T;
304
+
305
+ /**
306
+ * Push a context value for the rest of the current runWithContext() scope,
307
+ * remembering the previous one so {@link restoreContext} can unwind it. On
308
+ * Node it throws outside runWithContext(), where the value would leak into
309
+ * other requests.
270
310
  */
271
311
  export function provideContext(key: string, value: unknown): void;
272
312
 
313
+ /** A context provider created by {@link createContextProvider}. */
314
+ export interface ContextProvider<C = unknown> {
315
+ /**
316
+ * Called by the renderer as a zero-argument component: returns the children
317
+ * with their function components and function-valued props evaluated in
318
+ * the context.
319
+ */
320
+ (): C;
321
+ /**
322
+ * Run `renderFunction(children)` with the context provided. On Node an
323
+ * async render function keeps the context across its awaits.
324
+ */
325
+ <R>(renderFunction: (children: C) => R): R;
326
+ }
327
+
273
328
  /**
274
- * Wrap children in a context. The returned function provides the context,
275
- * renders, then restores the previous value.
329
+ * Wrap children in a context. Place the result in a component tree; the
330
+ * children see `value`, their siblings do not.
276
331
  */
277
- export function createContextProvider<C = unknown, R = unknown>(
332
+ export function createContextProvider<C = unknown>(
278
333
  key: string,
279
334
  value: unknown,
280
335
  children: C
281
- ): (renderFunction?: (children: C) => R) => R | C;
336
+ ): ContextProvider<C>;
282
337
 
283
338
  /** Pop one context value, restoring what {@link provideContext} replaced. */
284
339
  export function restoreContext(key: string): void;
285
340
 
286
341
  /**
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.
342
+ * Drop every context provided in the current execution. Values set through
343
+ * {@link globalStateManager} are left alone.
290
344
  */
291
345
  export function clearAllContexts(): void;
292
346
 
293
- /** Read the current context value, or `undefined`. */
347
+ /**
348
+ * Read the current context value. Falls back to {@link globalStateManager}
349
+ * when no context was provided for `key`.
350
+ */
294
351
  export function useContext<T = unknown>(key: string): T | undefined;
295
352
 
296
353
  // ============================================================================
@@ -300,8 +357,13 @@ export function useContext<T = unknown>(key: string): T | undefined;
300
357
  /** Where persistent state is written. */
301
358
  export type StorageKind = 'localStorage' | 'sessionStorage' | 'indexedDB' | 'memory';
302
359
 
303
- /** Storage backend contract. */
360
+ /**
361
+ * Storage backend contract. `set` resolves `true` once stored; rejecting or
362
+ * resolving `false` is reported through `onError`. An adapter whose
363
+ * `available` is `false` is never written to.
364
+ */
304
365
  export interface PersistenceAdapter {
366
+ available?: boolean;
305
367
  get(key: string): Promise<string | null> | string | null;
306
368
  set(key: string, value: string): Promise<boolean> | boolean;
307
369
  remove(key: string): Promise<boolean> | boolean;
@@ -309,8 +371,14 @@ export interface PersistenceAdapter {
309
371
  }
310
372
 
311
373
  export interface PersistentStateOptions {
312
- /** Backend; defaults to `'localStorage'` */
374
+ /**
375
+ * Backend; defaults to `'localStorage'`. On the server (no `window`) the
376
+ * browser backends read and write nothing, since Web Storage there would be
377
+ * shared by every request.
378
+ */
313
379
  storage?: StorageKind;
380
+ /** Custom backend, used as is (also on the server); overrides `storage` */
381
+ adapter?: PersistenceAdapter | null;
314
382
  /** Storage key; defaults to `'coherent-state'` */
315
383
  key?: string;
316
384
  /** Coalesce writes; defaults to `true` */
@@ -323,8 +391,12 @@ export interface PersistentStateOptions {
323
391
  include?: string[] | null;
324
392
  /** Persist everything except these keys */
325
393
  exclude?: string[] | null;
326
- /** Obfuscate the payload with `encryptionKey` */
394
+ /**
395
+ * XOR-obfuscate the payload with `encryptionKey`. This is obfuscation, not
396
+ * encryption: the key ships to the browser. Never store secrets.
397
+ */
327
398
  encrypt?: boolean;
399
+ /** Required when `encrypt` is `true`; there is no default key */
328
400
  encryptionKey?: string | null;
329
401
  onSave?: ((state: Record<string, unknown>) => void) | null;
330
402
  onLoad?: ((state: Record<string, unknown>) => void) | null;
@@ -332,11 +404,25 @@ export interface PersistentStateOptions {
332
404
  /** Tag stored payloads with `version` and run `migrate` on mismatch */
333
405
  versioning?: boolean;
334
406
  version?: string;
335
- migrate?: ((state: Record<string, unknown>, from: string) => Record<string, unknown>) | null;
407
+ /**
408
+ * Receives the stored payload as serialized by `serialize`, returns it
409
+ * migrated (still serialized) for `deserialize`
410
+ */
411
+ migrate?: ((serializedState: string, fromVersion: string, toVersion: string) => string) | null;
336
412
  /** Discard stored state older than this many ms */
337
413
  ttl?: number | null;
338
- /** Mirror updates to other tabs over BroadcastChannel */
414
+ /**
415
+ * Mirror updates to other tabs holding the same `key`, over a
416
+ * BroadcastChannel named after it. Off on the server.
417
+ */
339
418
  crossTab?: boolean;
419
+ /** IndexedDB database name (`storage: 'indexedDB'`); defaults to `'coherent-db'` */
420
+ dbName?: string;
421
+ /**
422
+ * IndexedDB object store name (`storage: 'indexedDB'`); defaults to
423
+ * `'state'`. Added to an existing database in a version upgrade.
424
+ */
425
+ storeName?: string;
340
426
  }
341
427
 
342
428
  /** A state container backed by storage. */
@@ -357,20 +443,31 @@ export interface PersistentState {
357
443
  subscribe(
358
444
  listener: (state: Record<string, unknown>, oldState: Record<string, unknown>) => void
359
445
  ): () => void;
360
- /** Force an immediate write */
361
- persist(): Promise<void>;
446
+ /** Force an immediate write; `false` when nothing was stored */
447
+ persist(): Promise<boolean>;
362
448
  /** Reload from storage; `false` when nothing was stored */
363
449
  restore(): Promise<boolean>;
364
450
  /** Remove the stored payload */
365
451
  clearStorage(): Promise<void>;
366
452
  load(): Promise<Record<string, unknown> | null>;
367
- save(): Promise<void>;
453
+ /** Write now; `false` when nothing was stored (failures go to `onError`) */
454
+ save(): Promise<boolean>;
455
+ /**
456
+ * Flush a pending debounced save, close the cross-tab channel and drop
457
+ * listeners.
458
+ */
459
+ destroy(): Promise<void>;
460
+ /**
461
+ * Settles once the automatic restore on creation is done: `true` when
462
+ * stored state was restored. Keys set before then keep their new value.
463
+ */
464
+ readonly ready: Promise<boolean>;
368
465
  readonly adapter: PersistenceAdapter;
369
466
  }
370
467
 
371
468
  /**
372
469
  * Create a state container that persists to storage. Unless the backend is
373
- * `'memory'`, stored state is restored on creation.
470
+ * `'memory'`, stored state is restored on creation; await `ready` for it.
374
471
  */
375
472
  export function createPersistentState(
376
473
  initialState?: Record<string, unknown>,
@@ -450,7 +547,11 @@ export interface ValidatedStateOptions {
450
547
  validators?: Record<string, Validator | Validator[]>;
451
548
  /** Reject writes that fail validation */
452
549
  strict?: boolean;
453
- /** Convert values to the declared type where possible */
550
+ /**
551
+ * Convert values to the declared type where that is unambiguous: numeric
552
+ * strings to numbers, `'true'`/`'false'`/`'1'`/`'0'` to booleans, numbers
553
+ * and booleans to strings. Anything else stays a type error.
554
+ */
454
555
  coerce?: boolean;
455
556
  onError?: ((errors: ValidationError[]) => void) | null;
456
557
  /** Validate on write; defaults to `true` */
@@ -458,7 +559,10 @@ export interface ValidatedStateOptions {
458
559
  /** Validate on read; defaults to `false` */
459
560
  validateOnGet?: boolean;
460
561
  required?: string[];
461
- /** Permit keys the schema does not mention; defaults to `true` */
562
+ /**
563
+ * Permit keys an object schema's `properties` do not mention; defaults to
564
+ * `true`. A schema's own `additionalProperties: false` always rejects them.
565
+ */
462
566
  allowUnknown?: boolean;
463
567
  }
464
568
 
@@ -578,7 +682,10 @@ export class ListState<T = unknown> {
578
682
  export class ModalState<D = unknown, R = unknown> {
579
683
  constructor(initialState?: Record<string, unknown>);
580
684
 
581
- /** Open with data; resolves once closed */
685
+ /**
686
+ * Open with data; resolves once closed. Opening again while open replaces
687
+ * the modal, resolving the earlier promise with `null`.
688
+ */
582
689
  open(data?: D): Promise<R | null>;
583
690
  /** Close, resolving the pending `open()` */
584
691
  close(result?: R | null): void;