chizu 0.2.71 → 0.3.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.
@@ -1,7 +1,5 @@
1
1
  import { Operation, Process, Inspect, Box } from 'immertation';
2
2
  import { ActionId, Task, Tasks } from '../boundary/components/tasks/types.ts';
3
- import { Option } from '@mobily/ts-belt/Option';
4
- import { Result as TsBeltResult } from '@mobily/ts-belt/Result';
5
3
  import { Regulator } from '../boundary/components/regulators/types.ts';
6
4
  import { Fault } from '../error/types.ts';
7
5
  import * as React from "react";
@@ -58,43 +56,15 @@ export declare class Brand {
58
56
  static readonly Channel: unique symbol;
59
57
  /** Node capture events used by Lifecycle.Node */
60
58
  static readonly Node: unique symbol;
61
- /** Identifies cache entry identifiers created with Entry() */
62
- static readonly Cache: unique symbol;
63
59
  }
64
60
  /**
65
- * Phantom brand symbol for value type tracking on cache entry identifiers.
66
- * Uses a function type `(t: T) => T` to enforce invariance, preventing
67
- * a cache entry declared for one type from being used with a different type.
68
- * @internal
69
- */
70
- declare const CacheValueBrand: unique symbol;
71
- /**
72
- * A branded cache entry identifier.
73
- *
74
- * When the second type parameter `C` is provided, the entry becomes callable
75
- * to produce channeled identifiers for independent cache slots per channel.
76
- *
77
- * @template T - The cached value type.
78
- * @template C - The channel type for channeled entries (defaults to never).
79
- */
80
- export type CacheId<T = unknown, C extends Filter = never> = {
81
- readonly [Brand.Cache]: symbol;
82
- readonly [CacheValueBrand]?: (t: T) => T;
83
- } & ([C] extends [never] ? unknown : {
84
- (channel: C): ChanneledCacheId<T, C>;
85
- });
86
- /**
87
- * Result of calling a channeled cache entry with a channel argument.
88
- * Contains the entry identity and the channel data for scoped cache access.
61
+ * Internal symbol for the global `Lifecycle.Fault` broadcast. Exposed so the
62
+ * dispatch pipeline can fire faults and short-circuit the regulator policy
63
+ * without depending on the `Lifecycle` class at runtime.
89
64
  *
90
- * @template T - The cached value type.
91
- * @template C - The channel type.
65
+ * @internal
92
66
  */
93
- export type ChanneledCacheId<T = unknown, C = unknown> = {
94
- readonly [Brand.Cache]: symbol;
95
- readonly [CacheValueBrand]?: (t: T) => T;
96
- readonly channel: C;
97
- };
67
+ export declare const FaultSymbol: unique symbol;
98
68
  /**
99
69
  * Factory functions for lifecycle actions.
100
70
  *
@@ -116,6 +86,9 @@ export type ChanneledCacheId<T = unknown, C = unknown> = {
116
86
  * // Now regulating Lifecycle.Mount only blocks THIS component's mount:
117
87
  * context.regulator.disallow(Actions.Mount);
118
88
  * ```
89
+ *
90
+ * `Lifecycle.Fault` is a singleton broadcast (not a factory). All components
91
+ * subscribe to the same shared symbol to receive global fault notifications.
119
92
  */
120
93
  export declare class Lifecycle {
121
94
  /** Creates a Mount lifecycle action. Triggered once on component mount (`useLayoutEffect`). */
@@ -151,6 +124,28 @@ export declare class Lifecycle {
151
124
  static Node(): HandlerPayload<unknown, {
152
125
  Name: string;
153
126
  }>;
127
+ /**
128
+ * Global fault broadcast. Receives a `Fault` whenever any action in the
129
+ * `<Boundary>` errors, times out, is supplanted, or is blocked by the
130
+ * regulator. Subscribe via `actions.useAction(Lifecycle.Fault, handler)`.
131
+ *
132
+ * Unlike the per-component `Lifecycle.Error()` factory, `Fault` is a single
133
+ * shared broadcast — every subscriber points at the same symbol. The
134
+ * regulator policy does not apply to `Fault`; faults always reach
135
+ * subscribers so error visibility cannot be silenced.
136
+ *
137
+ * @example
138
+ * ```tsx
139
+ * const actions = useActions<void, typeof Actions>();
140
+ *
141
+ * actions.useAction(Lifecycle.Fault, (context, fault) => {
142
+ * if (fault.reason === Reason.Errored) {
143
+ * console.error(`Action "${fault.action}" failed`, fault.error);
144
+ * }
145
+ * });
146
+ * ```
147
+ */
148
+ static Fault: BroadcastPayload<Fault>;
154
149
  }
155
150
  /**
156
151
  * Distribution modes for actions.
@@ -205,24 +200,6 @@ export declare enum Phase {
205
200
  /** Component has fully unmounted. */
206
201
  Unmounted = "unmounted"
207
202
  }
