Browse Source

fix(app): session load cap

adamelmore 1 month ago
parent
commit
319ad2a391
2 changed files with 129 additions and 40 deletions
  1. 86 35
      packages/app/src/context/global-sync.tsx
  2. 43 5
      packages/app/src/pages/layout.tsx

+ 86 - 35
packages/app/src/context/global-sync.tsx

@@ -225,6 +225,65 @@ function createGlobalSync() {
   const sessionLoads = new Map<string, Promise<void>>()
   const sessionLoads = new Map<string, Promise<void>>()
   const sessionMeta = new Map<string, { limit: number }>()
   const sessionMeta = new Map<string, { limit: number }>()
 
 
+  const sessionRecentWindow = 4 * 60 * 60 * 1000
+  const sessionRecentLimit = 50
+
+  function sessionUpdatedAt(session: Session) {
+    return session.time.updated ?? session.time.created
+  }
+
+  function compareSessionRecent(a: Session, b: Session) {
+    const aUpdated = sessionUpdatedAt(a)
+    const bUpdated = sessionUpdatedAt(b)
+    if (aUpdated !== bUpdated) return bUpdated - aUpdated
+    return a.id.localeCompare(b.id)
+  }
+
+  function takeRecentSessions(sessions: Session[], limit: number, cutoff: number) {
+    if (limit <= 0) return [] as Session[]
+    const selected: Session[] = []
+    const seen = new Set<string>()
+    for (const session of sessions) {
+      if (!session?.id) continue
+      if (seen.has(session.id)) continue
+      seen.add(session.id)
+
+      if (sessionUpdatedAt(session) <= cutoff) continue
+
+      const index = selected.findIndex((x) => compareSessionRecent(session, x) < 0)
+      if (index === -1) selected.push(session)
+      if (index !== -1) selected.splice(index, 0, session)
+      if (selected.length > limit) selected.pop()
+    }
+    return selected
+  }
+
+  function trimSessions(input: Session[], options: { limit: number; permission: Record<string, PermissionRequest[]> }) {
+    const limit = Math.max(0, options.limit)
+    const cutoff = Date.now() - sessionRecentWindow
+    const all = input
+      .filter((s) => !!s?.id)
+      .filter((s) => !s.time?.archived)
+      .sort((a, b) => a.id.localeCompare(b.id))
+
+    const roots = all.filter((s) => !s.parentID)
+    const children = all.filter((s) => !!s.parentID)
+
+    const base = roots.slice(0, limit)
+    const recent = takeRecentSessions(roots.slice(limit), sessionRecentLimit, cutoff)
+    const keepRoots = [...base, ...recent]
+
+    const keepRootIds = new Set(keepRoots.map((s) => s.id))
+    const keepChildren = children.filter((s) => {
+      if (s.parentID && keepRootIds.has(s.parentID)) return true
+      const perms = options.permission[s.id] ?? []
+      if (perms.length > 0) return true
+      return sessionUpdatedAt(s) > cutoff
+    })
+
+    return [...keepRoots, ...keepChildren].sort((a, b) => a.id.localeCompare(b.id))
+  }
+
   function ensureChild(directory: string) {
   function ensureChild(directory: string) {
     if (!directory) console.error("No directory provided")
     if (!directory) console.error("No directory provided")
     if (!children[directory]) {
     if (!children[directory]) {
@@ -323,7 +382,13 @@ function createGlobalSync() {
 
 
     const [store, setStore] = child(directory, { bootstrap: false })
     const [store, setStore] = child(directory, { bootstrap: false })
     const meta = sessionMeta.get(directory)
     const meta = sessionMeta.get(directory)
-    if (meta && meta.limit >= store.limit) return
+    if (meta && meta.limit >= store.limit) {
+      const next = trimSessions(store.session, { limit: store.limit, permission: store.permission })
+      if (next.length !== store.session.length) {
+        setStore("session", reconcile(next, { key: "id" }))
+      }
+      return
+    }
 
 
     const promise = globalSDK.client.session
     const promise = globalSDK.client.session
       .list({ directory, roots: true })
       .list({ directory, roots: true })
@@ -337,21 +402,9 @@ function createGlobalSync() {
         // a request is in-flight still get the expanded result.
         // a request is in-flight still get the expanded result.
         const limit = store.limit
         const limit = store.limit
 
 
-        const sandboxWorkspace = globalStore.project.some((p) => (p.sandboxes ?? []).includes(directory))
-        if (sandboxWorkspace) {
-          setStore("sessionTotal", nonArchived.length)
-          setStore("session", reconcile(nonArchived, { key: "id" }))
-          sessionMeta.set(directory, { limit })
-          return
-        }
+        const children = store.session.filter((s) => !!s.parentID)
+        const sessions = trimSessions([...nonArchived, ...children], { limit, permission: store.permission })
 
 
-        const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
-        // Include up to the limit, plus any updated in the last 4 hours
-        const sessions = nonArchived.filter((s, i) => {
-          if (i < limit) return true
-          const updated = new Date(s.time?.updated ?? s.time?.created).getTime()
-          return updated > fourHoursAgo
-        })
         // Store total session count (used for "load more" pagination)
         // Store total session count (used for "load more" pagination)
         setStore("sessionTotal", nonArchived.length)
         setStore("sessionTotal", nonArchived.length)
         setStore("session", reconcile(sessions, { key: "id" }))
         setStore("session", reconcile(sessions, { key: "id" }))
@@ -536,25 +589,25 @@ function createGlobalSync() {
         break
         break
       }
       }
       case "session.created": {
       case "session.created": {
-        const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
+        const info = event.properties.info
+        const result = Binary.search(store.session, info.id, (s) => s.id)
         if (result.found) {
         if (result.found) {
-          setStore("session", result.index, reconcile(event.properties.info))
+          setStore("session", result.index, reconcile(info))
           break
           break
         }
         }
-        setStore(
-          "session",
-          produce((draft) => {
-            draft.splice(result.index, 0, event.properties.info)
-          }),
-        )
-        if (!event.properties.info.parentID) {
-          setStore("sessionTotal", store.sessionTotal + 1)
+        const next = store.session.slice()
+        next.splice(result.index, 0, info)
+        const trimmed = trimSessions(next, { limit: store.limit, permission: store.permission })
+        setStore("session", reconcile(trimmed, { key: "id" }))
+        if (!info.parentID) {
+          setStore("sessionTotal", (value) => value + 1)
         }
         }
         break
         break
       }
       }
       case "session.updated": {
       case "session.updated": {
-        const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
-        if (event.properties.info.time.archived) {
+        const info = event.properties.info
+        const result = Binary.search(store.session, info.id, (s) => s.id)
+        if (info.time.archived) {
           if (result.found) {
           if (result.found) {
             setStore(
             setStore(
               "session",
               "session",
@@ -563,20 +616,18 @@ function createGlobalSync() {
               }),
               }),
             )
             )
           }
           }
-          if (event.properties.info.parentID) break
+          if (info.parentID) break
           setStore("sessionTotal", (value) => Math.max(0, value - 1))
           setStore("sessionTotal", (value) => Math.max(0, value - 1))
           break
           break
         }
         }
         if (result.found) {
         if (result.found) {
-          setStore("session", result.index, reconcile(event.properties.info))
+          setStore("session", result.index, reconcile(info))
           break
           break
         }
         }
-        setStore(
-          "session",
-          produce((draft) => {
-            draft.splice(result.index, 0, event.properties.info)
-          }),
-        )
+        const next = store.session.slice()
+        next.splice(result.index, 0, info)
+        const trimmed = trimSessions(next, { limit: store.limit, permission: store.permission })
+        setStore("session", reconcile(trimmed, { key: "id" }))
         break
         break
       }
       }
       case "session.deleted": {
       case "session.deleted": {

+ 43 - 5
packages/app/src/pages/layout.tsx

@@ -1592,6 +1592,7 @@ export default function Layout(props: ParentProps) {
     mobile?: boolean
     mobile?: boolean
     dense?: boolean
     dense?: boolean
     popover?: boolean
     popover?: boolean
+    children?: Map<string, string[]>
   }): JSX.Element => {
   }): JSX.Element => {
     const notification = useNotification()
     const notification = useNotification()
     const notifications = createMemo(() => notification.session.unseen(props.session.id))
     const notifications = createMemo(() => notification.session.unseen(props.session.id))
@@ -1600,6 +1601,16 @@ export default function Layout(props: ParentProps) {
     const hasPermissions = createMemo(() => {
     const hasPermissions = createMemo(() => {
       const permissions = sessionStore.permission?.[props.session.id] ?? []
       const permissions = sessionStore.permission?.[props.session.id] ?? []
       if (permissions.length > 0) return true
       if (permissions.length > 0) return true
+
+      const childIDs = props.children?.get(props.session.id)
+      if (childIDs) {
+        for (const id of childIDs) {
+          const childPermissions = sessionStore.permission?.[id] ?? []
+          if (childPermissions.length > 0) return true
+        }
+        return false
+      }
+
       const childSessions = sessionStore.session.filter((s) => s.parentID === props.session.id)
       const childSessions = sessionStore.session.filter((s) => s.parentID === props.session.id)
       for (const child of childSessions) {
       for (const child of childSessions) {
         const childPermissions = sessionStore.permission?.[child.id] ?? []
         const childPermissions = sessionStore.permission?.[child.id] ?? []
@@ -1898,6 +1909,19 @@ export default function Layout(props: ParentProps) {
         .filter((session) => !session.parentID && !session.time?.archived)
         .filter((session) => !session.parentID && !session.time?.archived)
         .toSorted(sortSessions(Date.now())),
         .toSorted(sortSessions(Date.now())),
     )
     )
+    const children = createMemo(() => {
+      const map = new Map<string, string[]>()
+      for (const session of workspaceStore.session) {
+        if (!session.parentID) continue
+        const existing = map.get(session.parentID)
+        if (existing) {
+          existing.push(session.id)
+          continue
+        }
+        map.set(session.parentID, [session.id])
+      }
+      return map
+    })
     const local = createMemo(() => props.directory === props.project.worktree)
     const local = createMemo(() => props.directory === props.project.worktree)
     const active = createMemo(() => {
     const active = createMemo(() => {
       const current = params.dir ? base64Decode(params.dir) : ""
       const current = params.dir ? base64Decode(params.dir) : ""
@@ -1911,10 +1935,9 @@ export default function Layout(props: ParentProps) {
     const open = createMemo(() => store.workspaceExpanded[props.directory] ?? local())
     const open = createMemo(() => store.workspaceExpanded[props.directory] ?? local())
     const boot = createMemo(() => open() || active())
     const boot = createMemo(() => open() || active())
     const loading = createMemo(() => open() && workspaceStore.status !== "complete" && sessions().length === 0)
     const loading = createMemo(() => open() && workspaceStore.status !== "complete" && sessions().length === 0)
-    const hasMore = createMemo(() => local() && workspaceStore.sessionTotal > workspaceStore.session.length)
+    const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length)
     const busy = createMemo(() => isBusy(props.directory))
     const busy = createMemo(() => isBusy(props.directory))
     const loadMore = async () => {
     const loadMore = async () => {
-      if (!local()) return
       setWorkspaceStore("limit", (limit) => limit + 5)
       setWorkspaceStore("limit", (limit) => limit + 5)
       await globalSync.project.loadSessions(props.directory)
       await globalSync.project.loadSessions(props.directory)
     }
     }
@@ -2073,7 +2096,9 @@ export default function Layout(props: ParentProps) {
                 <SessionSkeleton />
                 <SessionSkeleton />
               </Show>
               </Show>
               <For each={sessions()}>
               <For each={sessions()}>
-                {(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />}
+                {(session) => (
+                  <SessionItem session={session} slug={slug()} mobile={props.mobile} children={children()} />
+                )}
               </For>
               </For>
               <Show when={hasMore()}>
               <Show when={hasMore()}>
                 <div class="relative w-full py-1">
                 <div class="relative w-full py-1">
@@ -2288,8 +2313,21 @@ export default function Layout(props: ParentProps) {
         .filter((session) => !session.parentID && !session.time?.archived)
         .filter((session) => !session.parentID && !session.time?.archived)
         .toSorted(sortSessions(Date.now())),
         .toSorted(sortSessions(Date.now())),
     )
     )
+    const children = createMemo(() => {
+      const map = new Map<string, string[]>()
+      for (const session of workspaceStore.session) {
+        if (!session.parentID) continue
+        const existing = map.get(session.parentID)
+        if (existing) {
+          existing.push(session.id)
+          continue
+        }
+        map.set(session.parentID, [session.id])
+      }
+      return map
+    })
     const loading = createMemo(() => workspaceStore.status !== "complete" && sessions().length === 0)
     const loading = createMemo(() => workspaceStore.status !== "complete" && sessions().length === 0)
-    const hasMore = createMemo(() => workspaceStore.sessionTotal > workspaceStore.session.length)
+    const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length)
     const loadMore = async () => {
     const loadMore = async () => {
       setWorkspaceStore("limit", (limit) => limit + 5)
       setWorkspaceStore("limit", (limit) => limit + 5)
       await globalSync.project.loadSessions(props.project.worktree)
       await globalSync.project.loadSessions(props.project.worktree)
@@ -2308,7 +2346,7 @@ export default function Layout(props: ParentProps) {
             <SessionSkeleton />
             <SessionSkeleton />
           </Show>
           </Show>
           <For each={sessions()}>
           <For each={sessions()}>
-            {(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />}
+            {(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} children={children()} />}
           </For>
           </For>
           <Show when={hasMore()}>
           <Show when={hasMore()}>
             <div class="relative w-full py-1">
             <div class="relative w-full py-1">