From 0f9321ac34db099320b662d88dfd1d60c5c3e0b4 Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Tue, 22 Sep 2026 11:38:20 +0200 Subject: [PATCH] u --- README.md | 144 ++++++- src/App.jsx | 75 ++++ src/examples/automats/combinedMultiAutomat.js | 146 +++++++ src/examples/automats/profileAsyncAutomat.js | 93 +++++ .../components/AsyncApiCollapsibleView.jsx | 304 ++++++++++++++ .../components/AsyncApiPrimaryView.jsx | 327 +++++++++++++++ .../components/MultiApiCombinedDashboard.jsx | 389 ++++++++++++++++++ .../components/MultiApiIndividualControls.jsx | 204 +++++++++ src/index.css | 35 ++ src/lib/Automat.js | 153 +++++++ 10 files changed, 1865 insertions(+), 5 deletions(-) create mode 100644 src/examples/automats/combinedMultiAutomat.js create mode 100644 src/examples/automats/profileAsyncAutomat.js create mode 100644 src/examples/components/AsyncApiCollapsibleView.jsx create mode 100644 src/examples/components/AsyncApiPrimaryView.jsx create mode 100644 src/examples/components/MultiApiCombinedDashboard.jsx create mode 100644 src/examples/components/MultiApiIndividualControls.jsx diff --git a/README.md b/README.md index 674db90..9f01b61 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Creates an observable state container with optional persistence. - `persist` *(boolean)*: Persistence strategy when `name` is provided: - `false` (default): Persists in the `window` object (session memory, survives component unmounts and HMR). - `true`: Persists in `IndexedDB` (survives page reloads and browser restarts). + - `loader` *(function, optional)*: Asynchronous loader `(automat) => Promise`. Invoked when `getData()` is called while state is `'idle'`. ```js // Window-persisted (session memory) @@ -92,16 +93,57 @@ const sessionStore = new Automat( { name: 'filter', persist: false } ); -// IndexedDB-persisted (survives browser reload) -const cartStore = new Automat( - { items: [] }, - { addItem(item) { cartStore.setState({ items: [...cartStore.state.items, item] }); } }, - { name: 'cart', persist: true } +// Async API-backed with loader +const userStore = new Automat( + { status: 'idle', data: null }, + {}, + { + name: 'user', + loader: async (store) => { + const res = await fetch('/api/user'); + return { data: await res.json() }; + }, + } ); ``` --- +### `automat.getData(options?)` / `automat.load(options?)` + +Reads state for PureComponent constructor initialization, triggering asynchronous loading if state is idle: + +1. **First Mount (Data Not Loaded)**: + - Sets `{ status: 'pending' }` and kicks off the async loader. + - Synchronously returns the pending state for the first render. + - When the fetch completes, `setState()` notifies subscribers, causing a 2nd render with data. +2. **Subsequent Mounts / Remounts (Data Already Loaded)**: + - Directly returns `{ status: 'success', data }` synchronously in the constructor. + - Component renders completed data immediately. **Zero 2nd render occurs!** + +```jsx +class UserProfile extends PureComponent { + constructor(props) { + super(props); + // ๐Ÿ’ก Synchronous read in constructor: + this.state = userStore.getData(); + } + componentDidMount() { + this.unsubscribe = userStore.subscribe(this); + } + componentWillUnmount() { + this.unsubscribe(); + } + render() { + const { status, data } = this.state; + if (status === 'pending') return ; + return
{data.name}
; + } +} +``` + +--- + ### `automat.state` / `automat.getState()` Returns the current state snapshot. @@ -245,6 +287,28 @@ const filterStore = Automat.get('filter'); --- +### `Automat.combine(upstreamAutomats, combiner, options?)` + +Combines multiple upstream automats into a single derived container. + +- **Concurrent Orchestration**: When accessed via `.getData()` while any upstream is idle, triggers all idle upstreams to fetch concurrently in parallel. +- **Instant Constructor Resolution**: If all upstream automats have already resolved (`status === 'success'`), `.getData()` computes the projection synchronously in the component constructor, returning complete combined data with zero delay and a single render. + +```js +const dashboardAutomat = Automat.combine( + [userStatsAutomat, systemMetricsAutomat], + (statsState, metricsState) => ({ + stats: statsState.data, + metrics: metricsState.data, + }) +); + +// In component constructor: +this.state = dashboardAutomat.getData(); +``` + +--- + ### `automat.dispose()` Tears down all upstream subscriptions set up via `subscribeTo()`, clears all subscribers, and removes named instance registrations from the window registry. @@ -365,6 +429,76 @@ export class CartView extends PureComponent { --- +### 4. Async Fetch with Constructor Caching (Instant Re-Mount) + +When a component accesses `myAutomat.getData()` in its constructor while data is loading, it receives `{ status: 'pending' }` and renders the pending state. When the fetch completes, it renders again (2 renders total). + +When another component mounts (or the component remounts via a collapsible toggle) with data already loaded, `getData()` returns `{ status: 'success', data }` synchronously in the constructor โ€” displaying final data in **1 render with zero 2nd render**: + +```jsx +class UserCard extends PureComponent { + constructor(props) { + super(props); + // ๐Ÿ’ก Synchronous read in constructor: + // If already loaded, returns immediately -> 1 render only! + this.state = profileAsyncAutomat.getData(); + } + componentDidMount() { + this.unsubscribe = profileAsyncAutomat.subscribe(this); + } + componentWillUnmount() { + this.unsubscribe(); + } + render() { + const { status, data } = this.state; + if (status === 'pending') return
; + return
{data.name}
; + } +} +``` + +--- + +### 5. Combined Multi-API Automats (`Automat.combine`) + +Combines two delayed APIs. If accessed while upstreams are idle, it triggers both to load concurrently in parallel. If both had already been accessed earlier, it resolves instantly in the constructor: + +```js +const combinedDashboard = Automat.combine( + [userStatsAutomat, systemMetricsAutomat], + (stats, metrics) => ({ + stats: stats.data, + metrics: metrics.data, + }) +); + +class Dashboard extends PureComponent { + constructor(props) { + super(props); + // ๐Ÿ’ก If both upstreams were loaded earlier, resolves instantly in constructor: + this.state = combinedDashboard.getData(); + } + componentDidMount() { + this.unsubscribe = combinedDashboard.subscribe(this); + } + componentWillUnmount() { + this.unsubscribe(); + } + render() { + const { status, stats, metrics } = this.state; + if (status === 'pending') return
Loading dual sources in parallel...
; + return ( +
+

{stats.name}

+
Cluster: {metrics.clusterHealth}
+
+ ); + } +} +``` + +--- + ## Best Practices ### โœ… DO diff --git a/src/App.jsx b/src/App.jsx index 98ec4d3..8fd884b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,6 +9,10 @@ import IndexSelector from './examples/components/IndexSelector.jsx'; import DynamicAutomatSubscriber from './examples/components/DynamicAutomatSubscriber.jsx'; import CartManager from './examples/components/CartManager.jsx'; import CartBadgeDisplay from './examples/components/CartBadgeDisplay.jsx'; +import AsyncApiPrimaryView from './examples/components/AsyncApiPrimaryView.jsx'; +import AsyncApiCollapsibleView from './examples/components/AsyncApiCollapsibleView.jsx'; +import MultiApiIndividualControls from './examples/components/MultiApiIndividualControls.jsx'; +import MultiApiCombinedDashboard from './examples/components/MultiApiCombinedDashboard.jsx'; class App extends PureComponent { render() { @@ -225,6 +229,77 @@ class App extends PureComponent {
+ +
+ + {/* โ”€โ”€ Example 6: Async Fetch & Constructor Cache โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} +
+
+
+ 06 +

Async API Fetch & Constructor Cache (Instant Re-Mount)

+
+

+ An automat fetching asynchronous data via a simulated delayed API. When the primary component mounts, + calling profileAsyncAutomat.getData() in its constructor returns{' '} + {`{ status: 'pending' }`}, rendering a loading state (1st render), then re-rendering + when the fetch finishes (2nd render). When a second component mounts (or when unmounted and remounted via the collapsible toggle), + the data is already loaded in memory โ€” the constructor receives the resolved payload synchronously, rendering in{' '} + 1 render with zero 2nd render. +

+
+ Mount 1 Constructor + โ†’ + status: 'pending' (Render #1) + โ†’ + API Resolves + โ†’ + Render #2 (Data) + | + Mount 2 (Collapsible) + โ†’ + Constructor Has Data (1 Render Only) +
+
+ +
+ + +
+
+ +
+ + {/* โ”€โ”€ Example 7: Combined Multi-API Automats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} +
+
+
+ 07 +

Combined Multi-API Automats (Concurrent Orchestration)

+
+

+ Three automats in an orchestrated graph: two with delayed APIs (userStatsAutomat at ~700ms and{' '} + systemMetricsAutomat at ~1300ms), and one combined automat created with{' '} + Automat.combine(). When accessed while upstreams are idle, it triggers both to load in parallel, + merging results when both finish. However, if both have already been accessed earlier, the combined automat resolves{' '} + instantly in the component constructor with zero delay and a single render. +

+
+ combinedDashboardAutomat.getData() + โ†’ + Check Upstreams + โ†’ + If Idle: Fetch Concurrently (~1300ms parallel) + | + If Cached: Instant Constructor Return (1 Render) +
+
+ +
+ + +
+