{"record":{"id":"8532434c4e0c1e77","repo":"amir20/dozzle","slug":"no-loadmorelogentry-on-first-item","errorCode":null,"errorMessage":"No loadMoreLogEntry on first item","messagePattern":"No loadMoreLogEntry on first item","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"assets/composable/logLoader.ts","lineNumber":19,"sourceCode":"import { ShallowRef, type Ref } from \"vue\";\nimport { type LogMessage, LogEntry, LoadMoreLogEntry, SkippedLogsEntry } from \"@/models/LogEntry\";\nimport { Container } from \"@/models/Container\";\nimport { loadBetween } from \"@/composable/loadBetween\";\nimport { useAlertMerger } from \"@/composable/alertMerger\";\n\n// Matches the rolling window size used for stats history\nconst LOG_WINDOW_FOR_DELTA = 300;\n\nexport function useLogLoader(\n  messages: ShallowRef<LogEntry<LogMessage>[]>,\n  containers: Ref<Container[]>,\n  params: Ref<URLSearchParams>,\n  loadingMore: Ref<boolean>,\n) {\n  const { withAlerts, decorateVisible } = useAlertMerger(messages, containers, params);\n\n  async function loadOlderLogs(entry: LoadMoreLogEntry) {\n    if (!(messages.value[0] instanceof LoadMoreLogEntry)) throw new Error(\"No loadMoreLogEntry on first item\");\n    if (containers.value.length === 0) return;\n\n    const [loader, ...existingLogs] = messages.value;\n    if (existingLogs.length === 0) return;\n\n    const containerIDs = new Set(containers.value.map((c) => c.id));\n    const earliestByContainer = new Map<string, LogEntry<LogMessage>>();\n    const countByContainer = new Map<string, number>();\n    const nthByContainer = new Map<string, LogEntry<LogMessage>>();\n    for (const log of existingLogs) {\n      const id = log.containerID;\n      if (!id || !containerIDs.has(id)) continue;\n      if (!earliestByContainer.has(id)) {\n        earliestByContainer.set(id, log);\n      }\n      const count = (countByContainer.get(id) ?? 0) + 1;\n      countByContainer.set(id, count);\n      if (count <= LOG_WINDOW_FOR_DELTA) {","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/amir20/dozzle/blob/d9463cbe21874e44ab79db6fa63e746ca7d22928/assets/composable/logLoader.ts#L1-L37","documentation":"loadOlderLogs() implements the \"load more history\" affordance: the first element of the messages array must be the LoadMoreLogEntry placeholder that carries the loading state and cursor. If it is not — meaning the internal message-list invariant is broken — it throws \"No loadMoreLogEntry on first item\" rather than silently loading nothing. This is a programming/invariant error, not a runtime data error.","triggerScenarios":"Calling loadOlderLogs(entry) when messages.value[0] is a normal log entry, a SkippedLogsEntry, or the array is empty/starts with something else — typically because a stream reset, filter change, or container switch replaced the messages array while the LoadMoreLogEntry component was still mounted and triggered its callback.","commonSituations":"Clicking \"load older logs\" right after a live-stream reconnect cleared and rebuilt the messages array; applying a search/filter that rewrites messages synchronously while the load-more item is being clicked; calling loadOlderLogs from custom code without ensuring the sentinel entry is at index 0.","solutions":["Guard the call site: only invoke loadOlderLogs when messages[0] instanceof LoadMoreLogEntry.","Check for race conditions where the messages array is reset (reconnect, filter, container switch) between rendering the LoadMoreLogEntry and the click; re-validate or cancel the callback on reset.","Ensure code that rebuilds messages (event stream handlers) always re-inserts the LoadMoreLogEntry sentinel at index 0 when more history exists.","Wrap the call in try/catch and treat the throw as \"no more history available\" for the current view state."],"exampleFix":"// before\nawait loadOlderLogs(entry); // throws if messages[0] changed\n// after\nconst first = messages.value[0];\nif (first instanceof LoadMoreLogEntry) {\n  await loadOlderLogs(first);\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function canLoadOlder(msgs: LogEntry[]): msgs is [LoadMoreLogEntry, ...LogEntry[]] {\n  return msgs.length > 0 && msgs[0] instanceof LoadMoreLogEntry;\n}","tryCatchPattern":"try {\n  await loadOlderLogs(entry);\n} catch {\n  // sentinel gone: view was reset, nothing to load\n}","preventionTips":["Only bind the load-more handler to the actual LoadMoreLogEntry instance at index 0.","Rebuild or cancel load-more state whenever messages are reset by reconnects, filters, or container switches.","Keep the invariant that every messages rebuild re-inserts the LoadMoreLogEntry sentinel when history remains.","Treat the throw as a benign no-op in UI code rather than letting it bubble."],"tags":["invariant","state","race-condition","logs"],"backgroundTag":"internal-invariant-violation","analyzedSha":"d9463cbe21874e44ab79db6fa63e746ca7d22928","analyzedAt":"2026-09-07T10:08:55.855Z","contentChangedAt":"2026-09-07T10:08:55.855Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}