u
This commit is contained in:
@@ -77,6 +77,8 @@ export {
|
||||
isOrderDelivered,
|
||||
getNegativeSeekCutoff,
|
||||
formatNegativeSeek,
|
||||
getChunkStats,
|
||||
subscribeChunkStats,
|
||||
} from './orderAutomat';
|
||||
export { default as entityAutomat } from './entityAutomat';
|
||||
export { default as customergroupAutomat } from './customergroupAutomat';
|
||||
|
||||
@@ -74,13 +74,91 @@ let pendingChunkReject = null;
|
||||
const loadedChunks = new Set();
|
||||
const inflightChunks = new Map();
|
||||
|
||||
// Dev / Observability: Chunk event history and subscriber registry
|
||||
const MAX_CHUNK_EVENTS = 120;
|
||||
let nextEventId = 0;
|
||||
const chunkEvents = [];
|
||||
const chunkStatsListeners = new Set();
|
||||
|
||||
function recordChunkEvent(entry) {
|
||||
const event = {
|
||||
id: ++nextEventId,
|
||||
time: Date.now(),
|
||||
epoch: queryEpoch,
|
||||
...entry,
|
||||
};
|
||||
chunkEvents.push(event);
|
||||
if (chunkEvents.length > MAX_CHUNK_EVENTS) {
|
||||
chunkEvents.shift();
|
||||
}
|
||||
notifyChunkStats();
|
||||
}
|
||||
|
||||
function notifyChunkStats() {
|
||||
if (chunkStatsListeners.size === 0) return;
|
||||
const stats = getChunkStats();
|
||||
for (const listener of chunkStatsListeners) {
|
||||
try {
|
||||
listener(stats);
|
||||
} catch (e) {
|
||||
console.error('[orderAutomat] chunkStatsListener error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getChunkStats() {
|
||||
const total = automat?.state?.total ?? 0;
|
||||
const chunkCount = total > 0 ? Math.ceil(total / CHUNK_SIZE) : (loadedChunks.size > 0 ? Math.max(...loadedChunks) + 1 : 0);
|
||||
const items = automat?.state?.items;
|
||||
let loadedItemsCount = 0;
|
||||
if (Array.isArray(items)) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i] != null) loadedItemsCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
automatName: 'order',
|
||||
chunkSize: CHUNK_SIZE,
|
||||
total,
|
||||
chunkCount,
|
||||
loadedChunks: Array.from(loadedChunks).sort((a, b) => a - b),
|
||||
inflightChunks: Array.from(inflightChunks.keys()).sort((a, b) => a - b),
|
||||
pendingChunks: Array.from(pendingTargetChunks).sort((a, b) => a - b),
|
||||
loadedItemsCount,
|
||||
status: automat?.state?.status ?? 'idle',
|
||||
queryEpoch,
|
||||
filter: automat?.state?.filter ?? '',
|
||||
onlyUnpaid: automat?.state?.onlyUnpaid ?? false,
|
||||
onlyUndelivered: automat?.state?.onlyUndelivered ?? false,
|
||||
negativeSeekMonths: automat?.state?.negativeSeekMonths ?? 0,
|
||||
items,
|
||||
events: [...chunkEvents],
|
||||
};
|
||||
}
|
||||
|
||||
export function subscribeChunkStats(listener) {
|
||||
chunkStatsListeners.add(listener);
|
||||
try {
|
||||
listener(getChunkStats());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return () => {
|
||||
chunkStatsListeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// Clears pending debounced chunk requests
|
||||
function clearChunkDebounce() {
|
||||
if (chunkDebounceTimer) {
|
||||
clearTimeout(chunkDebounceTimer);
|
||||
chunkDebounceTimer = null;
|
||||
}
|
||||
pendingTargetChunks.clear();
|
||||
if (pendingTargetChunks.size > 0) {
|
||||
pendingTargetChunks.clear();
|
||||
notifyChunkStats();
|
||||
}
|
||||
pendingChunkPromise = null;
|
||||
pendingChunkResolve = null;
|
||||
pendingChunkReject = null;
|
||||
@@ -97,6 +175,12 @@ function _executeRequiredChunks(chunks, targetEpoch, prefetch = true) {
|
||||
return Promise.resolve(automat.state.items);
|
||||
}
|
||||
|
||||
recordChunkEvent({
|
||||
type: 'execute',
|
||||
chunks: chunkList,
|
||||
message: `Lade Chunks [${chunkList.join(', ')}] (Epoch ${targetEpoch})`,
|
||||
});
|
||||
|
||||
const promises = [];
|
||||
for (const c of chunkList) {
|
||||
promises.push(actions.loadChunk(c, targetEpoch));
|
||||
@@ -112,6 +196,11 @@ function _executeRequiredChunks(chunks, targetEpoch, prefetch = true) {
|
||||
if (total > 0 && c * CHUNK_SIZE >= total) {
|
||||
break;
|
||||
}
|
||||
recordChunkEvent({
|
||||
type: 'prefetch',
|
||||
chunkIndex: c,
|
||||
message: `Prefetch Chunk #${c}`,
|
||||
});
|
||||
void actions.loadChunk(c, targetEpoch);
|
||||
}
|
||||
}
|
||||
@@ -214,6 +303,12 @@ function applyFilterReload() {
|
||||
clearChunkDebounce();
|
||||
loadedChunks.clear();
|
||||
inflightChunks.clear();
|
||||
recordChunkEvent({
|
||||
type: 'filter_reload',
|
||||
epoch: queryEpoch,
|
||||
message: `Filter angewendet (Epoch ${queryEpoch})`,
|
||||
});
|
||||
notifyChunkStats();
|
||||
void actions.loadChunk(0, queryEpoch);
|
||||
}
|
||||
|
||||
@@ -261,6 +356,11 @@ const actions = {
|
||||
clearChunkDebounce();
|
||||
const chunks = new Set();
|
||||
for (let c = start; c <= end; c++) chunks.add(c);
|
||||
recordChunkEvent({
|
||||
type: 'require_immediate',
|
||||
chunks: Array.from(chunks),
|
||||
message: `Sofortige Anforderung: Chunks ${start}..${end}`,
|
||||
});
|
||||
return _executeRequiredChunks(chunks, queryEpoch, prefetch);
|
||||
}
|
||||
|
||||
@@ -282,6 +382,12 @@ const actions = {
|
||||
for (let c = start; c <= end; c++) {
|
||||
pendingTargetChunks.add(c);
|
||||
}
|
||||
recordChunkEvent({
|
||||
type: 'pending',
|
||||
chunks: Array.from(pendingTargetChunks),
|
||||
message: `Debounce vorgemerkt (${CHUNK_DEBOUNCE_MS}ms): Chunks [${Array.from(pendingTargetChunks).join(', ')}]`,
|
||||
});
|
||||
notifyChunkStats();
|
||||
|
||||
if (chunkDebounceTimer) {
|
||||
clearTimeout(chunkDebounceTimer);
|
||||
@@ -300,6 +406,7 @@ const actions = {
|
||||
chunkDebounceTimer = null;
|
||||
const chunksToLoad = new Set(pendingTargetChunks);
|
||||
pendingTargetChunks.clear();
|
||||
notifyChunkStats();
|
||||
|
||||
const resolve = pendingChunkResolve;
|
||||
const reject = pendingChunkReject;
|
||||
@@ -307,6 +414,12 @@ const actions = {
|
||||
pendingChunkResolve = null;
|
||||
pendingChunkReject = null;
|
||||
|
||||
recordChunkEvent({
|
||||
type: 'debounce_fire',
|
||||
chunks: Array.from(chunksToLoad),
|
||||
message: `Debounce abgelaufen: Starte Chunks [${Array.from(chunksToLoad).join(', ')}]`,
|
||||
});
|
||||
|
||||
_executeRequiredChunks(chunksToLoad, captureEpoch, true)
|
||||
.then((res) => resolve?.(res))
|
||||
.catch((err) => reject?.(err));
|
||||
@@ -322,9 +435,19 @@ const actions = {
|
||||
return Promise.resolve(automat.state.items);
|
||||
}
|
||||
if (loadedChunks.has(index)) {
|
||||
recordChunkEvent({
|
||||
type: 'hit',
|
||||
chunkIndex: index,
|
||||
message: `Cache-Hit: Chunk #${index} bereits im Speicher`,
|
||||
});
|
||||
return Promise.resolve(automat.state.items);
|
||||
}
|
||||
if (inflightChunks.has(index)) {
|
||||
recordChunkEvent({
|
||||
type: 'inflight_hit',
|
||||
chunkIndex: index,
|
||||
message: `Inflight-Hit: Chunk #${index} lädt bereits`,
|
||||
});
|
||||
return inflightChunks.get(index);
|
||||
}
|
||||
|
||||
@@ -334,6 +457,13 @@ const actions = {
|
||||
automat.setState({ status: 'loading', error: null });
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
recordChunkEvent({
|
||||
type: 'start',
|
||||
chunkIndex: index,
|
||||
message: `Netzwerk-Request: Lade Chunk #${index} (Offset ${offset}..${offset + limit - 1})`,
|
||||
});
|
||||
|
||||
const promise = fetchOrdersPage(offset, limit)
|
||||
.then(({ orders, total }) => {
|
||||
// Discard if active filter query changed while request was in flight
|
||||
@@ -359,6 +489,7 @@ const actions = {
|
||||
}
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
loadedChunks.add(index);
|
||||
|
||||
automat.setState({
|
||||
@@ -368,11 +499,27 @@ const actions = {
|
||||
error: null,
|
||||
});
|
||||
|
||||
recordChunkEvent({
|
||||
type: 'loaded',
|
||||
chunkIndex: index,
|
||||
count: orders.length,
|
||||
duration,
|
||||
message: `Chunk #${index} geladen (${orders.length} Aufträge in ${duration}ms)`,
|
||||
});
|
||||
|
||||
return items;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (targetEpoch === queryEpoch) {
|
||||
const duration = Date.now() - startTime;
|
||||
console.error(`[orderAutomat] loadChunk(${index}) error:`, err);
|
||||
recordChunkEvent({
|
||||
type: 'error',
|
||||
chunkIndex: index,
|
||||
duration,
|
||||
error: err?.message || String(err),
|
||||
message: `Fehler bei Chunk #${index}: ${err?.message || err}`,
|
||||
});
|
||||
if (index === 0 && automat.state.items === null) {
|
||||
automat.setState({ status: 'failed', error: err });
|
||||
}
|
||||
@@ -381,9 +528,11 @@ const actions = {
|
||||
})
|
||||
.finally(() => {
|
||||
inflightChunks.delete(index);
|
||||
notifyChunkStats();
|
||||
});
|
||||
|
||||
inflightChunks.set(index, promise);
|
||||
notifyChunkStats();
|
||||
return promise;
|
||||
},
|
||||
|
||||
@@ -478,6 +627,11 @@ const actions = {
|
||||
loadedChunks.clear();
|
||||
inflightChunks.clear();
|
||||
const count = Array.isArray(items) ? items.length : 0;
|
||||
recordChunkEvent({
|
||||
type: 'replace',
|
||||
message: `Items ersetzt (${count} Einträge)`,
|
||||
});
|
||||
notifyChunkStats();
|
||||
automat.setState({
|
||||
items,
|
||||
total: count,
|
||||
@@ -487,6 +641,63 @@ const actions = {
|
||||
return items;
|
||||
},
|
||||
|
||||
// MARK: evicts a specific chunk from cache for testing
|
||||
evictChunk(chunkIndex) {
|
||||
const index = Math.max(0, Number(chunkIndex) || 0);
|
||||
if (!loadedChunks.has(index)) return automat.state.items;
|
||||
loadedChunks.delete(index);
|
||||
let items = automat.state.items;
|
||||
if (Array.isArray(items)) {
|
||||
items = [...items];
|
||||
const offset = index * CHUNK_SIZE;
|
||||
for (let i = offset; i < offset + CHUNK_SIZE && i < items.length; i++) {
|
||||
items[i] = null;
|
||||
}
|
||||
automat.setState({ items });
|
||||
}
|
||||
recordChunkEvent({
|
||||
type: 'evict',
|
||||
chunkIndex: index,
|
||||
message: `Chunk #${index} aus Cache entfernt (evicted)`,
|
||||
});
|
||||
notifyChunkStats();
|
||||
return automat.state.items;
|
||||
},
|
||||
|
||||
// MARK: clears all loaded chunks from cache
|
||||
clearCache() {
|
||||
clearChunkDebounce();
|
||||
loadedChunks.clear();
|
||||
inflightChunks.clear();
|
||||
let items = automat.state.items;
|
||||
if (Array.isArray(items)) {
|
||||
items = new Array(automat.state.total || 0).fill(null);
|
||||
automat.setState({ items });
|
||||
}
|
||||
recordChunkEvent({
|
||||
type: 'clear',
|
||||
message: 'Alle Chunks aus dem Cache entfernt',
|
||||
});
|
||||
notifyChunkStats();
|
||||
return automat.state.items;
|
||||
},
|
||||
|
||||
// MARK: prefetches all remaining chunks
|
||||
prefetchAllChunks() {
|
||||
const total = automat.state.total || 0;
|
||||
const chunkCount = Math.ceil(total / CHUNK_SIZE);
|
||||
recordChunkEvent({
|
||||
type: 'prefetch_all',
|
||||
message: `Starte Prefetch aller ${chunkCount} Chunks`,
|
||||
});
|
||||
for (let c = 0; c < chunkCount; c++) {
|
||||
if (!loadedChunks.has(c) && !inflightChunks.has(c)) {
|
||||
void actions.loadChunk(c, queryEpoch);
|
||||
}
|
||||
}
|
||||
notifyChunkStats();
|
||||
},
|
||||
|
||||
// MARK: patches an order locally in the automat items cache
|
||||
patchOrder(id, patch) {
|
||||
if (!Array.isArray(automat.state.items)) return automat.state.items;
|
||||
@@ -514,6 +725,11 @@ const actions = {
|
||||
clearChunkDebounce();
|
||||
loadedChunks.clear();
|
||||
inflightChunks.clear();
|
||||
recordChunkEvent({
|
||||
type: 'reset',
|
||||
message: 'Order-Automat zurückgesetzt',
|
||||
});
|
||||
notifyChunkStats();
|
||||
automat.setState(initialState);
|
||||
},
|
||||
};
|
||||
@@ -533,6 +749,11 @@ export function requireChunk(chunkIndex, endOrOptions, maybeOptions) {
|
||||
orderAutomat.getItems = getItems;
|
||||
orderAutomat.requireChunk = requireChunk;
|
||||
orderAutomat.CHUNK_SIZE = CHUNK_SIZE;
|
||||
orderAutomat.getChunkStats = getChunkStats;
|
||||
orderAutomat.subscribeChunkStats = subscribeChunkStats;
|
||||
orderAutomat.evictChunk = actions.evictChunk;
|
||||
orderAutomat.clearCache = actions.clearCache;
|
||||
orderAutomat.prefetchAllChunks = actions.prefetchAllChunks;
|
||||
|
||||
// Reset on logout or company change
|
||||
let lastCompanyId = null;
|
||||
|
||||
Reference in New Issue
Block a user