createUploadController runs an effect() that calls dispatch. store.ts dispatch was `model.set(update(model(), msg))` — the reactive model() read made the effect depend on its own write and re-schedule forever, livelocking the main thread. Angular's NG0103 guard doesn't cover effect self-rescheduling, so no error was thrown; Firefox just killed the unresponsive tab. Only /registreren and /herregistratie (which mount the upload controller) were affected. dispatch now uses model.update((m) => update(m, msg)) — the current value is read untracked, so no effect can loop on its own dispatch. Hardens all wizard stores. Adds a regression spec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
34 lines
1.4 KiB
TypeScript
34 lines
1.4 KiB
TypeScript
import { Signal, signal } from '@angular/core';
|
|
|
|
/**
|
|
* A tiny "Elm-style" store. The whole idea: all state lives in ONE value
|
|
* (the Model). The only way to change it is to send a message (Msg) to a PURE
|
|
* function `update(model, msg)` that returns the next Model. Nothing else
|
|
* mutates state, so to understand the app you only read the update function.
|
|
*
|
|
* Side effects (HTTP, timers) do NOT go in `update` — that stays pure and easy
|
|
* to test. Instead, effectful "command" functions call the network and then
|
|
* `dispatch` a message describing what happened (e.g. Loaded / Failed).
|
|
*/
|
|
export interface Store<Model, Msg> {
|
|
/** The current state, as a read-only Angular signal. */
|
|
readonly model: Signal<Model>;
|
|
/** Send a message; the model becomes update(model, msg). */
|
|
dispatch(msg: Msg): void;
|
|
}
|
|
|
|
export function createStore<Model, Msg>(
|
|
init: Model,
|
|
update: (model: Model, msg: Msg) => Model,
|
|
): Store<Model, Msg> {
|
|
const model = signal(init);
|
|
return {
|
|
model: model.asReadonly(),
|
|
// Use `.update` (raw current value, no tracked read) not `set(update(model(), …))`:
|
|
// dispatch is a command and must never subscribe its caller to `model`. Reading
|
|
// `model()` here inside an effect that also dispatches makes the effect depend on
|
|
// its own write and livelock the main thread (crashed the upload wizards).
|
|
dispatch: (msg) => model.update((m) => update(m, msg)),
|
|
};
|
|
}
|