u
This commit is contained in:
196
docs/artikelAutomat.md
Normal file
196
docs/artikelAutomat.md
Normal 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`.
|
||||
Reference in New Issue
Block a user