refactor: fold machine-remote-data into remote-data.ts, PascalCase load lifecycle (RD-11)
`machine-remote-data.ts` defined a third encoding of an in-flight fetch: `LoadLifecycle`. It had three call sites, all one identical line, and the type was never imported by name. Move the mapping into `remote-data.ts` as `fromLoadLifecycle`, beside its neighbour `fromResource` — a `RemoteData` constructor, not a sixth encoding. The lowercase `loading`/`failed`/`loaded` tags on `BriefState`, `OrgTemplateState` and `StamdataEditorState` existed only because `LoadLifecycle` required them. Now that the constraint is inline and PascalCase, the three machines' load-lifecycle tags become `Loading`, `Failed` and `Loaded` — matching their own PascalCase message tags in the same file. `stamdata-editor.machine.spec.ts` no longer asserts a PascalCase message producing a lowercase state. `BriefStatus` (the letter's draft/submitted/approved/rejected/sent status, parsed off the wire from `BriefViewDto`) is a separate tag family and is untouched — its tag count stays 54 before and after this change. Delete `machine-remote-data.ts` and merge its spec into `remote-data.spec.ts`. Regenerate `behaviour-spec.mdx` (the `machineRemoteData` section heading becomes `fromLoadLifecycle`) and confirm `gen:snippets` reports no drift, since `remote-data.ts` carries a showcase region. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -819,6 +819,12 @@ classes.
|
||||
- is empty-safe: undefined, null, and empty string all yield the empty string
|
||||
- returns empty for an unparseable string rather than "Invalid Date"
|
||||
|
||||
#### fromLoadLifecycle
|
||||
|
||||
- maps Loading → Loading
|
||||
- maps Failed → Failure carrying an Error with the reason
|
||||
- maps Loaded → Success carrying the whole loaded state
|
||||
|
||||
#### httpClientFetch
|
||||
|
||||
- sends the pending idempotency key as a header for a write, not a fresh one per attempt
|
||||
@@ -836,12 +842,6 @@ classes.
|
||||
- keeps query + hash on both targets
|
||||
- the root maps nl → / and en → /en/
|
||||
|
||||
#### machineRemoteData
|
||||
|
||||
- maps loading → Loading
|
||||
- maps failed → Failure carrying an Error with the reason
|
||||
- maps loaded → Success carrying the whole loaded state
|
||||
|
||||
#### parseBsn (elfproef)
|
||||
|
||||
- accepts a valid BSN (passes the elfproef)
|
||||
|
||||
@@ -69,7 +69,7 @@ The idiom this repo uses instead — see `brief.page.ts`, `dashboard.page.ts`,
|
||||
// in the component class
|
||||
protected readonly loaded = computed(() => {
|
||||
const s = this.model(); // or store.someRemoteData()
|
||||
return s.tag === 'loaded' ? s : undefined;
|
||||
return s.tag === 'Loaded' ? s : undefined;
|
||||
});
|
||||
```
|
||||
|
||||
@@ -94,8 +94,8 @@ timing/outcome of `/api/*` calls. Try it on `/brief` or `/dashboard`.
|
||||
A store's own state machine (its `*.machine.ts`) should own the **domain** lifecycle of
|
||||
what it holds (draft → submitted → approved, in the brief's case) — not the network
|
||||
fetch's loading/failure, which is a generic concern `RemoteData` already models. Where a
|
||||
machine's own `loading`/`failed` tags purely mirror the fetch (nothing extra beyond "not
|
||||
loaded yet" / "the GET failed"), project them onto a `RemoteData` computed at the store
|
||||
layer for `<app-async>` to render, the way `BriefStore.remoteData` does — the machine
|
||||
machine's own `Loading`/`Failed`/`Loaded` tags purely mirror the fetch (nothing extra
|
||||
beyond "not loaded yet" / "the GET failed"), project them with `fromLoadLifecycle` at the
|
||||
store layer for `<app-async>` to render, the way `BriefStore.remoteData` does — the machine
|
||||
keeps deciding what the _letter_ is doing, `RemoteData` keeps deciding what the _fetch_ is
|
||||
doing.
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { machineRemoteData } from './machine-remote-data';
|
||||
import { loading, success } from '../testing/remote-data';
|
||||
|
||||
describe('machineRemoteData', () => {
|
||||
it('maps loading → Loading', () => {
|
||||
expect(machineRemoteData({ tag: 'loading' })).toEqual(loading());
|
||||
});
|
||||
|
||||
it('maps failed → Failure carrying an Error with the reason', () => {
|
||||
const rd = machineRemoteData({ tag: 'failed', reason: 'boom' });
|
||||
expect(rd.tag).toBe('Failure');
|
||||
if (rd.tag === 'Failure') expect(rd.error.message).toBe('boom');
|
||||
});
|
||||
|
||||
it('maps loaded → Success carrying the whole loaded state', () => {
|
||||
const loaded = { tag: 'loaded', foo: 42 } as const;
|
||||
expect(machineRemoteData(loaded)).toEqual(success(loaded));
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import { RemoteData } from '@shared/application/remote-data';
|
||||
|
||||
/** The standard load-lifecycle tags an editor machine exposes. */
|
||||
export type LoadLifecycle =
|
||||
{ tag: 'loading' } | { tag: 'failed'; reason: string } | { tag: 'loaded' };
|
||||
|
||||
/**
|
||||
* Project an Elm-machine state onto `RemoteData` for the `<app-async>` seam. The machine
|
||||
* keeps owning its own domain lifecycle (draft/submitted/…); this is purely the
|
||||
* loading/failed/loaded → async mapping, which was byte-identical across BriefStore,
|
||||
* OrgTemplateStore and StamdataStore (WP-31). Wrap the call in a `computed`.
|
||||
*/
|
||||
export function machineRemoteData<S extends LoadLifecycle>(
|
||||
s: S,
|
||||
): RemoteData<Error, Extract<S, { tag: 'loaded' }>> {
|
||||
switch (s.tag) {
|
||||
case 'loading':
|
||||
return { tag: 'Loading' };
|
||||
case 'failed':
|
||||
return { tag: 'Failure', error: new Error(s.reason) };
|
||||
default: // 'loaded'
|
||||
return { tag: 'Success', value: s as Extract<S, { tag: 'loaded' }> };
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { RemoteData, map2, map, successOf } from './remote-data';
|
||||
import { RemoteData, fromLoadLifecycle, map2, map, successOf } from './remote-data';
|
||||
import { loading, failure, empty, success } from '../testing/remote-data';
|
||||
|
||||
const loadingRd: RemoteData<string, number> = loading();
|
||||
@@ -33,3 +33,20 @@ describe('successOf', () => {
|
||||
expect(successOf(empty())).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromLoadLifecycle', () => {
|
||||
it('maps Loading → Loading', () => {
|
||||
expect(fromLoadLifecycle({ tag: 'Loading' })).toEqual(loading());
|
||||
});
|
||||
|
||||
it('maps Failed → Failure carrying an Error with the reason', () => {
|
||||
const rd = fromLoadLifecycle({ tag: 'Failed', reason: 'boom' });
|
||||
expect(rd.tag).toBe('Failure');
|
||||
if (rd.tag === 'Failure') expect(rd.error.message).toBe('boom');
|
||||
});
|
||||
|
||||
it('maps Loaded → Success carrying the whole loaded state', () => {
|
||||
const loadedState = { tag: 'Loaded', foo: 42 } as const;
|
||||
expect(fromLoadLifecycle(loadedState)).toEqual(success(loadedState));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,26 @@ export function fromResource<T>(
|
||||
return { tag: 'Loading' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Project an Elm-machine's load lifecycle onto `RemoteData`, for the `<app-async>` seam. The
|
||||
* machine keeps owning its own domain lifecycle (draft/submitted/…); this is purely the
|
||||
* Loading/Failed/Loaded → async mapping, which was byte-identical across BriefStore,
|
||||
* OrgTemplateStore and StamdataStore (WP-31). A `RemoteData` constructor, not a sixth
|
||||
* encoding — wrap the call in a `computed`.
|
||||
*/
|
||||
export function fromLoadLifecycle<
|
||||
S extends { tag: 'Loading' } | { tag: 'Failed'; reason: string } | { tag: 'Loaded' },
|
||||
>(s: S): RemoteData<Error, Extract<S, { tag: 'Loaded' }>> {
|
||||
switch (s.tag) {
|
||||
case 'Loading':
|
||||
return { tag: 'Loading' };
|
||||
case 'Failed':
|
||||
return { tag: 'Failure', error: new Error(s.reason) };
|
||||
default: // 'Loaded'
|
||||
return { tag: 'Success', value: s as Extract<S, { tag: 'Loaded' }> };
|
||||
}
|
||||
}
|
||||
|
||||
// #region showcase:fold
|
||||
/** Exhaustive fold: you must handle every case, checked at compile time. */
|
||||
export function foldRemote<E, T, R>(
|
||||
|
||||
Reference in New Issue
Block a user