chizu 0.2.42 → 0.2.44

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/README.md CHANGED
@@ -17,11 +17,14 @@ Strongly typed React framework using generators and efficiently updated views al
17
17
  1. [Model annotations](#model-annotations)
18
18
  1. [Lifecycle actions](#lifecycle-actions)
19
19
  1. [Distributed actions](#distributed-actions)
20
+ 1. [Consuming actions](#consuming-actions)
21
+ 1. [Stateful props](#stateful-props)
20
22
  1. [Action decorators](#action-decorators)
21
23
  1. [Real-time applications](#real-time-applications)
22
24
  1. [Utility functions](#utility-functions)
23
25
  1. [Referential equality](#referential-equality)
24
26
  1. [Action regulator](#action-regulator)
27
+ 1. [Context providers](#context-providers)
25
28
 
26
29
  ## Benefits
27
30
 
@@ -280,7 +283,7 @@ class {
280
283
  - **`Lifecycle.Mount`** – Triggered once when the component mounts (`useLayoutEffect`).
281
284
  - **`Lifecycle.Node`** – Triggered after the component renders (`useEffect`).
282
285
  - **`Lifecycle.Error`** – Triggered when an action throws an error. Receives `ErrorDetails` as payload.
283
- - **`Lifecycle.Unmount`** – Triggered when the component unmounts.
286
+ - **`Lifecycle.Unmount`** – Triggered when the component unmounts. All in-flight actions are automatically aborted before this handler runs.
284
287
 
285
288
  **Note:** Actions should ideally be self-contained and handle expected errors internally using patterns like [Option](https://mobily.github.io/ts-belt/api/option) or [Result](https://mobily.github.io/ts-belt/api/result) types to update the model accordingly. `Lifecycle.Error` is intended for timeouts, aborts, and uncaught catastrophic errors – not routine error handling.
286
289
 
@@ -304,6 +307,8 @@ export class Actions extends DistributedActions {
304
307
  }
305
308
  ```
306
309
 
310
+ `createDistributedAction()` returns a `DistributedPayload<T>` type, which is distinct from the `Payload<T>` returned by `createAction()`. This enables compile-time enforcement &ndash; only distributed actions can be passed to `actions.consume()`.
311
+
307
312
  Any component that defines a handler for `DistributedActions.SignedOut` will receive the action when it's dispatched from any other component. For direct access to the broadcast emitter, use `useBroadcast()`:
308
313
 
309
314
  ```ts
@@ -320,6 +325,41 @@ broadcast.on(DistributedActions.SignedOut, (payload) => {
320
325
  });
321
326
  ```
322
327
 
328
+ ## Consuming actions
329
+
330
+ The `consume()` method subscribes to a distributed action and re-renders content whenever a new value is dispatched, making it ideal for global context scenarios where you want to fetch data once and access it throughout your app without prop drilling. The callback receives a `Box<T>` from [Immertation](https://github.com/Wildhoney/Immertation) containing the `value` and an `inspect` proxy for checking annotation status.
331
+
332
+ ```tsx
333
+ export default function Visitor(): React.ReactElement {
334
+ const [model, actions] = useVisitorActions();
335
+
336
+ return (
337
+ <div>
338
+ {actions.consume(Actions.Visitor, (visitor) =>
339
+ visitor.inspect.pending() ? <>Loading&hellip;</> : visitor.value.name,
340
+ )}
341
+ </div>
342
+ );
343
+ }
344
+ ```
345
+
346
+ > **Important:** The `consume()` method only accepts distributed actions created with `createDistributedAction()`. Attempting to pass a local action created with `createAction()` will result in a TypeScript error. This is enforced at compile-time to prevent confusion &ndash; local actions are scoped to a single component and cannot be consumed across the application.
347
+
348
+ > **Note:** When a component mounts, `consume()` displays the most recent value for that action, even if it was dispatched before the component mounted. This is managed by the `Consumer` context provider. If no value has been dispatched yet, `consume()` renders `null` until the first dispatch occurs.
349
+
350
+ ## Stateful props
351
+
352
+ Chizu uses the `Box<T>` type from [Immertation](https://github.com/Wildhoney/Immertation) to wrap values with metadata about their async state. Passing `Box<T>` to React components allows them to observe an object's state &ndash; checking if a value is pending, how many operations are in flight, and what the optimistic draft value is &ndash; all without additional state management.
353
+
354
+ The `Box<T>` type has two properties:
355
+
356
+ - **`box.value`** &ndash; The payload (e.g., `Country` object with `name`, `flag`, etc.).
357
+ - **`box.inspect`** &ndash; An `Inspect<T>` proxy for checking annotation status:
358
+ - `box.inspect.pending()` &ndash; Returns `true` if any pending annotations exist.
359
+ - `box.inspect.remaining()` &ndash; Returns the count of pending annotations.
360
+ - `box.inspect.draft()` &ndash; Returns the draft value from the latest annotation.
361
+ - `box.inspect.is(Op.Update)` &ndash; Checks if the annotation matches a specific operation.
362
+
323
363
  ## Action decorators
324
364
 
325
365
  Chizu provides decorators to add common functionality to your actions. Import `use` from Chizu and apply decorators to action properties:
@@ -650,7 +690,7 @@ function useSearchActions(props: Props) {
650
690
 
651
691
  ## Action regulator
652
692
 
653
- The `Regulator` class is accessed via `context.actions.regulator` inside your action handlers. It provides fine-grained control over asynchronous actions by managing `AbortController` instances and action policies. You can programmatically allow or disallow actions, and abort running actions either globally or by type.
693
+ The regulator is accessed via `context.regulator` inside your action handlers. It provides fine-grained control over asynchronous actions by managing `AbortController` instances and action policies. You can programmatically allow or disallow actions, and abort running actions across all components in the context.
654
694
 
655
695
  ### Usage
656
696
 
@@ -658,10 +698,7 @@ The `Regulator` class is accessed via `context.actions.regulator` inside your ac
658
698
  const fetchAction = useAction<Model, typeof Actions, "Fetch">(
659
699
  async (context) => {
660
700
  // Disallow future dispatches of these actions
661
- context.actions.regulator.policy.disallow.matching(
662
- Actions.Fetch,
663
- Actions.Save,
664
- );
701
+ context.regulator.policy.disallow.matching([Actions.Fetch, Actions.Save]);
665
702
 
666
703
  // Future dispatches via useAction will be aborted immediately
667
704
  // and the error handler will receive Reason.Disallowed
@@ -671,10 +708,7 @@ const fetchAction = useAction<Model, typeof Actions, "Fetch">(
671
708
  const resetAction = useAction<Model, typeof Actions, "Reset">(
672
709
  async (context) => {
673
710
  // Allow the actions again
674
- context.actions.regulator.policy.allow.matching(
675
- Actions.Fetch,
676
- Actions.Save,
677
- );
711
+ context.regulator.policy.allow.matching([Actions.Fetch, Actions.Save]);
678
712
  },
679
713
  );
680
714
  ```
@@ -682,19 +716,85 @@ const resetAction = useAction<Model, typeof Actions, "Reset">(
682
716
  You can also abort running actions:
683
717
 
684
718
  ```ts
685
- context.actions.regulator.abort.matching(Actions.Fetch);
686
- context.actions.regulator.abort.all();
719
+ // Abort specific actions across all components
720
+ context.regulator.abort.matching([Actions.Fetch]);
721
+
722
+ // Abort all actions across all components
723
+ context.regulator.abort.all();
724
+
725
+ // Abort only the current action instance
726
+ context.regulator.abort.self();
687
727
  ```
688
728
 
689
729
  ### API
690
730
 
691
- - `add(action: Action, controller: AbortController): void` — Registers an AbortController for a given action.
692
- - `controller(action: Action): AbortController` — Creates and registers an AbortController for an action, aborting immediately if disallowed by policy.
693
- - `abort.all(): void` — Aborts all controllers and removes them.
694
- - `abort.matching(...actions: Action[]): void` — Aborts controllers for specific actions and removes them.
695
- - `policy.allow.all(...actions: Action[]): void` — Allows one or more actions.
696
- - `policy.allow.matching(...actions: Action[]): void` — Allows specific actions.
697
- - `policy.disallow.all(...actions: Action[]): void` — Disallows one or more actions.
698
- - `policy.disallow.matching(...actions: Action[]): void` — Disallows specific actions.
731
+ **Abort methods:**
732
+
733
+ - `abort.all()` — Aborts all running actions across all components in the context.
734
+ - `abort.matching(actions)` — Aborts specific actions (array) across all components.
735
+ - `abort.self()` — Aborts only the current action instance.
736
+ - `abort.own()` — Aborts all actions belonging to the current component only (called automatically on unmount).
737
+
738
+ **Allow methods:**
739
+
740
+ - `policy.allow.all()` — Clears all disallow policies across all components.
741
+ - `policy.allow.matching(actions)` — Allows specific actions (array) across all components.
742
+ - `policy.allow.self()` — Allows the current action.
743
+ - `policy.allow.own()` — Clears all disallow policies belonging to the current component only.
744
+
745
+ **Disallow methods:**
746
+
747
+ - `policy.disallow.all()` — Clears all allow policies across all components.
748
+ - `policy.disallow.matching(actions)` — Disallows specific actions (array) across all components.
749
+ - `policy.disallow.self()` — Disallows the current action.
750
+ - `policy.disallow.own()` — Clears all allow policies belonging to the current component only.
751
+
752
+ The regulator is useful for advanced scenarios where you need to centrally manage cancellation and permission of asynchronous actions, such as rate limiting, feature toggling, or global aborts.
753
+
754
+ ## Context providers
755
+
756
+ Chizu provides context providers for advanced use cases where you need isolated contexts. These are edge cases &ndash; most applications don't need them.
757
+
758
+ ### `Broadcaster`
759
+
760
+ Creates an isolated broadcast context for distributed actions. Useful for libraries that want their own broadcast context without interfering with the host application:
761
+
762
+ ```tsx
763
+ import { Broadcaster } from "chizu";
764
+
765
+ function MyLibraryRoot({ children }) {
766
+ return <Broadcaster>{children}</Broadcaster>;
767
+ }
768
+ ```
769
+
770
+ Components inside `<Broadcaster>` have their own isolated broadcast channel. Distributed actions dispatched inside won't reach components outside, and vice versa.
771
+
772
+ ### `Regulators`
773
+
774
+ Creates an isolated regulator context. All regulator operations (`abort.all()`, `policy.disallow.matching()`, etc.) only affect components within the same `Regulators` provider:
775
+
776
+ ```tsx
777
+ import { Regulators } from "chizu";
778
+
779
+ function Example({ children }) {
780
+ return <Regulators>{children}</Regulators>;
781
+ }
782
+ ```
783
+
784
+ This is useful for libraries that need action control without affecting the host application's actions. An `abort.all()` inside the provider won't abort actions outside it.
785
+
786
+ ### `Consumer`
787
+
788
+ Creates an isolated consumer context for storing distributed action values. The Consumer stores the latest payload for each distributed action, enabling the `consume()` method to display the most recent value even when components mount after the action was dispatched:
789
+
790
+ ```tsx
791
+ import { Consumer } from "chizu";
792
+
793
+ function MyLibraryRoot({ children }) {
794
+ return <Consumer>{children}</Consumer>;
795
+ }
796
+ ```
797
+
798
+ Components inside `<Consumer>` have their own isolated value store. Actions consumed inside won't see values dispatched outside, and vice versa. This is useful for libraries that want to use `consume()` without interfering with the host application's consumed values.
699
799
 
700
- The `Regulator` class is useful for advanced scenarios where you need to centrally manage cancellation and permission of asynchronous actions, such as rate limiting, feature toggling, or global aborts.
800
+ **Note:** In most applications, you don't need to provide a `Consumer` &ndash; one is created automatically at the default context level. Only use `<Consumer>` when you need isolation for library boundaries or testing.
@@ -1,21 +1,44 @@
1
- import { Action, Payload } from '../types/index.ts';
1
+ import { Payload, DistributedPayload } from '../types/index.ts';
2
+ import { Action } from '../regulator/types.ts';
2
3
  /**
3
- * Defines a new action with a given payload type.
4
+ * Defines a new local action with a given payload type.
5
+ * Local actions are scoped to the component that defines them and cannot be consumed
6
+ * by other components. Use `createDistributedAction()` for actions that need to be
7
+ * shared across components via `actions.consume()`.
4
8
  *
5
9
  * @template T The type of the payload that the action will carry.
6
10
  * @param name An optional name for the action, used for debugging purposes.
7
11
  * @returns A new action symbol.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const Increment = createAction<number>("Increment");
16
+ *
17
+ * // Dispatch within the same component
18
+ * actions.dispatch(Increment, 5);
19
+ * ```
8
20
  */
9
21
  export declare function createAction<T = never>(name?: string): Payload<T>;
10
22
  /**
11
23
  * Defines a new distributed action with a given payload type.
12
24
  * Distributed actions are broadcast to all mounted components that have defined a handler for them.
13
25
  *
26
+ * Returns a `DistributedPayload<T>` which is required by `actions.consume()`. Local actions
27
+ * created with `createAction()` cannot be consumed &ndash; this is enforced at compile-time.
28
+ *
14
29
  * @template T The type of the payload that the action will carry.
15
30
  * @param name An optional name for the action, used for debugging purposes.
16
- * @returns A new distributed action symbol.
31
+ * @returns A new distributed action symbol that can be used with `consume()`.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const SignedOut = createDistributedAction<User>("SignedOut");
36
+ *
37
+ * // Can be consumed across components
38
+ * actions.consume(SignedOut, (box) => <div>{box.value.name}</div>);
39
+ * ```
17
40
  */
18
- export declare function createDistributedAction<T = never>(name?: string): Payload<T>;
41
+ export declare function createDistributedAction<T = never>(name?: string): DistributedPayload<T>;
19
42
  /**
20
43
  * Checks whether an action is a distributed action.
21
44
  * Distributed actions are broadcast to all mounted components that have defined a handler for them.