22 * Account Map API route.
33 *
44 * Returns aggregated entity data (projects, nodes, workspaces, sessions,
5- * tasks, ideas ) and their relationships for the authenticated user.
5+ * tasks) and their relationships for the authenticated user.
66 * Used by the Account Map visualization page.
7+ *
8+ * Performance note: this endpoint fans out to one ProjectData DO per project
9+ * to fetch sessions. For accounts with many projects, this can be slow on
10+ * cold DOs (~5-20ms each). KV caching (30s TTL) absorbs repeated reads.
711 */
812import { Hono } from 'hono' ;
913import { eq } from 'drizzle-orm' ;
@@ -13,22 +17,61 @@ import * as schema from '../db/schema';
1317import { getUserId , requireAuth , requireApproved } from '../middleware/auth' ;
1418import * as projectDataService from '../services/project-data' ;
1519
16- /**
17- * Default max entities per type. Configurable via ACCOUNT_MAP_MAX_ENTITIES env var.
18- */
20+ /** Default max entities per type from D1. Configurable via ACCOUNT_MAP_MAX_ENTITIES. */
1921const DEFAULT_MAX_ENTITIES = 200 ;
2022
23+ /** Max sessions fetched per project from DO. Configurable via ACCOUNT_MAP_MAX_SESSIONS_PER_PROJECT. */
24+ const DEFAULT_MAX_SESSIONS_PER_PROJECT = 20 ;
25+
26+ /** KV cache TTL in seconds. Configurable via ACCOUNT_MAP_CACHE_TTL_SECONDS. */
27+ const DEFAULT_CACHE_TTL_SECONDS = 30 ;
28+
29+ interface SessionSummary {
30+ id : string ;
31+ projectId : string ;
32+ topic : string | null ;
33+ status : string ;
34+ messageCount : number ;
35+ workspaceId : string | null ;
36+ taskId : string | null ;
37+ }
38+
39+ interface Relationship {
40+ source : string ;
41+ target : string ;
42+ type : string ;
43+ active : boolean ;
44+ }
45+
2146const accountMapRoutes = new Hono < { Bindings : Env } > ( ) ;
2247
2348accountMapRoutes . use ( '/*' , requireAuth ( ) , requireApproved ( ) ) ;
2449
2550accountMapRoutes . get ( '/' , async ( c ) => {
2651 const userId = getUserId ( c ) ;
52+
53+ const parsedMax = parseInt ( c . env . ACCOUNT_MAP_MAX_ENTITIES ?? '' , 10 ) ;
54+ const maxEntities = Number . isFinite ( parsedMax ) && parsedMax > 0
55+ ? parsedMax
56+ : DEFAULT_MAX_ENTITIES ;
57+
58+ const parsedSessionCap = parseInt ( c . env . ACCOUNT_MAP_MAX_SESSIONS_PER_PROJECT ?? '' , 10 ) ;
59+ const maxSessionsPerProject = Number . isFinite ( parsedSessionCap ) && parsedSessionCap > 0
60+ ? parsedSessionCap
61+ : DEFAULT_MAX_SESSIONS_PER_PROJECT ;
62+
63+ const cacheTtl = parseInt ( c . env . ACCOUNT_MAP_CACHE_TTL_SECONDS ?? '' , 10 ) || DEFAULT_CACHE_TTL_SECONDS ;
64+
65+ // --- KV cache check ---
66+ const cacheKey = `account-map:${ userId } ` ;
67+ const cached = await c . env . KV . get ( cacheKey , 'json' ) ;
68+ if ( cached ) {
69+ return c . json ( cached ) ;
70+ }
71+
72+ // --- D1 queries: projects, nodes, workspaces, tasks ---
2773 const db = drizzle ( c . env . DATABASE , { schema } ) ;
28- const maxEntities =
29- parseInt ( c . env . ACCOUNT_MAP_MAX_ENTITIES ?? '' , 10 ) || DEFAULT_MAX_ENTITIES ;
3074
31- // --- D1 queries: projects, nodes, workspaces ---
3275 const [ projectRows , nodeRows , workspaceRows , taskRows ] = await Promise . all ( [
3376 db
3477 . select ( {
@@ -90,66 +133,40 @@ accountMapRoutes.get('/', async (c) => {
90133 . limit ( maxEntities ) ,
91134 ] ) ;
92135
93- // --- Fan out to ProjectData DOs for sessions and ideas ---
94- interface SessionSummary {
95- id : string ;
96- projectId : string ;
97- topic : string | null ;
98- status : string ;
99- messageCount : number ;
100- workspaceId : string | null ;
101- taskId : string | null ;
102- }
103-
104- interface IdeaSummary {
105- id : string ;
106- projectId : string ;
107- title : string ;
108- status : string ;
109- linkedSessionCount : number ;
110- }
111-
112- const allSessions : SessionSummary [ ] = [ ] ;
113- const allIdeas : IdeaSummary [ ] = [ ] ;
114-
136+ // --- Fan out to ProjectData DOs for sessions ---
137+ // Each callback returns its sessions; collected after settlement to avoid shared-array mutation.
115138 const doResults = await Promise . allSettled (
116139 projectRows . map ( async ( project ) => {
117140 const sessionsResult = await projectDataService . listSessions (
118141 c . env ,
119142 project . id ,
120143 null ,
121- maxEntities ,
144+ maxSessionsPerProject ,
122145 0
123146 ) ;
124147
125- for ( const s of sessionsResult . sessions ) {
126- allSessions . push ( {
127- id : s . id as string ,
128- projectId : project . id ,
129- topic : ( s . topic as string ) ?? null ,
130- status : ( s . status as string ) ?? 'unknown' ,
131- messageCount : ( s . messageCount as number ) ?? 0 ,
132- workspaceId : ( s . workspaceId as string ) ?? null ,
133- taskId : ( s . taskId as string ) ?? null ,
134- } ) ;
135- }
148+ return sessionsResult . sessions . map ( ( s ) : SessionSummary => ( {
149+ id : s . id as string ,
150+ projectId : project . id ,
151+ topic : ( s . topic as string ) ?? null ,
152+ status : ( s . status as string ) ?? 'unknown' ,
153+ messageCount : ( s . messageCount as number ) ?? 0 ,
154+ workspaceId : ( s . workspaceId as string ) ?? null ,
155+ taskId : ( s . taskId as string ) ?? null ,
156+ } ) ) ;
136157 } )
137158 ) ;
138159
160+ const allSessions : SessionSummary [ ] = [ ] ;
139161 for ( const result of doResults ) {
140- if ( result . status === 'rejected' ) {
162+ if ( result . status === 'fulfilled' ) {
163+ allSessions . push ( ...result . value ) ;
164+ } else {
141165 console . warn ( 'AccountMap: failed to fetch DO data:' , result . reason ) ;
142166 }
143167 }
144168
145169 // --- Build relationships ---
146- interface Relationship {
147- source : string ;
148- target : string ;
149- type : string ;
150- active : boolean ;
151- }
152-
153170 const relationships : Relationship [ ] = [ ] ;
154171
155172 // Project → Workspace
@@ -222,7 +239,7 @@ accountMapRoutes.get('/', async (c) => {
222239 }
223240 }
224241
225- return c . json ( {
242+ const payload = {
226243 projects : projectRows ,
227244 nodes : nodeRows ,
228245 workspaces : workspaceRows ,
@@ -236,9 +253,14 @@ accountMapRoutes.get('/', async (c) => {
236253 executionStep : t . executionStep ,
237254 priority : t . priority ,
238255 } ) ) ,
239- ideas : allIdeas ,
240256 relationships,
241- } ) ;
257+ } ;
258+
259+ // --- Cache in KV (fire-and-forget) ---
260+ void c . env . KV . put ( cacheKey , JSON . stringify ( payload ) , { expirationTtl : cacheTtl } )
261+ . catch ( ( err : unknown ) => console . warn ( 'AccountMap: KV cache write failed:' , err ) ) ;
262+
263+ return c . json ( payload ) ;
242264} ) ;
243265
244266export { accountMapRoutes } ;
0 commit comments