u
This commit is contained in:
@@ -208,7 +208,7 @@ const actions = {
|
||||
* @param {{ parentId: number|string, name: string }} param0
|
||||
*/
|
||||
async createArticle({ parentId, name }) {
|
||||
const res = await articleApi.create({ parentId, name });
|
||||
const res = await articleApi.create({ categoryId: parentId, name });
|
||||
const created = res.article;
|
||||
if (created) appendArticlesToCategoryViews({ categoryId: parentId, articles: [created] });
|
||||
return created;
|
||||
@@ -223,7 +223,7 @@ const actions = {
|
||||
if (list.length === 0) throw new Error('Keine Namen übergeben.');
|
||||
const created = [];
|
||||
for (const name of list) {
|
||||
const res = await articleApi.create({ parentId, name });
|
||||
const res = await articleApi.create({ categoryId: parentId, name });
|
||||
if (res.article) created.push(res.article);
|
||||
}
|
||||
appendArticlesToCategoryViews({ categoryId: parentId, articles: created });
|
||||
|
||||
@@ -37,12 +37,16 @@ import supplierAutomat from './supplierAutomat';
|
||||
* @typedef {Object} SseState
|
||||
* @property {'disconnected'|'connecting'|'connected'|'error'} status Connection state.
|
||||
* @property {string|null} companyId Company the stream is bound to.
|
||||
* @property {Array<{id: number, type: string, data: string, lastEventId: string, time: number}>} events Recent stream events.
|
||||
*/
|
||||
|
||||
const EVENT_HISTORY_LIMIT = 200;
|
||||
|
||||
/** @type {SseState} */
|
||||
const initialState = {
|
||||
status: 'disconnected',
|
||||
companyId: null,
|
||||
events: [],
|
||||
};
|
||||
|
||||
/** Event name prefix for per-article signals. */
|
||||
@@ -54,9 +58,11 @@ let eventSource = null;
|
||||
|
||||
/**
|
||||
* Handlers for statically named events (`category`, `articles`, …).
|
||||
* @type {Map<string, Set<(event: MessageEvent) => void>>}
|
||||
* @type {Map<string, Set<{handler: (event: MessageEvent) => void, detach: (() => void)|null}>>}
|
||||
*/
|
||||
const handlers = new Map();
|
||||
let nextEventId = 0;
|
||||
const seenEvents = new WeakSet();
|
||||
|
||||
/**
|
||||
* Handlers per watched SKU, so one `article-<sku>` listener is attached while
|
||||
@@ -65,6 +71,31 @@ const handlers = new Map();
|
||||
*/
|
||||
const articleWatchers = new Map();
|
||||
|
||||
function recordEvent(event) {
|
||||
if (seenEvents.has(event)) return;
|
||||
seenEvents.add(event);
|
||||
|
||||
const entry = {
|
||||
id: ++nextEventId,
|
||||
type: event.type,
|
||||
data: typeof event.data === 'string' ? event.data : '',
|
||||
lastEventId: event.lastEventId ?? '',
|
||||
time: Date.now(),
|
||||
};
|
||||
sseAutomat.setState((state) => ({
|
||||
events: [...state.events.slice(-(EVENT_HISTORY_LIMIT - 1)), entry],
|
||||
}));
|
||||
}
|
||||
|
||||
function addTrackedListener(es, type, handler) {
|
||||
const listener = (event) => {
|
||||
recordEvent(event);
|
||||
handler(event);
|
||||
};
|
||||
es.addEventListener(type, listener);
|
||||
return () => es.removeEventListener(type, listener);
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (!eventSource) return;
|
||||
eventSource.close();
|
||||
@@ -80,8 +111,8 @@ function disconnect() {
|
||||
*/
|
||||
function attachHandlers(es) {
|
||||
for (const [event, set] of handlers) {
|
||||
for (const handler of set) {
|
||||
es.addEventListener(event, handler);
|
||||
for (const entry of set) {
|
||||
entry.detach = addTrackedListener(es, event, entry.handler);
|
||||
}
|
||||
}
|
||||
for (const [sku] of articleWatchers) {
|
||||
@@ -100,7 +131,8 @@ function attachHandlers(es) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function attachArticleListener(sku) {
|
||||
eventSource?.addEventListener(`${ARTICLE_EVENT_PREFIX}${sku}`, (event) => {
|
||||
if (!eventSource) return;
|
||||
addTrackedListener(eventSource, `${ARTICLE_EVENT_PREFIX}${sku}`, (event) => {
|
||||
const set = articleWatchers.get(sku);
|
||||
if (!set || set.size === 0) return;
|
||||
const changed = String(event.type).slice(ARTICLE_EVENT_PREFIX.length);
|
||||
@@ -121,7 +153,7 @@ function connect() {
|
||||
const es = new EventSource(url, { withCredentials: true });
|
||||
|
||||
// Category tree changed: refresh the tree and every open category view.
|
||||
es.addEventListener('category', () => {
|
||||
addTrackedListener(es, 'category', () => {
|
||||
void categoryAutomat.actions.loadTree().catch((err) => {
|
||||
console.error('[sse] category tree refresh failed', err);
|
||||
});
|
||||
@@ -129,44 +161,47 @@ function connect() {
|
||||
});
|
||||
|
||||
// Article index changed: refresh the index and open category views.
|
||||
es.addEventListener('articles', () => {
|
||||
addTrackedListener(es, 'articles', () => {
|
||||
invalidateAllCategoryViews();
|
||||
void articleIndexAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] article index refresh failed', err);
|
||||
});
|
||||
});
|
||||
|
||||
es.addEventListener('warehouse', () => {
|
||||
addTrackedListener(es, 'warehouse', () => {
|
||||
void warehouseAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] warehouse refresh failed', err);
|
||||
});
|
||||
});
|
||||
|
||||
es.addEventListener('attributename', () => {
|
||||
addTrackedListener(es, 'attributename', () => {
|
||||
void attributeAutomat.actions.load().catch((err) => {
|
||||
console.error('[sse] attributename refresh failed', err);
|
||||
});
|
||||
});
|
||||
|
||||
es.addEventListener('propertyname', () => {
|
||||
addTrackedListener(es, 'propertyname', () => {
|
||||
void propertyAutomat.actions.load().catch((err) => {
|
||||
console.error('[sse] propertyname refresh failed', err);
|
||||
});
|
||||
});
|
||||
|
||||
es.addEventListener('supplier', () => {
|
||||
addTrackedListener(es, 'supplier', () => {
|
||||
void supplierAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] supplier refresh failed', err);
|
||||
});
|
||||
});
|
||||
|
||||
es.addEventListener('open', () => {
|
||||
addTrackedListener(es, 'open', () => {
|
||||
sseAutomat.setState({ status: 'connected' });
|
||||
});
|
||||
|
||||
addTrackedListener(es, 'message', () => {});
|
||||
|
||||
// EventSource reconnects on its own; only surface the state.
|
||||
es.onerror = () => {
|
||||
console.error('[sse] event stream error');
|
||||
recordEvent({ type: 'error', data: '', lastEventId: '' });
|
||||
sseAutomat.setState({ status: 'error' });
|
||||
};
|
||||
|
||||
@@ -186,6 +221,10 @@ const actions = {
|
||||
connect();
|
||||
},
|
||||
|
||||
clearEvents() {
|
||||
sseAutomat.setState({ events: [] });
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a handler for a statically named backend event.
|
||||
*
|
||||
@@ -202,11 +241,15 @@ const actions = {
|
||||
set = new Set();
|
||||
handlers.set(event, set);
|
||||
}
|
||||
set.add(handler);
|
||||
eventSource?.addEventListener(event, handler);
|
||||
let entry = Array.from(set).find((candidate) => candidate.handler === handler);
|
||||
if (!entry) {
|
||||
entry = { handler, detach: null };
|
||||
set.add(entry);
|
||||
if (eventSource) entry.detach = addTrackedListener(eventSource, event, handler);
|
||||
}
|
||||
return () => {
|
||||
eventSource?.removeEventListener(event, handler);
|
||||
set.delete(handler);
|
||||
entry.detach?.();
|
||||
set.delete(entry);
|
||||
if (set.size === 0) handlers.delete(event);
|
||||
};
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user