208
- /**
209
- * Operations for toggling boolean feature flags via `actions.feature()`.
210
- *
211
- * @example
212
- * ```ts
213
- * actions.feature("sidebar", Feature.Toggle);
214
- * actions.feature("sidebar", Feature.On);
215
- * actions.feature("sidebar", Feature.Off);
216
- * ```
217
- */
218
- export declare enum Feature {
219
- /** Sets the feature to `true`. */
220
- On = "on",
221
- /** Sets the feature to `false`. */
222
- Off = "off",
223
- /** Inverts the current boolean value. */
224
- Toggle = "toggle"
225
- }
226
203
  /**
227
204
  * Primary key type for identifying entities in collections.
228
205
  * Can be undefined (not yet assigned), a symbol (temporary/local), or a concrete value T.
@@ -292,8 +269,12 @@ export type ChanneledAction<P = unknown, C = unknown> = {
292
269
  * Broadcast actions are sent to all mounted components. Values are cached so that
293
270
  * late-mounting components receive the most recent payload.
294
271
  *
272
+ * Late-mounting components receive the most recent cached payload via their
273
+ * `useAction` handler during mount. Use `peek()` in a `Lifecycle.Mount` handler
274
+ * to check whether a cached value exists before performing default fetches.
275
+ *
295
276
  * This type extends `HandlerPayload<P, C>` with an additional brand to enforce at compile-time
296
- * that only broadcast actions can be passed to `context.actions.read()`.
277
+ * that only broadcast actions can be passed to `context.actions.resolution()`.
297
278
  *
298
279
  * @template P - The payload type for the action
299
280
  * @template C - The channel type for channeled dispatches (defaults to never)
@@ -302,8 +283,8 @@ export type ChanneledAction<P = unknown, C = unknown> = {
302
283
  * ```ts
303
284
  * const SignedOut = Action<User>("SignedOut", Distribution.Broadcast);
304
285
  *
305
- * // Read the latest value inside a handler
306
- * const user = await context.actions.read(SignedOut);
286
+ * // Resolve the latest value inside a handler
287
+ * const user = await context.actions.resolution(SignedOut);
307
288
  * ```
308
289
  */
