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