model: add onSuccess/onFailure lifecycle hooks to modelRegister
This commit is contained in:
+5
-1
@@ -144,6 +144,8 @@ modelRegister('firewall', {
|
|||||||
return r.data;
|
return r.data;
|
||||||
},
|
},
|
||||||
defaultData: null, // optional, initial data value
|
defaultData: null, // optional, initial data value
|
||||||
|
// onSuccess: (name, data, param?) => { }, // optional — after model.data is set (also for null)
|
||||||
|
// onFailure: (name, error) => { }, // optional — after model.error is set (real throws only)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Parameterized example — tab-aware fetch:
|
// Parameterized example — tab-aware fetch:
|
||||||
@@ -164,6 +166,8 @@ modelRegister('logs', {
|
|||||||
| `definition.subsystem` | WS topic string. Use `'firewall'`, `'dnsmasq'`, etc. Use `'*'` to match all topics. |
|
| `definition.subsystem` | WS topic string. Use `'firewall'`, `'dnsmasq'`, etc. Use `'*'` to match all topics. |
|
||||||
| `definition.fetch(signal?, param?)` | Async function that fetches and returns data. Throws on error. Receives optional `AbortSignal` and optional parameter (e.g., tab key). |
|
| `definition.fetch(signal?, param?)` | Async function that fetches and returns data. Throws on error. Receives optional `AbortSignal` and optional parameter (e.g., tab key). |
|
||||||
| `definition.defaultData` | Optional initial data value (default: `null`) |
|
| `definition.defaultData` | Optional initial data value (default: `null`) |
|
||||||
|
| `definition.onSuccess(name, data, param?)` | Optional lifecycle hook called after `model.data` is assigned — including `data === null` (a resolved `null` is normal, not an error). `param` is the action object passed to `fetch` (or `undefined`), so hooks can tell which action produced the data. Fire-and-forget: hook errors are caught and logged via `console.warn`; they never clobber `model.error`, the returned promise, or the `finally` flag clearing. |
|
||||||
|
| `definition.onFailure(name, error)` | Optional lifecycle hook called after `model.error` is assigned. Only reachable on a real throw from `fetch` (e.g., network error). Same fire-and-forget error isolation as `onSuccess`. |
|
||||||
|
|
||||||
### `getModel(name)`
|
### `getModel(name)`
|
||||||
|
|
||||||
@@ -212,7 +216,7 @@ modelFetch('logs', 'nginx-access');
|
|||||||
- On failure, stores error in `model.error`.
|
- On failure, stores error in `model.error`.
|
||||||
- Flags cleared in `finally` block.
|
- Flags cleared in `finally` block.
|
||||||
- Does not abort in-progress fetches — other consumers may still need the data.
|
- Does not abort in-progress fetches — other consumers may still need the data.
|
||||||
- The `param` argument is passed to `fetch(signal, param)` for parameterized models. Dedup key is `name: param`.
|
- The `param` argument is passed to `fetch(signal, param)` for parameterized models. Dedup key is `name` (no param) or `name: JSON.stringify(param)` (with param) — object params (e.g. `{ action: 'refresh' }` vs `{ action: 'check' }`) therefore get distinct keys, and param-less `modelFetch(name)` calls retain the bare `name` key.
|
||||||
|
|
||||||
### `refreshByTopic(topic)`
|
### `refreshByTopic(topic)`
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
import { reactive } from './reactivity.js';
|
import { reactive } from './reactivity.js';
|
||||||
|
|
||||||
/** Registered models: name → { model, subsystem, fetch } */
|
/** Registered models: name → { model, subsystem, fetch, onSuccess?, onFailure? } */
|
||||||
const _models = new Map();
|
const _models = new Map();
|
||||||
|
|
||||||
/** In-flight fetch promises for dedup: name → Promise */
|
/** In-flight fetch promises for dedup: name → Promise */
|
||||||
@@ -29,6 +29,13 @@ const _fetchPromises = new Map();
|
|||||||
* @param {string} definition.subsystem - WS topic to listen for ('*' = all)
|
* @param {string} definition.subsystem - WS topic to listen for ('*' = all)
|
||||||
* @param {function} definition.fetch - async (signal?, param?) => Promise<data>
|
* @param {function} definition.fetch - async (signal?, param?) => Promise<data>
|
||||||
* @param {any} [definition.defaultData] - Initial data value (default: null)
|
* @param {any} [definition.defaultData] - Initial data value (default: null)
|
||||||
|
* @param {function} [definition.onSuccess] - (name, data, param?) => void.
|
||||||
|
* Called after model.data is assigned. Also fires when data === null —
|
||||||
|
* only a thrown fetch error skips it. Hook errors are caught and logged
|
||||||
|
* (console.warn) and never affect the fetch promise or model state.
|
||||||
|
* @param {function} [definition.onFailure] - (name, error) => void.
|
||||||
|
* Called after model.error is assigned, only on a real throw from fetch.
|
||||||
|
* Hook errors are caught and logged and never affect the fetch promise.
|
||||||
* @returns {object} reactive model
|
* @returns {object} reactive model
|
||||||
*/
|
*/
|
||||||
export function modelRegister(name, definition) {
|
export function modelRegister(name, definition) {
|
||||||
@@ -43,6 +50,8 @@ export function modelRegister(name, definition) {
|
|||||||
model,
|
model,
|
||||||
subsystem: definition.subsystem,
|
subsystem: definition.subsystem,
|
||||||
fetch: definition.fetch,
|
fetch: definition.fetch,
|
||||||
|
onSuccess: definition.onSuccess,
|
||||||
|
onFailure: definition.onFailure,
|
||||||
});
|
});
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
@@ -59,9 +68,13 @@ export function getModel(name) {
|
|||||||
return entry.model;
|
return entry.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build dedup key from model name and optional param. */
|
/**
|
||||||
|
* Build dedup key from model name and optional param.
|
||||||
|
* Objects (e.g. `{ action: 'refresh' }`) are JSON-serialized so distinct
|
||||||
|
* param objects get distinct keys; strings/undefined keep bare behavior.
|
||||||
|
*/
|
||||||
function _dedupKey(name, param) {
|
function _dedupKey(name, param) {
|
||||||
return param !== undefined ? `${name}:${String(param)}` : name;
|
return param !== undefined ? name + ':' + JSON.stringify(param) : name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,8 +110,20 @@ export function modelFetch(name, signalOrParam, signal) {
|
|||||||
try {
|
try {
|
||||||
const data = await entry.fetch(actualSignal, param);
|
const data = await entry.fetch(actualSignal, param);
|
||||||
model.data = data;
|
model.data = data;
|
||||||
|
// Fires for any resolved result, including data === null. Hook
|
||||||
|
// errors must not clobber model state or the returned promise.
|
||||||
|
try {
|
||||||
|
entry.onSuccess?.(name, data, param);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Model onSuccess hook (' + name + ') threw:', err);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
model.error = e.message || 'Fetch failed';
|
model.error = e.message || 'Fetch failed';
|
||||||
|
try {
|
||||||
|
entry.onFailure?.(name, e);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Model onFailure hook (' + name + ') threw:', err);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
model.loading = false;
|
model.loading = false;
|
||||||
model.refreshing = false;
|
model.refreshing = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user