309
290
  export type BroadcastPayload<P = unknown, C extends Filter = never> = HandlerPayload<P, C> & {
@@ -313,20 +294,21 @@ export type BroadcastPayload<P = unknown, C extends Filter = never> = HandlerPay
313
294
  * Branded type for multicast action objects created with `Action()` and `Distribution.Multicast`.
314
295
  * Multicast actions are dispatched to all components within a named scope boundary.
315
296
  *
316
- * When dispatching a multicast action, you MUST provide the scope name as the third argument:
297
+ * When dispatching a multicast action, you MUST provide the scope carrier as the third argument:
317
298
  * ```ts
318
- * actions.dispatch(Actions.Multicast.Update, payload, { scope: "MyScope" });
299
+ * actions.dispatch(Actions.Multicast.Update, payload, { scope: Actions.Multicast });
319
300
  * ```
320
301
  *
321
- * Components receive multicast events only if they are descendants of a `<Scope name="...">`.
302
+ * Components receive multicast events only if they are descendants of a `<Scope of={...}>`.
322
303
  *
323
304
  * @template P - The payload type for the action
324
305
  * @template C - The channel type for channeled dispatches (defaults to never)
325
306
  *
326
307
  * @example
327
308
  * ```tsx
328
- * // Define multicast actions in a shared class
309
+ * // Define multicast actions and their scope together
329
310
  * class MulticastActions {
311
+ * static Scope = "Counter" as const;
330
312
  * static Update = Action<number>("Update", Distribution.Multicast);
331
313
  * }
332
314
  *
@@ -336,26 +318,47 @@ export type BroadcastPayload<P = unknown, C extends Filter = never> = HandlerPay
336
318
  * }
337
319
  *
338
320
  * // In JSX - create a named scope boundary
339
- * <Scope name="Counter">
321
+ * <Scope of={MulticastActions}>
340
322
  * <CounterA />
341
323
  * <CounterB />
342
324
  * </Scope>
343
325
  *
344
- * // Inside CounterA - dispatch to all components in "Counter" scope
345
- * actions.dispatch(Actions.Multicast.Update, 42, { scope: "Counter" });
326
+ * // Inside CounterA - dispatch to all components in the scope
327
+ * actions.dispatch(Actions.Multicast.Update, 42, { scope: Actions.Multicast });
346
328
  * // CounterA and CounterB both receive the event
347
329
  * ```
348
330
  */
349
331
  export type MulticastPayload<P = unknown, C extends Filter = never> = HandlerPayload<P, C> & {
350
332
  readonly [Brand.Multicast]: true;
351
333
  };
334
+ /**
335
+ * A scope carrier — any object exposing a `Scope` string literal.
336
+ *
337
+ * By convention this is the feature's `MulticastActions` class, which owns
338
+ * the scope name alongside its multicast action declarations:
339
+ *
340
+ * ```ts
341
+ * export class MulticastActions {
342
+ * static Scope = "my-feature" as const;
343
+ * static Update = Action<T>("Update", Distribution.Multicast);
344
+ * }
345
+ * ```
346
+ *
347
+ * Requiring a carrier (rather than a bare string) prevents typos and enforces
348
+ * a single source of truth for each scope name.
349
+ *
350
+ * @template S - The scope name literal type.
351
+ */
352
+ export type ScopeCarrier<S extends string = string> = {
353
+ readonly Scope: S;
354
+ };
352
355
  /**
353
356
  * Options for multicast dispatch.
354
357
  * Required when dispatching a multicast action.
355
358
  */
356
359
  export type MulticastOptions = {
357
- /** The name of the scope to multicast to. Must match a `<Scope name="...">` ancestor. */
358
- scope: string;
360
+ /** Carrier object exposing the scope name via `.Scope`. Typically the feature's `MulticastActions` class. */
361
+ scope: ScopeCarrier;
359
362
  };
360
363
  /**
361
364
  * Extracts the payload type `P` from a `HandlerPayload<P>` or `ChanneledAction<P, C>`.
@@ -439,38 +442,106 @@ type AssertSync<F> = IsAsync<F> extends true ? "Error: async functions are not a
439
442
  export type Props = Record<string, unknown>;
440
443
  /**
441
444
  * Extracts the nodes type from a Model.
442
- * If the model has a `nodes` property, returns its type.
445
+ * If the model has a `meta.nodes` property, returns its type.
443
446
  * Otherwise returns an empty record.
444
447
  *
445
448
  * @example
446
449
  * ```ts
447
450
  * type Model = {
448
451
  * count: number;
449
- * nodes: { button: HTMLButtonElement | null };
452
+ * meta: { nodes: { container: HTMLButtonElement | null } };
450
453
  * };
451
454
  *
452
- * type N = Nodes<Model>; // { button: HTMLButtonElement | null }
455
+ * type N = ExtractNodes<Model>; // { container: HTMLButtonElement | null }
453
456
  * ```
454
457
  */
455
- export type Nodes<M> = M extends {
456
- nodes: infer N extends Record<string, unknown>;
458
+ export type ExtractNodes<M> = M extends {
459
+ meta: {
460
+ nodes: infer N extends Record<string, unknown>;
461
+ };
457
462
  } ? N : Record<never, unknown>;
458
463
  /**
459
- * Extracts the `features` property from a Model type.
460
- * If the model has a `features` property whose values are all booleans,
464
+ * Transforms `meta.nodes` values to include `| null`, reflecting
465
+ * that node refs are initially `null` until a DOM element is captured.
466
+ * Models without `meta.nodes` pass through unchanged.
467
+ *
468
+ * @internal
469
+ */
470
+ export type NullableNodes<M> = M extends {
471
+ meta: infer Meta & {
472
+ nodes: infer N extends Record<string, unknown>;
473
+ };
474
+ } ? Omit<M, "meta"> & {
475
+ meta: Omit<Meta, "nodes"> & {
476
+ nodes: {
477
+ [K in keyof N]: N[K] | null;
478
+ };
479
+ };
480
+ } : M;
481
+ /**
482
+ * Extracts the `meta.features` property from a Model type.
483
+ * If the model has a `meta.features` property whose values are all booleans,
461
484
  * returns its type. Otherwise returns an empty record, making the
462
- * `feature()` method effectively uncallable.
485
+ * `features` methods effectively uncallable.
463
486
  *
464
487
  * @example
465
488
  * ```ts
466
- * enum Feature { Sidebar = 'Sidebar', Modal = 'Modal' }
467
- * type Model = { count: number; features: { [K in Feature]: boolean } };
468
- * type F = Features<Model>; // { Sidebar: boolean; Modal: boolean }
489
+ * type Model = { count: number; meta: { features: { sidebar: boolean; modal: boolean } } };
490
+ * type F = FeatureFlags<Model>; // { sidebar: boolean; modal: boolean }
469
491
  * ```
470
492
  */
471
493
  export type FeatureFlags<M> = M extends {
472
- features: infer F extends Record<string, boolean>;
494
+ meta: {
495
+ features: infer F extends Record<string, boolean>;
496
+ };
473
497
  } ? F : Record<never, boolean>;
498
+ /**
499
+ * Resolves to `unknown` when the model has no `meta.features` or all
500
+ * feature values are booleans. Resolves to a descriptive string literal when
501
+ * any value is not a boolean, causing an intersection with the model type
502
+ * to become `never` and producing a compile-time error.
503
+ *
504
+ * @internal
505
+ */
506
+ export type ValidateFeatures<M> = M extends {
507
+ meta: {
508
+ features: infer F;
509
+ };
510
+ } ? F extends Record<string, boolean> ? unknown : "meta.features values must all be boolean" : unknown;
511
+ /**
512
+ * Utility type for the `meta` model property. Combines optional `nodes`
513
+ * and `features` sub-objects into a single meta shape.
514
+ *
515
+ * - When `N` is provided, produces `{ nodes: { [K in keyof N]: N[K] | null } }`.
516
+ * - When `F` is provided, produces `{ features: F }`.
517
+ * - Pass `void` to omit either sub-object.
518
+ *
519
+ * @template N - Node type record (e.g. `{ input: HTMLInputElement }`)
520
+ * @template F - Feature flags record (e.g. `{ sidebar: boolean }`)
521
+ *
522
+ * @example
523
+ * ```ts
524
+ * import type { Meta } from "chizu";
525
+ *
526
+ * type Model = {
527
+ * count: number;
528
+ * meta: Meta<{ input: HTMLInputElement }, { sidebar: boolean }>;
529
+ * };
530
+ * // Equivalent to:
531
+ * // meta: { nodes: { input: HTMLInputElement | null }; features: { sidebar: boolean } }
532
+ * ```
533
+ */
534
+ export type Meta<N extends Record<string, unknown> | void = void, F extends Record<string, boolean> | void = void> = ([N] extends [void] ? unknown : {
535
+ nodes: {
536
+ [K in keyof N]: N[K] | null;
537
+ };
538
+ }) & ([F] extends [void] ? unknown : {
539
+ features: F;
540
+ });
541
+ export declare namespace Meta {
542
+ type Nodes<N extends Record<string, unknown>> = Meta<N>;
543
+ type Features<F extends Record<string, boolean>> = Meta<void, F>;
544
+ }
474
545
  /**
475
546
  * Constraint type for action containers.
476
547
  * Actions are symbols grouped in an object (typically a class with static properties).
@@ -573,18 +644,20 @@ export type HandlerContext<M extends Model | void, _AC extends Actions | void, D
573
644
  */
574
645
  readonly tasks: ReadonlySet<Task>;
575
646
  /**
576
- * Captured DOM nodes registered via `actions.node()`.
577
- * Nodes may be `null` if not yet captured or if the node was unmounted.
647
+ * Meta properties including captured DOM nodes and feature flags.
578
648
  *
579
649
  * @example
580
650
  * ```ts
581
651
  * actions.useAction(Actions.Focus, (context) => {
582
- * context.nodes.input?.focus();
652
+ * context.meta.nodes.input?.focus();
583
653
  * });
584
654
  * ```
585
655
  */
586
- readonly nodes: {
587
- [K in keyof Nodes<M>]: Nodes<M>[K] | null;
656
+ readonly meta: {
657
+ readonly nodes: {
658
+ [K in keyof ExtractNodes<M>]: ExtractNodes<M>[K] | null;
659
+ };
660
+ readonly features: Readonly<FeatureFlags<M>>;
588
661
  };
589
662
  /**
590
663
  * The regulator API for controlling which actions may be dispatched.
@@ -608,78 +681,42 @@ export type HandlerContext<M extends Model | void, _AC extends Actions | void, D
608
681
  readonly regulator: Regulator;
609
682
  readonly actions: {
610
683
  produce<F extends (draft: {
611
- model: M;
684
+ model: Omit<M, "meta">;
612
685
  readonly inspect: Readonly<Inspect<M>>;
613
686
  }) => void>(ƒ: F & AssertSync<F>): void;
614
687
  dispatch(action: ActionOrChanneled, payload?: unknown, options?: MulticastOptions): Promise<void>;
615
688
  annotate<T>(operation: Operation, value: T): T;
616
689
  /**
617
- * Fetches a value from the cache or executes the callback if not cached / expired.
618
- *
619
- * The callback must return an `Option<T>` or `Result<T, E>`. Only `Some` / `Ok`
620
- * values are stored; `None` / `Error` results are skipped and `{ data: null }`
621
- * is returned. Exactly one layer of `Option` / `Result` is unwrapped.
622
- *
623
- * @param entry - The cache entry identifier (from `Entry()`).
624
- * @param ttl - Time-to-live in milliseconds.
625
- * @param fn - Async callback that produces the value to cache.
626
- * @returns An object with `data` set to the cached or freshly-fetched value, or `null`.
627
- *
628
- * @example
629
- * ```ts
630
- * const { data } = await context.actions.cacheable(
631
- * CacheStore.Pairs,
632
- * 30_000,
633
- * async () => Some(await api.fetchPairs()),
634
- * );
635
- * ```
636
- */
637
- cacheable<T>(entry: CacheId<T> | ChanneledCacheId<T>, ttl: number, fn: () => Promise<Option<T>>): Promise<{
638
- data: T | null;
639
- }>;
640
- cacheable<T>(entry: CacheId<T> | ChanneledCacheId<T>, ttl: number, fn: () => Promise<TsBeltResult<T, unknown>>): Promise<{
641
- data: T | null;
642
- }>;
643
- /**
644
- * Removes a cached value from the store.
645
- *
646
- * @param entry - The cache entry identifier to invalidate.
690
+ * Feature toggle methods for mutating boolean flags on the model.
647
691
  *
648
692
  * @example
649
693
  * ```ts
650
- * context.actions.invalidate(CacheStore.Pairs);
651
- * context.actions.invalidate(CacheStore.User({ UserId: 5 }));
694
+ * context.actions.features.on("sidebar");
695
+ * context.actions.features.off("sidebar");
696
+ * context.actions.features.invert("sidebar");
652
697
  * ```
653
698
  */
654
- invalidate(entry: CacheId<unknown> | ChanneledCacheId<unknown>): void;
655
- /**
656
- * Mutates a boolean feature flag on the model and triggers a re-render.
657
- *
658
- * Requires the model to have a `features` property whose keys are the feature names.
659
- * Read feature state from `model.features` directly.
660
- *
661
- * @example
662
- * ```ts
663
- * context.actions.feature(Feature.Sidebar, Feature.Toggle);
664
- * context.actions.feature(Feature.Sidebar, Feature.Off);
665
- * ```
666
- */
667
- feature<K extends keyof FeatureFlags<M>>(name: K, operation: Feature): void;
699
+ features: {
700
+ on<K extends keyof FeatureFlags<M>>(name: K): void;
701
+ off<K extends keyof FeatureFlags<M>>(name: K): void;
702
+ invert<K extends keyof FeatureFlags<M>>(name: K): void;
703
+ };
668
704
  /**
669
- * Reads the latest broadcast or multicast value, waiting for annotations to settle.
705
+ * Returns the resolved broadcast or multicast value, waiting for any
706
+ * pending annotations to settle before resolving.
670
707
  *
671
708
  * If a value has already been dispatched it resolves immediately.
672
709
  * Otherwise it waits until the next dispatch of the action.
673
710
  * Resolves with `null` if the task is aborted before a value arrives.
674
711
  *
675
- * @param action - The broadcast or multicast action to read.
676
- * @param options - For multicast actions, must include `{ scope: "ScopeName" }`.
712
+ * @param action - The broadcast or multicast action to resolve.
713
+ * @param options - For multicast actions, must include `{ scope: MulticastActions }`.
677
714
  * @returns The dispatched value, or `null` if aborted.
678
715
  *
679
716
  * @example
680
717
  * ```ts
681
718
  * actions.useAction(Actions.FetchPosts, async (context) => {
682
- * const user = await context.actions.read(Actions.Broadcast.User);
719
+ * const user = await context.actions.resolution(Actions.Broadcast.User);
683
720
  * if (!user) return;
684
721
  * const posts = await fetchPosts(user.id, {
685
722
  * signal: context.task.controller.signal,
@@ -688,15 +725,15 @@ export type HandlerContext<M extends Model | void, _AC extends Actions | void, D
688
725
  * });
689
726
  * ```
690
727
  */
691
- read<T>(action: BroadcastPayload<T>): Promise<T | null>;
692
- read<T>(action: MulticastPayload<T>, options: MulticastOptions): Promise<T | null>;
728
+ resolution<T>(action: BroadcastPayload<T>): Promise<T | null>;
729
+ resolution<T>(action: MulticastPayload<T>, options: MulticastOptions): Promise<T | null>;
693
730
  /**
694
731
  * Returns the latest broadcast or multicast value immediately without
695
732
  * waiting for annotations to settle. Use this when you need the current
696
733
  * cached value and do not need to wait for pending operations to complete.
697
734
  *
698
735
  * @param action - The broadcast or multicast action to peek at.
699
- * @param options - For multicast actions, must include `{ scope: "ScopeName" }`.
736
+ * @param options - For multicast actions, must include `{ scope: MulticastActions }`.
700
737
  * @returns The cached value, or `null` if no value has been dispatched.
701
738
  *
702
739
  * @example
@@ -795,19 +832,19 @@ export type Handlers<M extends Model | void, AC extends Actions | void, D extend
795
832
  [K in OwnKeys<AC>]: OwnKeys<AC[K]> extends never ? (context: HandlerContext<M, AC, D>, ...args: [Payload<AC[K] & HandlerPayload<unknown>>] extends [never] ? [] : [payload: Payload<AC[K] & HandlerPayload<unknown>>]) => void | Promise<void> | AsyncGenerator | Generator : Handlers<M, AC[K] & Actions, D>;
796
833
  };
797
834
  export type UseActions<M extends Model | void, AC extends Actions | void, D extends Props = Props> = [
798
- Readonly<M>,
835
+ Readonly<NullableNodes<M>>,
799
836
  {
800
837
  /**
801
838
  * Dispatches an action with an optional payload.
802
839
  *
803
- * For multicast actions, you MUST provide the scope as the third argument:
840
+ * For multicast actions, you MUST provide the scope carrier as the third argument:
804
841
  * ```ts
805
- * actions.dispatch(Actions.Multicast.Update, payload, { scope: "MyScope" });
842
+ * actions.dispatch(Actions.Multicast.Update, payload, { scope: Actions.Multicast });
806
843
  * ```
807
844
  *
808
845
  * @param action - The action to dispatch
809
846
  * @param payload - The payload to send with the action
810
- * @param options - For multicast actions, must include `{ scope: "ScopeName" }`
847
+ * @param options - For multicast actions, must include `{ scope: MulticastActions }`
811
848
  */
812
849
  dispatch<P>(action: HandlerPayload<P>, payload?: P, options?: MulticastOptions): Promise<void>;
813
850
  dispatch<P>(action: BroadcastPayload<P>, payload?: P, options?: MulticastOptions): Promise<void>;
@@ -815,39 +852,32 @@ export type UseActions<M extends Model | void, AC extends Actions | void, D exte
815
852
  dispatch<P, C extends Filter>(action: ChanneledAction<P, C>, payload?: P, options?: MulticastOptions): Promise<void>;
816
853
  inspect: Inspect<M>;
817
854
  /**
818
- * Captured DOM nodes registered via `node()`.
819
- * Nodes may be `null` if not yet captured or if the node was unmounted.
855
+ * Meta properties including captured DOM nodes and feature flags.
820
856
  *
821
857
  * @example
822
858
  * ```tsx
823
- * type Model = {
824
- * count: number;
825
- * nodes: { input: HTMLInputElement };
826
- * };
827
- * const [model, actions] = useActions<Model, typeof Actions>(model);
828
- *
829
- * // Access captured nodes
830
- * actions.nodes.input?.focus();
859
+ * actions.meta.nodes.input?.focus();
860
+ * actions.meta.features.sidebar; // boolean
831
861
  * ```
832
862
  */
833
- nodes: {
834
- [K in keyof Nodes<M>]: Nodes<M>[K] | null;
863
+ meta: {
864
+ readonly nodes: {
865
+ [K in keyof ExtractNodes<M>]: ExtractNodes<M>[K] | null;
866
+ };
867
+ readonly features: Readonly<FeatureFlags<M>>;
835
868
  };
836
869
  /**
837
- * Captures a DOM node for later access via `nodes` or `context.nodes`.
838
- * Use as a ref callback on JSX nodes.
870
+ * Captures a DOM node for later access via `meta.nodes`.
871
+ * Use as a ref callback on JSX elements.
839
872
  *
840
- * @param name - The node key (must match a key in Model['nodes'])
873
+ * @param name - The node key (must match a key in the model's `meta.nodes`)
841
874
  * @param node - The DOM node or null (when unmounting)
842
875
  *
843
876
  * @example
844
877
  * ```tsx
845
878
  * type Model = {
846
879
  * count: number;
847
- * nodes: {
848
- * container: HTMLDivElement;
849
- * input: HTMLInputElement;
850
- * };
880
+ * meta: Meta<{ container: HTMLDivElement; input: HTMLInputElement }>;
851
881
  * };
852
882
  *
853
883
  * const [model, actions] = useActions<Model, typeof Actions>(model);
@@ -859,20 +889,23 @@ export type UseActions<M extends Model | void, AC extends Actions | void, D exte
859
889
  * );
860
890
  * ```
861
891
  */
862
- node<K extends keyof Nodes<M>>(name: K, node: Nodes<M>[K] | null): void;
892
+ node<K extends keyof ExtractNodes<M>>(name: K, node: ExtractNodes<M>[K] | null): void;
863
893
  /**
864
- * Mutates a boolean feature flag on the model and triggers a re-render.
865
- *
866
- * Read feature state from `model.features` directly.
894
+ * Feature toggle methods for mutating boolean flags on the model.
867
895
  *
868
896
  * @example
869
897
  * ```tsx
870
- * actions.feature(Feature.Sidebar, Feature.Toggle);
898
+ * actions.features.on("sidebar");
899
+ * actions.features.invert("sidebar");
871
900
  *
872
- * {model.features.Sidebar && <Sidebar />}
901
+ * {model.meta.features.sidebar && <Sidebar />}
873
902
  * ```
874
903
  */
875
- feature<K extends keyof FeatureFlags<M>>(name: K, operation: Feature): void;
904
+ features: {
905
+ on<K extends keyof FeatureFlags<M>>(name: K): void;
906
+ off<K extends keyof FeatureFlags<M>>(name: K): void;
907
+ invert<K extends keyof FeatureFlags<M>>(name: K): void;
908
+ };
876
909
  /**
877
910
  * Streams broadcast values declaratively in JSX using a render-prop pattern.
878
911
  *
@@ -925,4 +958,22 @@ export type UseActions<M extends Model | void, AC extends Actions | void, D exte
925
958
  * ```
926
959
  */
927
960
  useAction<A extends ActionId | HandlerPayload | ChanneledAction>(action: A, handler: (context: HandlerContext<M, AC, D>, ...args: [Payload<A>] extends [never] ? [] : [payload: Payload<A>]) => void | Promise<void> | AsyncGenerator | Generator): void;
961
+ /**
962
+ * Connects a {@link Resource} declared at module scope to this component.
963
+ * Returns a thunk that fetches fresh data on every call &ndash; concurrent
964
+ * calls share the in-flight promise. The Resource's `onSuccess` and
965
+ * `onError` callbacks receive `(response, data, dispatch)` where `data`
966
+ * is this component's reactive `data` proxy.
967
+ *
968
+ * @example
969
+ * ```ts
970
+ * const fetchUser = actions.useResource(resources.user);
971
+ *
972
+ * actions.useAction(Actions.Mount, async (context) => {
973
+ * const data = await fetchUser();
974
+ * context.actions.produce(({ model }) => { model.user = data; });
975
+ * });
976
+ * ```
977
+ */
978
+ useResource<T, E, Args extends readonly unknown[]>(resource: import('../resource/index.ts').ResourceHandle<T, E, Args>): (...args: Args) => Promise<T>;
928
979
  };
@@ -1,21 +1,4 @@
1
1
  import { Pk } from '../types/index.ts';
2
- /**
3
- * Configuration constants for Chizu action symbols.
4
- */
5
- export declare const config: {
6
- /** Prefix for all Chizu action symbols. */
7
- actionPrefix: string;
8
- /** Prefix for broadcast action symbols. */
9
- broadcastActionPrefix: string;
10
- /** Prefix for multicast action symbols. */
11
- multicastActionPrefix: string;
12
- /** Prefix for channeled action symbols. */
13
- channelPrefix: string;
14
- /** Prefix for cache operation symbols. */
15
- cachePrefix: string;
16
- /** Prefix for lifecycle action symbols. */
17
- lifecyclePrefix: string;
18
- };
19
2
  /**
20
3
  * Returns a promise that resolves after the specified number of milliseconds.
21
4
  * The sleep will reject with an AbortError when the signal is aborted,
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,29 @@
1
+ /**
2
+ * Internal symbol description factories. Each function returns a namespaced
3
+ * string suitable for `Symbol()` descriptions or `startsWith` checks.
4
+ *
5
+ * @internal
6
+ */
7
+ export declare const describe: {
8
+ /** Unicast action description. `describe.action("Fetch")` &rarr; `"chizu.action/Fetch"` */
9
+ action: (name?: string) => string;
10
+ /** Broadcast action description. `describe.broadcast("User")` &rarr; `"chizu.action/broadcast/User"` */
11
+ broadcast: (name?: string) => string;
12
+ /** Multicast action description. `describe.multicast("Update")` &rarr; `"chizu.action/multicast/Update"` */
13
+ multicast: (name?: string) => string;
14
+ /** Channeled action description. `describe.channel("user")` &rarr; `"chizu.channel/user"` */
15
+ channel: (name?: string) => string;
16
+ /** Cache entry description. `describe.cache("users")` &rarr; `"chizu.cache/users"` */
17
+ cache: (name?: string) => string;
18
+ /** Lifecycle action description. `describe.lifecycle("Mount")` &rarr; `"chizu.action.lifecycle/Mount"` */
19
+ lifecycle: (name?: string) => string;
20
+ /** Mount replay sentinel description. Used to create the {@link replay} symbol. */
21
+ replay: (name?: string) => string;
22
+ };
23
+ /**
24
+ * Flat record used for shallow property comparison in {@link changes}.
25
+ * @internal
26
+ */
1
27
  type Changes = Record<string, unknown>;
2
28
  /**
3
29
  * Get high-level changed paths between two objects.