@voltro/client 0.13.0 → 0.14.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.d.ts CHANGED
@@ -174,6 +174,16 @@ export declare interface ApiHandle {
174
174
  readonly errorBus: RpcErrorBus;
175
175
  }
176
176
 
177
+ /**
178
+ * Apply seeds from the hydration payload, BEFORE the first client render.
179
+ *
180
+ * Unknown store names are skipped rather than throwing: a payload can outlive a
181
+ * deploy (a cached document, a user with a stale tab), and refusing to boot the
182
+ * app over a store somebody deleted would turn a cosmetic staleness into a
183
+ * blank page.
184
+ */
185
+ export declare const applyStoreSeeds: (seeds: ReadonlyArray<StoreSeed> | undefined) => void;
186
+
177
187
  export declare interface AsyncValidationResult {
178
188
  readonly status: ValidationStatus;
179
189
  readonly valid: boolean | undefined;
@@ -351,6 +361,9 @@ export declare const clearMutations: () => void;
351
361
  /** Drop successfully-sent entries (call after a drain to compact the queue). */
352
362
  export declare const clearSent: (queue: Outbox) => Outbox;
353
363
 
364
+ /** Drop the recorded history (tests, and a devtools "clear"). */
365
+ export declare const clearStoreHistory: () => void;
366
+
354
367
  export declare const CLIENT_NAME: "framework-client";
355
368
 
356
369
  export declare interface ClientErrorEvent {
@@ -533,11 +546,28 @@ export declare interface DataTableState<Row> {
533
546
  readonly hasMore: boolean;
534
547
  }
535
548
 
549
+ /** All defined stores, in definition order. */
550
+ export declare const definedStores: () => ReadonlyArray<StoreHandle<never>>;
551
+
536
552
  /** Declare a typed analytics event. */
537
553
  export declare const defineEvent: <const Name extends string, A, I>(name: Name, payload: Schema.Schema<A, I>) => EventDescriptor<Name, A>;
538
554
 
539
- /** Identity helper (like `defineTracking('CheckoutButton', { onMount: 'checkout.viewed' })`). */
540
- export declare const defineTracking: (name: string, map: TrackingMap) => TrackingSpec;
555
+ /**
556
+ * Define a store.
557
+ *
558
+ * `initial` is a FUNCTION, not a value, so every keyed instance gets its own
559
+ * fresh state rather than sharing one object — the bug you find weeks later
560
+ * when editing order A also edits order B.
561
+ */
562
+ export declare const defineStore: <S extends object>(name: string, initial: () => S, options?: {
563
+ readonly persist?: PersistOptions<S>;
564
+ }) => StoreHandle<S>;
565
+
566
+ /** Identity helper (like `defineTracking('CheckoutButton', { onMount: 'checkout.viewed' })`).
567
+ *
568
+ * Pass the component's props as the type argument to get a typed catalogue:
569
+ * `defineTracking<ComposerProps>('TodoComposer', { onSubmit: (p) => … })`. */
570
+ export declare const defineTracking: <P = Record<string, unknown>>(name: string, map: TrackingMap<P>) => TrackingSpec<P>;
541
571
 
542
572
  /**
543
573
  * Pure projection: turn a capability manifest into one `EntityAdminSpec` per
@@ -588,6 +618,9 @@ declare interface EntryState<T> {
588
618
  cachedSnapshot: CacheSnapshot<T> | null;
589
619
  }
590
620
 
621
+ /** Compares two selector results. Return true to keep the previous value. */
622
+ export declare type Equals<T> = (a: T, b: T) => boolean;
623
+
591
624
  /** Helper: pattern-match the `_tag` field on a thrown error value. Works
592
625
  * on Schema.TaggedError instances + plain `{_tag: ..., …}` objects the
593
626
  * rpc wire emits. Returns undefined for non-tagged errors. */
@@ -711,6 +744,8 @@ export declare const getMutationNotifier: () => MutationNotifier | undefined;
711
744
  /** Read the current feed. Newest-first; up to MAX_MUTATIONS entries. */
712
745
  export declare const getMutations: () => ReadonlyArray<MutationEvent>;
713
746
 
747
+ declare type Listener = () => void;
748
+
714
749
  /**
715
750
  * Non-fetching cache used as a LOADING baseline:
716
751
  * - during SSR (no runtime / no provider), and
@@ -737,6 +772,8 @@ export declare class LoadingSubscriptionCache extends SubscriptionCache {
737
772
  };
738
773
  }
739
774
 
775
+ export declare const makeStoreSeedBag: () => StoreSeedBag;
776
+
740
777
  /** Exported for tests: the inspect URL this hook fetches. */
741
778
  export declare const manifestUrl: (base: string) => string;
742
779
 
@@ -884,6 +921,31 @@ declare interface PermissionState_2 {
884
921
  }
885
922
  export { PermissionState_2 as PermissionState }
886
923
 
924
+ /**
925
+ * Where a store's state survives a reload.
926
+ *
927
+ * Declarative because the hand-rolled version is always the same three bugs:
928
+ * reading before the browser exists (SSR crashes), writing on every keystroke
929
+ * (a synchronous localStorage write per character), and a parse that throws on
930
+ * a value written by an older version of the app and takes the boot with it.
931
+ */
932
+ export declare interface PersistOptions<S> {
933
+ /** Storage key. Namespaced by the caller — this is written verbatim. */
934
+ readonly key: string;
935
+ /** `local` survives a browser restart; `session` lasts the tab. */
936
+ readonly storage?: 'local' | 'session';
937
+ /** Persist only part of the state — a draft, not a whole UI tree. */
938
+ readonly pick?: (state: S) => Partial<S>;
939
+ /**
940
+ * Migrate a value written by an older shape.
941
+ *
942
+ * Returning `undefined` DISCARDS it, which is the right answer far more often
943
+ * than guessing: a stale draft is an annoyance, a half-migrated one is a bug
944
+ * report nobody can reproduce.
945
+ */
946
+ readonly migrate?: (stored: unknown) => Partial<S> | undefined;
947
+ }
948
+
887
949
  /** Structural mirror of @voltro/runtime's PreviewDiff (client stays decoupled). */
888
950
  export declare interface PreviewDiff {
889
951
  readonly rows: ReadonlyArray<PreviewRowDiff>;
@@ -996,10 +1058,13 @@ export declare const reportClientError: (error: unknown, context?: Record<string
996
1058
  * module-global flags + any pending grace-period timer). */
997
1059
  export declare const _resetFrameworkRuntimesWarning: () => void;
998
1060
 
1061
+ /** Test helper: forget every defined store. NEVER call this in app code. */
1062
+ export declare const resetStoreRegistryForTests: () => void;
1063
+
999
1064
  export declare const resolveByTag: (client: unknown, tag: string) => unknown;
1000
1065
 
1001
1066
  /** Resolve one map entry against the props. Pure. */
1002
- export declare const resolveTrackingEvent: (entry: TrackingEntry | undefined, props: Record<string, unknown>) => TrackingEvent | null;
1067
+ export declare const resolveTrackingEvent: <P>(entry: TrackingEntry<P> | undefined, props: P) => TrackingEvent | null;
1003
1068
 
1004
1069
  export declare interface ResourceCanInput {
1005
1070
  /** The action being gated (e.g. `'read'`, `'write'`, `'delete'`). */
@@ -1163,6 +1228,18 @@ export declare const schemaToColumns: (schema: Schema.Schema.Any) => ReadonlyArr
1163
1228
  */
1164
1229
  export declare const schemaToFields: (schema: Schema.Schema.Any) => ReadonlyArray<FieldDescriptor>;
1165
1230
 
1231
+ /**
1232
+ * Seed a store for THIS request's hydration. Call it anywhere on the server
1233
+ * during a render — a loader, a layout loader — and the value reaches the
1234
+ * client's first render without any serialization step of your own.
1235
+ */
1236
+ export declare const seedStore: <S extends object>(handle: StoreHandle<S>, value: Partial<S>, opts?: {
1237
+ readonly key?: string;
1238
+ }) => void;
1239
+
1240
+ /** Reads a slice out of the state. Its RESULT identity decides re-render. */
1241
+ export declare type Selector<S, T> = (state: S) => T;
1242
+
1166
1243
  /** One persisted/wire element of a resumable stream. Mirrors `@voltro/ai`'s
1167
1244
  * `SeqEvent` without taking a dependency on it (the client stays AI-agnostic). */
1168
1245
  export declare interface SeqElement<E> {
@@ -1220,6 +1297,12 @@ export declare type SequenceResult<Ctx> = {
1220
1297
  * at boot, next to your toast provider. */
1221
1298
  export declare const setMutationNotifier: (notifier: MutationNotifier | undefined) => void;
1222
1299
 
1300
+ /**
1301
+ * Install the request-scoped bag resolver. Server-side only; pass `null` to
1302
+ * uninstall (tests).
1303
+ */
1304
+ export declare const setStoreSeedResolver: (resolver: (() => StoreSeedBag | null) | null) => void;
1305
+
1223
1306
  /**
1224
1307
  * Transition a pending mutation to 'success' or 'error'. Updates the
1225
1308
  * existing entry in-place (preserves order in the buffer) so the
@@ -1236,6 +1319,17 @@ export declare const settleMutation: (id: string, outcome: {
1236
1319
  readonly error: unknown;
1237
1320
  }) => void;
1238
1321
 
1322
+ /**
1323
+ * Shallow equality over own enumerable entries — the comparator a selector that
1324
+ * BUILDS something needs.
1325
+ *
1326
+ * `(s) => ({ a: s.a, b: s.b })` and `(s) => s.items.filter(…)` return a fresh
1327
+ * reference every call, so the default identity check always reports "changed".
1328
+ * Comparing one level deep is what makes those selectors behave the way reading
1329
+ * a single field already does.
1330
+ */
1331
+ export declare const shallow: <T>(a: T, b: T) => boolean;
1332
+
1239
1333
  /** Shallow element-wise array equality. Pure — the cache-invalidation core. */
1240
1334
  export declare const shallowArrayEqual: (a: ReadonlyArray<unknown>, b: ReadonlyArray<unknown>) => boolean;
1241
1335
 
@@ -1337,6 +1431,141 @@ export declare interface StepOptions<Ctx, Result> {
1337
1431
  * from a step that is being rolled back after it. */
1338
1432
  export declare type StepUndo<Result, Ctx = Record<string, unknown>> = (result: Result, ctx: Ctx) => unknown | Promise<unknown>;
1339
1433
 
1434
+ export declare interface StoreHandle<S> {
1435
+ readonly name: string;
1436
+ /**
1437
+ * Read a slice in a component. Re-renders ONLY when the selector's RESULT
1438
+ * changes, so a store with fifty fields re-renders a component that reads one
1439
+ * of them exactly as often as that one field changes.
1440
+ *
1441
+ * "Changes" means `Object.is` by default. A selector that BUILDS something —
1442
+ * an object literal, a mapped or filtered array — returns a fresh reference
1443
+ * every call, so pass `{ equals: shallow }` for those. In dev the framework
1444
+ * warns, once, when it detects the case.
1445
+ */
1446
+ readonly use: <T>(selector: Selector<S, T>, opts?: StoreUseOptions<T>) => T;
1447
+ /** The underlying instance — for actions, effects and tests. */
1448
+ readonly instance: (key?: string) => StoreInstance<S>;
1449
+ /** Shorthand for `instance(key).get()`. */
1450
+ readonly get: (key?: string) => S;
1451
+ /** Shorthand for `instance(key).set(...)`. */
1452
+ readonly set: (next: Partial<S> | Updater<S>, key?: string, label?: string) => void;
1453
+ /**
1454
+ * Run several writes as ONE — one notification, one devtools entry named
1455
+ * `label`, one undo step.
1456
+ *
1457
+ * Without it, an action that touches three fields is three of each: the
1458
+ * devtools show three anonymous writes instead of the thing the user did,
1459
+ * and Ctrl-Z walks back through a third of a change at a time. React batches
1460
+ * the RE-RENDERS on its own; it cannot batch the meaning.
1461
+ *
1462
+ * ```ts
1463
+ * checkout.batch('applyCoupon', () => {
1464
+ * checkout.set({ coupon })
1465
+ * checkout.set({ total: recompute(coupon) })
1466
+ * })
1467
+ * ```
1468
+ *
1469
+ * If `fn` throws, every write it made is rolled back — nothing was announced
1470
+ * yet, so a failed action cannot leave the half-applied state that is the
1471
+ * usual reason people reach for a transaction. A nested batch joins its
1472
+ * parent. Passing an `async` function is an error: everything after its
1473
+ * first `await` would land outside the batch.
1474
+ */
1475
+ readonly batch: (label: string, fn: () => void, key?: string) => void;
1476
+ /**
1477
+ * Revert the most recent write to this instance.
1478
+ *
1479
+ * Possible only because every write goes through one seam — the previous
1480
+ * state is recorded there, so undo is a lookup rather than a feature the
1481
+ * store had to be designed around. Returns false when there is nothing to
1482
+ * undo (nothing recorded, or the history has scrolled past it).
1483
+ */
1484
+ readonly undo: (key?: string) => boolean;
1485
+ /** Re-apply the write an `undo` reverted. False when there is nothing ahead. */
1486
+ readonly redo: (key?: string) => boolean;
1487
+ /** Whether `undo` / `redo` would do anything — for disabling a button. */
1488
+ readonly canUndo: (key?: string) => boolean;
1489
+ readonly canRedo: (key?: string) => boolean;
1490
+ /** Drop a keyed instance's state. The global instance cannot be dropped. */
1491
+ readonly release: (key: string) => void;
1492
+ /** Every live key — for devtools and for tests that assert cleanup. */
1493
+ readonly keys: () => ReadonlyArray<string>;
1494
+ }
1495
+
1496
+ export declare const storeHistory: () => ReadonlyArray<StoreMutation>;
1497
+
1498
+ /** One store instance: the global one, or one per key. */
1499
+ export declare interface StoreInstance<S> {
1500
+ /** Current state. For actions and tests — components read via a selector. */
1501
+ readonly get: () => S;
1502
+ /** Replace the state. A partial object is merged; a function is applied. */
1503
+ readonly set: (next: Partial<S> | Updater<S>, label?: string) => void;
1504
+ /** Apply many writes as ONE. See {@link StoreHandle.batch}. */
1505
+ readonly batch: (label: string, fn: () => void) => void;
1506
+ /** Reset to the initial state this instance was created with. */
1507
+ readonly reset: () => void;
1508
+ /** Low-level subscribe (what `use` is built on). Returns an unsubscribe. */
1509
+ readonly subscribe: (listener: Listener) => () => void;
1510
+ }
1511
+
1512
+ /** One recorded write. `prev`/`next` are the state objects themselves — they
1513
+ * are immutable by convention, so holding them costs a reference. */
1514
+ export declare interface StoreMutation {
1515
+ readonly store: string;
1516
+ /** The keyed instance, or undefined for the global one. */
1517
+ readonly key?: string;
1518
+ /** Whatever the caller passed as `set(next, label)`. */
1519
+ readonly label?: string;
1520
+ /**
1521
+ * True when this write WAS an undo.
1522
+ *
1523
+ * Recorded so a devtools feed shows the complete story, and skipped by
1524
+ * `undo()` so repeated calls walk BACK through the stack instead of toggling
1525
+ * between the last two states. Undoing an undo is a redo, and a redo that
1526
+ * pretends to be an undo is the kind of surprise nobody debugs twice.
1527
+ */
1528
+ readonly isUndo?: true;
1529
+ readonly prev: unknown;
1530
+ readonly next: unknown;
1531
+ readonly at: number;
1532
+ }
1533
+
1534
+ /** One seeded value, as it travels in the hydration payload. */
1535
+ export declare interface StoreSeed {
1536
+ readonly store: string;
1537
+ readonly key?: string;
1538
+ readonly value: unknown;
1539
+ }
1540
+
1541
+ /** Collects seeds for ONE request. The server creates one per render. */
1542
+ export declare interface StoreSeedBag {
1543
+ readonly seeds: StoreSeed[];
1544
+ }
1545
+
1546
+ export declare interface StoreUseOptions<T> {
1547
+ /** Which instance to read. Omitted = the global one. */
1548
+ readonly key?: string;
1549
+ /**
1550
+ * How to decide the selected value is unchanged. Defaults to `Object.is`.
1551
+ *
1552
+ * Pass {@link shallow} whenever the selector builds something new — an object
1553
+ * literal, a mapped or filtered array. Identity comparison can never see two
1554
+ * fresh references as equal, so without it such a selector re-renders on
1555
+ * every change to any field in the store.
1556
+ */
1557
+ readonly equals?: Equals<T>;
1558
+ /**
1559
+ * Keep this keyed instance alive after its last subscriber unmounts.
1560
+ *
1561
+ * The default is to drop it, because instances are created on demand and
1562
+ * nothing else would ever remove them: a table keyed by row id would
1563
+ * accumulate one per row ever rendered. Set this for state that must survive
1564
+ * navigating away and back — a half-filled form, say.
1565
+ */
1566
+ readonly retain?: boolean;
1567
+ }
1568
+
1340
1569
  /** Subscribe to client errors. Returns an unsubscribe fn. */
1341
1570
  export declare const subscribeClientErrors: (listener: ClientErrorListener) => (() => void);
1342
1571
 
@@ -1345,6 +1574,9 @@ export declare const subscribeClientTraces: (listener: ClientTraceListener) => (
1345
1574
 
1346
1575
  export declare const subscribeMutations: (cb: () => void) => (() => void);
1347
1576
 
1577
+ /** Subscribe to writes — the devtools panel's feed. Returns an unsubscribe. */
1578
+ export declare const subscribeStoreHistory: (listener: Listener) => (() => void);
1579
+
1348
1580
  export declare class SubscriptionCache {
1349
1581
  private readonly entries;
1350
1582
  private readonly inactiveTtlMs;
@@ -1649,8 +1881,14 @@ export declare interface SubscriptionStateWithFallback<T> extends SubscriptionMe
1649
1881
  readonly isEmpty: boolean;
1650
1882
  }
1651
1883
 
1652
- /** A map entry: a bare event name, or a function of the component's props. */
1653
- export declare type TrackingEntry = string | ((props: Record<string, unknown>) => TrackingEvent);
1884
+ /** A map entry: a bare event name, or a function of the component's props.
1885
+ *
1886
+ * `P` is the component's prop type. It defaults to an untyped bag so a
1887
+ * catalogue can stay loose, but naming it is what makes the payload builders
1888
+ * read real fields instead of indexing into `unknown` and casting — and a cast
1889
+ * is exactly what an event catalogue must not need, because it is also the
1890
+ * list of what leaves the browser. */
1891
+ export declare type TrackingEntry<P = Record<string, unknown>> = string | ((props: P) => TrackingEvent);
1654
1892
 
1655
1893
  /** A resolved analytics event: a name + arbitrary payload. */
1656
1894
  export declare type TrackingEvent = {
@@ -1659,21 +1897,38 @@ export declare type TrackingEvent = {
1659
1897
 
1660
1898
  /** The declarative map. `onMount`/`onUnmount` are lifecycle; every other key is
1661
1899
  * a CALLBACK PROP name to wrap (so calling `onClick` also fires its event). */
1662
- export declare interface TrackingMap {
1663
- readonly onMount?: TrackingEntry;
1664
- readonly onUnmount?: TrackingEntry;
1665
- readonly [callback: string]: TrackingEntry | undefined;
1900
+ export declare interface TrackingMap<P = Record<string, unknown>> {
1901
+ readonly onMount?: TrackingEntry<P>;
1902
+ readonly onUnmount?: TrackingEntry<P>;
1903
+ readonly [callback: string]: TrackingEntry<P> | undefined;
1666
1904
  }
1667
1905
 
1668
1906
  /** Where resolved events go — the analytics sink (the client→server transport
1669
1907
  * provides this; tests provide a recorder). */
1670
1908
  export declare type TrackingSink = (event: TrackingEvent) => void;
1671
1909
 
1672
- export declare interface TrackingSpec {
1910
+ export declare interface TrackingSpec<P = Record<string, unknown>> {
1673
1911
  readonly name: string;
1674
- readonly map: TrackingMap;
1912
+ readonly map: TrackingMap<P>;
1675
1913
  }
1676
1914
 
1915
+ /**
1916
+ * Jump one instance to the state recorded at `index` — the devtools panel's
1917
+ * back/forward.
1918
+ *
1919
+ * Deliberately NOT built on `undo`. Undo is a stack that CONSUMES entries, so
1920
+ * stepping forward again would be impossible; time travel needs the log to stay
1921
+ * intact and a cursor to move over it. `side` picks which half of the recorded
1922
+ * step to restore, so stepping back from entry i means `('before', i)` and
1923
+ * forward means `('after', i)`.
1924
+ *
1925
+ * The jump is itself recorded — the log should show what happened, including
1926
+ * that somebody travelled — but marked so `undo()` skips it, exactly as an undo
1927
+ * is skipped. Returns false for an index that no longer exists (the log is
1928
+ * bounded, so a long session scrolls past the beginning).
1929
+ */
1930
+ export declare const travelToStoreState: (index: number, side: "before" | "after") => boolean;
1931
+
1677
1932
  export declare const UNDO_APPLY_TAG = "__voltro.undo.apply";
1678
1933
 
1679
1934
  export declare const UNDO_LOG_TAG = "__voltro.undo.log";
@@ -1733,6 +1988,9 @@ declare type UpdatePayload<Messages extends WorkflowClientMessages, Name extends
1733
1988
  readonly payload: infer Payload;
1734
1989
  } ? Payload : unknown : unknown;
1735
1990
 
1991
+ /** Applied to the current state to produce the next one. */
1992
+ export declare type Updater<S> = (state: S) => S;
1993
+
1736
1994
  declare type UpdateResult<Messages extends WorkflowClientMessages, Name extends string> = NonNullable<Messages['updates']> extends Readonly<Record<Name, infer Update>> ? Update extends {
1737
1995
  readonly result: infer Result;
1738
1996
  } ? Omit<WorkflowUpdateResult, 'result'> & {
@@ -2112,7 +2370,7 @@ export declare const useTableSkeleton: (apiName: string, queryTag: string) => Re
2112
2370
  * the returned props. The sink is the analytics transport (injected, so the
2113
2371
  * kernel stays transport-agnostic — and testable).
2114
2372
  */
2115
- export declare const useTracking: (spec: TrackingSpec, props: Record<string, unknown>, sink: TrackingSink) => Record<string, unknown>;
2373
+ export declare const useTracking: <P extends object>(spec: TrackingSpec<P>, props: P, sink: TrackingSink) => P;
2116
2374
 
2117
2375
  export declare const useUndo: (options: UseUndoOptions) => UndoControls;
2118
2376
 
@@ -2376,6 +2634,6 @@ export declare interface WorkflowWaitingFor {
2376
2634
  * Lifecycle keys (onMount/onUnmount) are skipped — those fire from effects.
2377
2635
  * Pure: returns a new props object, mutates nothing.
2378
2636
  */
2379
- export declare const wrapTrackedCallbacks: (spec: TrackingSpec, props: Record<string, unknown>, sink: TrackingSink) => Record<string, unknown>;
2637
+ export declare const wrapTrackedCallbacks: <P extends object>(spec: TrackingSpec<P>, props: P, sink: TrackingSink) => P;
2380
2638
 
2381
2639
  export { }