This commit is contained in:
seb
2026-09-23 17:24:33 +02:00
parent 0391e845f0
commit b331f8f535
17 changed files with 2352 additions and 202 deletions

196
docs/artikelAutomat.md Normal file
View File

@@ -0,0 +1,196 @@
# `artikelAutomat`
> **Source File**: [`src/automats/artikelAutomat.js`](file:///home/seb/src/dashfront/src/automats/artikelAutomat.js)
> **Registration Key**: `'artikel'` (`Automat.get('artikel')` / `window.__AUTOMATS__.get('artikel')`)
---
## 1. Overview & Purpose
`artikelAutomat` serves as the centralized cache and reactive distributor for individual article detail payloads (`GET /api/article/:sku`).
### Core Responsibilities
- **In-flight Deduplication**: Multiple components or route guards requesting the same SKU concurrently share a single HTTP request via an internal `Map<string, Promise<object>>`.
- **Zero-Flicker Navigation**: Navigating between tabs or returning to previously viewed articles reuses the cached detail object instantly without round-trips.
- **Optimistic Updates**: Immediate UI updates (such as renaming an article via `setArticleName`) without awaiting network round-trips.
- **Granular Subscriptions**: Components can subscribe to an individual SKU via `createArticleSelector(sku)` rather than re-rendering on every cache change.
---
## 2. State Structure
```ts
interface ArtikelState {
/**
* Cached article detail payloads keyed by SKU string.
* A missing key indicates the article is not yet loaded into memory.
* Values include full article details: prices, photos, eans, brand, unit,
* categoryIds, manifests, child variants, etc.
*/
bySku: Record<string, object>;
}
```
### Initial State
```js
const initialState = {
bySku: {},
};
```
---
## 3. Configuration & Options
```js
new Automat(initialState, actions, { name: 'artikel' });
```
- **`name`**: `'artikel'` — registers the automat globally.
- **`persist`**: `false` (default in-memory window lifecycle).
---
## 4. Actions & Methods
Access actions via `artikelAutomat.actions.<method>()`.
### `loadArticle(sku: string): Promise<object>`
Loads an article detail into the cache. If already present in `bySku`, returns the cached article immediately. If an HTTP request for this SKU is currently in-flight, it returns that shared promise. Otherwise, dispatches `GET /api/article/:sku`, normalizes the response, stores it in `bySku`, and cleans up the in-flight map.
### `reloadArticle(sku: string): Promise<object>`
Bypasses the cached copy and silently fetches a fresh detail payload from the backend. The existing cache entry is preserved until the response arrives. Typically used during SSE mutation events or user pull-to-refresh.
### `setArticle(article: object): object`
Puts an article detail object directly into `bySku[article.sku]`. Requires `article` to be an object with a defined `sku`. Used after create/update API mutations so that all subscribers immediately observe the latest data.
### `setArticleName(sku: string, name: string): object | null`
Optimistically updates the `name` property of a cached article without making an API call. Returns the updated article object or `null` if the SKU was not found in cache.
### `clearArticle(sku: string): void`
Removes a specific SKU from `bySku`. Useful after deleting an article or when forcing a complete refetch on next access.
### `clear(): void`
Resets the entire `bySku` dictionary to empty (`{}`).
---
## 5. Selectors & Helper Functions
Exported alongside `artikelAutomat`:
### `getArticle(sku: string): object | null`
Synchronous read from `artikelAutomat.state.bySku[sku]`. Returns `null` if not found or if `sku` is nullish.
```js
const article = getArticle('10042');
```
### `hasArticle(sku: string): boolean`
Returns `true` if `sku` is already loaded in `bySku`.
```js
if (!hasArticle('10042')) {
void artikelAutomat.actions.loadArticle('10042');
}
```
### `createArticleSelector(sku: string): (state: ArtikelState) => object | null`
Produces a memoized selector function for a specific SKU. Pass this to `artikelAutomat.subscribe(target, selector)` so your component only re-renders when this specific article's data changes.
### `selectArticle(sku: string): ScopedSlice<object | null>`
Returns a scoped slice for the given SKU (`artikelAutomat.select(createArticleSelector(sku))`).
---
## 6. Usage Examples
### A. React `PureComponent` Detail Header
```jsx
import { PureComponent } from 'react';
import PropTypes from 'prop-types';
import artikelAutomat, { getArticle, createArticleSelector } from '../automats/artikelAutomat';
export class ArticleHeader extends PureComponent {
static propTypes = {
sku: PropTypes.string.isRequired,
};
constructor(props) {
super(props);
// Synchronous initial state read prevents layout shift / loading spinner flicker
this.state = {
article: getArticle(props.sku),
};
}
componentDidMount() {
this.subscribe(this.props.sku);
if (!this.state.article) {
artikelAutomat.actions.loadArticle(this.props.sku).catch(console.error);
}
}
componentDidUpdate(prevProps) {
if (prevProps.sku !== this.props.sku) {
this.subscribe(this.props.sku);
if (!hasArticle(this.props.sku)) {
artikelAutomat.actions.loadArticle(this.props.sku).catch(console.error);
}
}
}
componentWillUnmount() {
this.unsub?.();
}
subscribe(sku) {
this.unsub?.();
this.setState({ article: getArticle(sku) });
this.unsub = artikelAutomat.subscribe(this, createArticleSelector(sku));
}
render() {
const { article } = this.state;
if (!article) return <div>Artikel wird geladen...</div>;
return (
<div>
<h2>{article.name}</h2>
<span>SKU: {article.sku}</span>
</div>
);
}
}
```
### B. Inline Optimistic Rename
```js
import { artikelAutomat } from '../automats';
import { article as articleApi } from '../api';
async function updateName(sku, newName) {
// 1. Optimistic update in UI cache
artikelAutomat.actions.setArticleName(sku, newName);
try {
// 2. Persist to backend
const res = await articleApi.update(sku, { name: newName });
// 3. Re-set authoritative response
artikelAutomat.actions.setArticle(res.article);
} catch (err) {
// 4. Rollback on failure
await artikelAutomat.actions.reloadArticle(sku);
throw err;
}
}
```
---
## 7. Best Practices & Gotchas
1. **Always clean up subscriptions**: Save `this.unsub = artikelAutomat.subscribe(...)` and call `this.unsub()` in `componentWillUnmount`.
2. **Use `createArticleSelector`**: Never subscribe an article view component with the raw `artikelAutomat.subscribe(this)` without a selector; otherwise, any other article loaded or modified anywhere in the app will trigger an unneeded re-render.
3. **Do not mutate directly**: Never write `artikelAutomat.state.bySku[sku].name = 'foo'`. Always use `setArticle` or `setArticleName`.