11export const dynamic = "force-dynamic" ;
22
3+ import { unstable_cache } from "next/cache" ;
34import { createClient } from "@supabase/supabase-js" ;
45import { NextRequest , NextResponse } from "next/server" ;
56import type { ToolEventType , ToolEventMetadata } from "@/lib/types" ;
@@ -48,6 +49,84 @@ function getAnonClient() {
4849 return createClient ( url , anon ) ;
4950}
5051
52+ const fetchSignals = unstable_cache (
53+ async ( sortedIds : string [ ] ) : Promise < ToolRiskSignal [ ] > => {
54+ const db = getAnonClient ( ) ;
55+ if ( ! db )
56+ return sortedIds . map ( ( id ) => ( {
57+ tool_id : id ,
58+ signal : null ,
59+ event_type : null ,
60+ detected_at : null ,
61+ metadata : null ,
62+ } ) ) ;
63+
64+ const thirtyDaysAgo = new Date ( Date . now ( ) - 30 * 24 * 60 * 60 * 1000 ) . toISOString ( ) ;
65+
66+ const { data : events , error } = await db
67+ . from ( "tool_events" )
68+ . select ( "tool_id, type, detected_at, metadata" )
69+ . in ( "tool_id" , sortedIds )
70+ . gte ( "detected_at" , thirtyDaysAgo )
71+ . order ( "detected_at" , { ascending : false } ) ;
72+
73+ if ( error )
74+ return sortedIds . map ( ( id ) => ( {
75+ tool_id : id ,
76+ signal : null ,
77+ event_type : null ,
78+ detected_at : null ,
79+ metadata : null ,
80+ } ) ) ;
81+
82+ const signalMap = new Map < string , ToolRiskSignal > ( ) ;
83+
84+ for ( const ev of events ?? [ ] ) {
85+ const signal = classifyEvent (
86+ ev . type as ToolEventType ,
87+ ( ev . metadata ?? { } ) as ToolEventMetadata
88+ ) ;
89+ if ( ! signal ) continue ;
90+
91+ const existing = signalMap . get ( ev . tool_id ) ;
92+ if ( ! existing ) {
93+ signalMap . set ( ev . tool_id , {
94+ tool_id : ev . tool_id as string ,
95+ signal,
96+ event_type : ev . type as ToolEventType ,
97+ detected_at : ev . detected_at as string ,
98+ metadata : ( ev . metadata ?? { } ) as ToolEventMetadata ,
99+ } ) ;
100+ } else {
101+ const existingPriority = SIGNAL_PRIORITY . indexOf ( existing . signal ! ) ;
102+ const newPriority = SIGNAL_PRIORITY . indexOf ( signal ) ;
103+ if ( newPriority < existingPriority ) {
104+ signalMap . set ( ev . tool_id , {
105+ tool_id : ev . tool_id as string ,
106+ signal,
107+ event_type : ev . type as ToolEventType ,
108+ detected_at : ev . detected_at as string ,
109+ metadata : ( ev . metadata ?? { } ) as ToolEventMetadata ,
110+ } ) ;
111+ }
112+ }
113+ }
114+
115+ return sortedIds . map (
116+ ( id ) =>
117+ signalMap . get ( id ) ?? {
118+ tool_id : id ,
119+ signal : null ,
120+ event_type : null ,
121+ detected_at : null ,
122+ metadata : null ,
123+ }
124+ ) ;
125+ } ,
126+ [ "pulse-signals" ] ,
127+ { revalidate : 300 }
128+ ) ;
129+
51130export async function POST ( req : NextRequest ) {
52131 let body : unknown ;
53132 try {
@@ -64,84 +143,21 @@ export async function POST(req: NextRequest) {
64143 ) ;
65144 }
66145
67- const db = getAnonClient ( ) ;
68- if ( ! db ) {
69- const signals : ToolRiskSignal [ ] = tool_ids . map ( ( id ) => ( {
70- tool_id : id as string ,
71- signal : null ,
72- event_type : null ,
73- detected_at : null ,
74- metadata : null ,
75- } ) ) ;
76- return NextResponse . json ( { signals } satisfies EventsResponse ) ;
77- }
146+ const sortedIds = [ ...tool_ids ] . sort ( ) as string [ ] ;
147+ const signals = await fetchSignals ( sortedIds ) ;
78148
79- const thirtyDaysAgo = new Date ( Date . now ( ) - 30 * 24 * 60 * 60 * 1000 ) . toISOString ( ) ;
80-
81- const { data : events , error } = await db
82- . from ( "tool_events" )
83- . select ( "tool_id, type, detected_at, metadata" )
84- . in ( "tool_id" , tool_ids )
85- . gte ( "detected_at" , thirtyDaysAgo )
86- . order ( "detected_at" , { ascending : false } ) ;
87-
88- if ( error ) {
89- const signals : ToolRiskSignal [ ] = tool_ids . map ( ( id ) => ( {
90- tool_id : id as string ,
91- signal : null ,
92- event_type : null ,
93- detected_at : null ,
94- metadata : null ,
95- } ) ) ;
96- return NextResponse . json ( { signals } satisfies EventsResponse ) ;
97- }
98-
99- // Build signal map: for each tool, keep the highest-priority signal
100- const signalMap = new Map < string , ToolRiskSignal > ( ) ;
101-
102- for ( const ev of events ?? [ ] ) {
103- const signal = classifyEvent (
104- ev . type as ToolEventType ,
105- ( ev . metadata ?? { } ) as ToolEventMetadata
106- ) ;
107- if ( ! signal ) continue ;
108-
109- const existing = signalMap . get ( ev . tool_id ) ;
110- if ( ! existing ) {
111- signalMap . set ( ev . tool_id , {
112- tool_id : ev . tool_id as string ,
113- signal,
114- event_type : ev . type as ToolEventType ,
115- detected_at : ev . detected_at as string ,
116- metadata : ( ev . metadata ?? { } ) as ToolEventMetadata ,
117- } ) ;
118- } else {
119- // Replace only if the new signal has higher priority
120- const existingPriority = SIGNAL_PRIORITY . indexOf ( existing . signal ! ) ;
121- const newPriority = SIGNAL_PRIORITY . indexOf ( signal ) ;
122- if ( newPriority < existingPriority ) {
123- signalMap . set ( ev . tool_id , {
124- tool_id : ev . tool_id as string ,
125- signal,
126- event_type : ev . type as ToolEventType ,
127- detected_at : ev . detected_at as string ,
128- metadata : ( ev . metadata ?? { } ) as ToolEventMetadata ,
129- } ) ;
130- }
131- }
132- }
133-
134- const signals : ToolRiskSignal [ ] = tool_ids . map ( ( id ) => {
135- return (
149+ // Re-order signals to match the original request order
150+ const signalMap = new Map ( signals . map ( ( s ) => [ s . tool_id , s ] ) ) ;
151+ const ordered : ToolRiskSignal [ ] = tool_ids . map (
152+ ( id ) =>
136153 signalMap . get ( id as string ) ?? {
137154 tool_id : id as string ,
138155 signal : null ,
139156 event_type : null ,
140157 detected_at : null ,
141158 metadata : null ,
142159 }
143- ) ;
144- } ) ;
160+ ) ;
145161
146- return NextResponse . json ( { signals } satisfies EventsResponse ) ;
162+ return NextResponse . json ( { signals : ordered } satisfies EventsResponse ) ;
147163}
0 commit comments