-
Notifications
You must be signed in to change notification settings - Fork 508
feat: implement lazy loading for session content #3329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,9 +23,12 @@ export { createEmptyLoadedSessionData, type LoadedSessionData } from "./types"; | |
|
|
||
| const LABEL = "SessionPersister"; | ||
|
|
||
| export type LoadMode = "metadata" | "full"; | ||
|
|
||
| async function processFiles( | ||
| files: Partial<Record<string, string>>, | ||
| result: LoadedSessionData, | ||
| mode: LoadMode = "full", | ||
| ): Promise<void> { | ||
| for (const [path, content] of Object.entries(files)) { | ||
| if (!content) continue; | ||
|
|
@@ -34,6 +37,10 @@ async function processFiles( | |
| } | ||
| } | ||
|
|
||
| if (mode === "metadata") { | ||
| return; | ||
| } | ||
|
|
||
| for (const [path, content] of Object.entries(files)) { | ||
| if (!content) continue; | ||
| if (path.endsWith(SESSION_TRANSCRIPT_FILE)) { | ||
|
|
@@ -53,13 +60,23 @@ async function processFiles( | |
|
|
||
| export async function loadAllSessionData( | ||
| dataDir: string, | ||
| mode: LoadMode = "full", | ||
| ): Promise<LoadResult<LoadedSessionData>> { | ||
| const result = createEmptyLoadedSessionData(); | ||
| const sessionsDir = [dataDir, "sessions"].join(sep()); | ||
|
|
||
| const patterns = | ||
| mode === "metadata" | ||
| ? [SESSION_META_FILE] | ||
| : [ | ||
| SESSION_META_FILE, | ||
| SESSION_TRANSCRIPT_FILE, | ||
| `*${SESSION_NOTE_EXTENSION}`, | ||
| ]; | ||
|
|
||
| const scanResult = await fsSyncCommands.scanAndRead( | ||
| sessionsDir, | ||
| [SESSION_META_FILE, SESSION_TRANSCRIPT_FILE, `*${SESSION_NOTE_EXTENSION}`], | ||
| patterns, | ||
| true, | ||
| null, | ||
| ); | ||
|
|
@@ -72,20 +89,30 @@ export async function loadAllSessionData( | |
| return err(scanResult.error); | ||
| } | ||
|
|
||
| await processFiles(scanResult.data.files, result); | ||
| await processFiles(scanResult.data.files, result, mode); | ||
| return ok(result); | ||
| } | ||
|
|
||
| export async function loadSingleSession( | ||
| dataDir: string, | ||
| sessionId: string, | ||
| mode: LoadMode = "full", | ||
| ): Promise<LoadResult<LoadedSessionData>> { | ||
| const result = createEmptyLoadedSessionData(); | ||
| const sessionsDir = [dataDir, "sessions"].join(sep()); | ||
|
|
||
| const patterns = | ||
| mode === "metadata" | ||
| ? [SESSION_META_FILE] | ||
| : [ | ||
| SESSION_META_FILE, | ||
| SESSION_TRANSCRIPT_FILE, | ||
| `*${SESSION_NOTE_EXTENSION}`, | ||
| ]; | ||
|
|
||
| const scanResult = await fsSyncCommands.scanAndRead( | ||
| sessionsDir, | ||
| [SESSION_META_FILE, SESSION_TRANSCRIPT_FILE, `*${SESSION_NOTE_EXTENSION}`], | ||
| patterns, | ||
| true, | ||
| `/${sessionId}/`, | ||
| ); | ||
|
|
@@ -98,6 +125,50 @@ export async function loadSingleSession( | |
| return err(scanResult.error); | ||
| } | ||
|
|
||
| await processFiles(scanResult.data.files, result); | ||
| await processFiles(scanResult.data.files, result, mode); | ||
| return ok(result); | ||
| } | ||
|
|
||
| export async function loadSessionContent( | ||
| dataDir: string, | ||
| sessionId: string, | ||
| ): Promise<LoadResult<LoadedSessionData>> { | ||
| const result = createEmptyLoadedSessionData(); | ||
| const sessionsDir = [dataDir, "sessions"].join(sep()); | ||
|
|
||
| const scanResult = await fsSyncCommands.scanAndRead( | ||
| sessionsDir, | ||
| [SESSION_TRANSCRIPT_FILE, `*${SESSION_NOTE_EXTENSION}`], | ||
| true, | ||
| `/${sessionId}/`, | ||
| ); | ||
|
|
||
| if (scanResult.status === "error") { | ||
| if (isDirectoryNotFoundError(scanResult.error)) { | ||
| return ok(result); | ||
| } | ||
| console.error( | ||
| `[${LABEL}] loadSessionContent scan error:`, | ||
| scanResult.error, | ||
| ); | ||
| return err(scanResult.error); | ||
| } | ||
|
|
||
| for (const [path, content] of Object.entries(scanResult.data.files)) { | ||
| if (!content) continue; | ||
| if (path.endsWith(SESSION_TRANSCRIPT_FILE)) { | ||
| processTranscriptFile(path, content, result); | ||
| } | ||
| } | ||
|
|
||
| const mdPromises: Promise<void>[] = []; | ||
| for (const [path, content] of Object.entries(scanResult.data.files)) { | ||
| if (!content) continue; | ||
| if (path.endsWith(SESSION_NOTE_EXTENSION)) { | ||
| mdPromises.push(processMdFile(path, content, result)); | ||
| } | ||
| } | ||
| await Promise.all(mdPromises); | ||
|
|
||
| return ok(result); | ||
| } | ||
|
Comment on lines
+132
to
174
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Session raw_md content is never loaded during lazy loading The Click to expandProblemIn
// In note.ts:36-39
if (path.endsWith(SESSION_MEMO_FILE)) {
if (result.sessions[fm.session_id]) { // Always false since sessions is empty!
result.sessions[fm.session_id].raw_md = tiptapContent;
}
}Then in const session = result.data.sessions[sessionId]; // undefined
if (session?.raw_md) {
store.setCell("sessions", sessionId, "raw_md", session.raw_md);
}ImpactUser notes stored in FixIn Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,8 @@ | ||||||||||||||||||||||||||||||||
| import { commands as fsSyncCommands } from "@hypr/plugin-fs-sync"; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import type { Store } from "../../store/main"; | ||||||||||||||||||||||||||||||||
| import { getDataDir } from "../shared"; | ||||||||||||||||||||||||||||||||
| import { loadSessionContent } from "./load/index"; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export interface SessionOpsConfig { | ||||||||||||||||||||||||||||||||
| store: Store; | ||||||||||||||||||||||||||||||||
|
|
@@ -9,17 +11,110 @@ export interface SessionOpsConfig { | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let config: SessionOpsConfig | null = null; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const contentLoadState = { | ||||||||||||||||||||||||||||||||
| loaded: new Set<string>(), | ||||||||||||||||||||||||||||||||
| loading: new Set<string>(), | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export function initSessionOps(cfg: SessionOpsConfig) { | ||||||||||||||||||||||||||||||||
| config = cfg; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export function clearContentLoadState() { | ||||||||||||||||||||||||||||||||
| contentLoadState.loaded.clear(); | ||||||||||||||||||||||||||||||||
| contentLoadState.loading.clear(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export function isSessionContentLoaded(sessionId: string): boolean { | ||||||||||||||||||||||||||||||||
| return contentLoadState.loaded.has(sessionId); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export function isSessionContentLoading(sessionId: string): boolean { | ||||||||||||||||||||||||||||||||
| return contentLoadState.loading.has(sessionId); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export function markSessionContentLoaded(sessionId: string) { | ||||||||||||||||||||||||||||||||
| contentLoadState.loaded.add(sessionId); | ||||||||||||||||||||||||||||||||
| contentLoadState.loading.delete(sessionId); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| function getConfig(): SessionOpsConfig { | ||||||||||||||||||||||||||||||||
| if (!config) { | ||||||||||||||||||||||||||||||||
| throw new Error("[SessionOps] Not initialized. Call initSessionOps first."); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return config; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export async function ensureSessionContentLoaded( | ||||||||||||||||||||||||||||||||
| sessionId: string, | ||||||||||||||||||||||||||||||||
| ): Promise<boolean> { | ||||||||||||||||||||||||||||||||
| if (contentLoadState.loaded.has(sessionId)) { | ||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (contentLoadState.loading.has(sessionId)) { | ||||||||||||||||||||||||||||||||
| return new Promise((resolve) => { | ||||||||||||||||||||||||||||||||
| const checkInterval = setInterval(() => { | ||||||||||||||||||||||||||||||||
| if (contentLoadState.loaded.has(sessionId)) { | ||||||||||||||||||||||||||||||||
| clearInterval(checkInterval); | ||||||||||||||||||||||||||||||||
| resolve(true); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if (!contentLoadState.loading.has(sessionId)) { | ||||||||||||||||||||||||||||||||
| clearInterval(checkInterval); | ||||||||||||||||||||||||||||||||
| resolve(false); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| }, 50); | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| contentLoadState.loading.add(sessionId); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| const { store } = getConfig(); | ||||||||||||||||||||||||||||||||
| const dataDir = await getDataDir(); | ||||||||||||||||||||||||||||||||
| const result = await loadSessionContent(dataDir, sessionId); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (result.status === "error") { | ||||||||||||||||||||||||||||||||
| console.error( | ||||||||||||||||||||||||||||||||
| `[SessionOps] Failed to load content for session ${sessionId}:`, | ||||||||||||||||||||||||||||||||
| result.error, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| contentLoadState.loading.delete(sessionId); | ||||||||||||||||||||||||||||||||
| contentLoadState.loaded.add(sessionId); | ||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||
|
Comment on lines
+77
to
+84
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Error handling breaks retry logic: When content loading fails, the session is still marked as loaded (line 83). This prevents any future retry attempts, permanently leaving the session without its content. if (result.status === "error") {
console.error(
`[SessionOps] Failed to load content for session ${sessionId}:`,
result.error,
);
contentLoadState.loading.delete(sessionId);
// Don't mark as loaded on error - allow retry
return false;
}Remove line 83 (
Suggested change
Spotted by Graphite Agent |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| store.transaction(() => { | ||||||||||||||||||||||||||||||||
| for (const [transcriptId, transcript] of Object.entries( | ||||||||||||||||||||||||||||||||
| result.data.transcripts, | ||||||||||||||||||||||||||||||||
| )) { | ||||||||||||||||||||||||||||||||
| store.setRow("transcripts", transcriptId, transcript); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| for (const [noteId, note] of Object.entries(result.data.enhanced_notes)) { | ||||||||||||||||||||||||||||||||
| store.setRow("enhanced_notes", noteId, note); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const session = result.data.sessions[sessionId]; | ||||||||||||||||||||||||||||||||
| if (session?.raw_md) { | ||||||||||||||||||||||||||||||||
| store.setCell("sessions", sessionId, "raw_md", session.raw_md); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| contentLoadState.loading.delete(sessionId); | ||||||||||||||||||||||||||||||||
| contentLoadState.loaded.add(sessionId); | ||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||
| console.error( | ||||||||||||||||||||||||||||||||
| `[SessionOps] Error loading content for session ${sessionId}:`, | ||||||||||||||||||||||||||||||||
| error, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| contentLoadState.loading.delete(sessionId); | ||||||||||||||||||||||||||||||||
| contentLoadState.loaded.add(sessionId); | ||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export async function moveSessionToFolder( | ||||||||||||||||||||||||||||||||
| sessionId: string, | ||||||||||||||||||||||||||||||||
| targetFolderId: string, | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 React hooks called after conditional early return
The
TabContentNotecomponent has an early return at lines 96-102 that occurs before hooks are called, violating React's rules of hooks.Click to expand
Problem
In
TabContentNote, hooks (useEffectat line 104 anduseQueryat line 135) are called after a conditional early return:React requires hooks to be called in the same order on every render. When
contentLoadingistrue, the hooks after the return statement are not called, but whencontentLoadingbecomesfalse, they suddenly are. This can cause:Fix
Move all hooks before the early return, or move the loading check into the JSX return.
Was this helpful? React with 👍 or 👎 to provide feedback.