@@ -639,18 +639,6 @@ function calculateQueryComplexity(filter: NostrFilter): number {
639639// Event processing
640640async function processEvent ( event : NostrEvent , sessionId : string , env : Env ) : Promise < { success : boolean ; message : string ; bookmark ?: string } > {
641641 try {
642- // Check cache for duplicate event ID using primary for consistency
643- const session = env . RELAY_DATABASE . withSession ( 'first-primary' ) ;
644- const existingEvent = await session
645- . prepare ( "SELECT id FROM events WHERE id = ? LIMIT 1" )
646- . bind ( event . id )
647- . first ( ) ;
648-
649- if ( existingEvent ) {
650- console . log ( `Duplicate event detected: ${ event . id } ` ) ;
651- return { success : false , message : "duplicate: already have this event" , bookmark : session . getBookmark ( ) ?? undefined } ;
652- }
653-
654642 // NIP-05 validation if enabled (bypassed for kind 1059)
655643 if ( event . kind !== 1059 && checkValidNip05 && event . kind !== 0 ) {
656644 const isValidNIP05 = await validateNIP05FromKind0 ( event . pubkey , env ) ;
@@ -670,7 +658,7 @@ async function processEvent(event: NostrEvent, sessionId: string, env: Env): Pro
670658 return { success : true , message : "Ephemeral event broadcast" } ;
671659 }
672660
673- // Save event directly to database
661+ // Save event directly to database (duplicate check happens inside saveEventToDatabase)
674662 return await saveEventToDatabase ( event , env ) ;
675663
676664 } catch ( error : any ) {
@@ -835,66 +823,54 @@ async function saveEventToDatabase(event: NostrEvent, env: Env): Promise<{ succe
835823 return { success : false , message : "duplicate: event already exists" , bookmark : session . getBookmark ( ) ?? undefined } ;
836824 }
837825
838- // Only insert tags if the event was successfully inserted
839- if ( tagInserts . length > 0 ) {
840- for ( let j = 0 ; j < tagInserts . length ; j += 50 ) {
841- const tagChunk = tagInserts . slice ( j , j + 50 ) ;
842- const tagBatch = tagChunk . map ( t =>
843- session . prepare ( `
844- INSERT INTO tags (event_id, tag_name, tag_value)
845- VALUES (?, ?, ?)
846- ` ) . bind ( event . id , t . name , t . value )
847- ) ;
826+ // Consolidate all post-insert writes (tags, caches, content hash) into a single batch
827+ // to minimize D1 round-trips to the primary
828+ const postInsertBatch : ReturnType < typeof session . prepare > [ ] = [ ] ;
848829
849- if ( tagBatch . length > 0 ) {
850- await session . batch ( tagBatch ) ;
851- }
852- }
830+ // Tag inserts
831+ for ( const t of tagInserts ) {
832+ postInsertBatch . push (
833+ session . prepare ( 'INSERT INTO tags (event_id, tag_name, tag_value) VALUES (?, ?, ?)' ) . bind ( event . id , t . name , t . value )
834+ ) ;
853835 }
854836
855- // Update event tags cache for common tags (legacy single-value cache)
837+ // Event tags cache (legacy single-value cache)
856838 if ( tagP || tagE || tagA ) {
857- await session . prepare ( `
858- INSERT INTO event_tags_cache (event_id, pubkey, kind, created_at, tag_p, tag_e, tag_a)
859- VALUES (?, ?, ?, ?, ?, ?, ?)
860- ON CONFLICT(event_id) DO UPDATE SET
861- tag_p = excluded.tag_p,
862- tag_e = excluded.tag_e,
863- tag_a = excluded.tag_a
864- ` ) . bind (
865- event . id ,
866- event . pubkey ,
867- event . kind ,
868- event . created_at ,
869- tagP ,
870- tagE ,
871- tagA
872- ) . run ( ) ;
873- }
874-
875- // Insert ALL p/e/a/t/d/r/L/s/u tags into multi-value cache
876- // Chunk in batches of 50 to stay well under D1's 100 statement batch limit
839+ postInsertBatch . push (
840+ session . prepare ( `
841+ INSERT INTO event_tags_cache (event_id, pubkey, kind, created_at, tag_p, tag_e, tag_a)
842+ VALUES (?, ?, ?, ?, ?, ?, ?)
843+ ON CONFLICT(event_id) DO UPDATE SET
844+ tag_p = excluded.tag_p, tag_e = excluded.tag_e, tag_a = excluded.tag_a
845+ ` ) . bind ( event . id , event . pubkey , event . kind , event . created_at , tagP , tagE , tagA )
846+ ) ;
847+ }
848+
849+ // Multi-value tag cache for p/e/a/t/d/r/L/s/u tags
877850 const cacheableTags = tagInserts . filter ( t => [ 'p' , 'e' , 'a' , 't' , 'd' , 'r' , 'L' , 's' , 'u' ] . includes ( t . name ) ) ;
878- if ( cacheableTags . length > 0 ) {
879- for ( let j = 0 ; j < cacheableTags . length ; j += 50 ) {
880- const cacheChunk = cacheableTags . slice ( j , j + 50 ) ;
881- const cacheBatch = cacheChunk . map ( t =>
882- session . prepare ( `
883- INSERT OR IGNORE INTO event_tags_cache_multi (event_id, pubkey, kind, created_at, tag_type, tag_value)
884- VALUES (?, ?, ?, ?, ?, ?)
885- ` ) . bind ( event . id , event . pubkey , event . kind , event . created_at , t . name , t . value )
886- ) ;
887- await session . batch ( cacheBatch ) ;
888- }
851+ for ( const t of cacheableTags ) {
852+ postInsertBatch . push (
853+ session . prepare ( `
854+ INSERT OR IGNORE INTO event_tags_cache_multi (event_id, pubkey, kind, created_at, tag_type, tag_value)
855+ VALUES (?, ?, ?, ?, ?, ?)
856+ ` ) . bind ( event . id , event . pubkey , event . kind , event . created_at , t . name , t . value )
857+ ) ;
889858 }
890859
891- // Insert content hash if present
860+ // Content hash
892861 if ( contentHash ) {
893- await session . prepare ( `
894- INSERT INTO content_hashes (hash, event_id, pubkey, created_at)
895- VALUES (?, ?, ?, ?)
896- ON CONFLICT(hash) DO NOTHING
897- ` ) . bind ( contentHash , event . id , event . pubkey , event . created_at ) . run ( ) ;
862+ postInsertBatch . push (
863+ session . prepare ( `
864+ INSERT INTO content_hashes (hash, event_id, pubkey, created_at)
865+ VALUES (?, ?, ?, ?)
866+ ON CONFLICT(hash) DO NOTHING
867+ ` ) . bind ( contentHash , event . id , event . pubkey , event . created_at )
868+ ) ;
869+ }
870+
871+ // Execute all post-insert writes in chunks of 50 (D1 batch limit is 100)
872+ for ( let i = 0 ; i < postInsertBatch . length ; i += 50 ) {
873+ await session . batch ( postInsertBatch . slice ( i , i + 50 ) ) ;
898874 }
899875
900876 // Cache the event ID in worker cache to prevent duplicates
@@ -927,58 +903,57 @@ async function processDeletionEvent(event: NostrEvent, env: Env): Promise<{ succ
927903
928904 let deletedCount = 0 ;
929905 const errors : string [ ] = [ ] ;
906+ const idsToDelete : string [ ] = [ ] ;
930907
908+ // First pass: verify ownership for all events (reads only)
931909 for ( const eventId of deletedEventIds ) {
932910 try {
933- // Check if the event exists in D1
934911 const existing = await session . prepare (
935912 "SELECT pubkey FROM events WHERE id = ? LIMIT 1"
936913 ) . bind ( eventId ) . first ( ) ;
937914
938- // If event doesn't exist in D1, skip (events in queue will be processed later)
939915 if ( ! existing ) {
940916 console . warn ( `Event ${ eventId } not found in D1. Nothing to delete (may be in queue).` ) ;
941917 continue ;
942918 }
943919
944- // Check ownership
945920 if ( existing . pubkey !== event . pubkey ) {
946921 console . warn ( `Event ${ eventId } does not belong to pubkey ${ event . pubkey } . Skipping deletion.` ) ;
947922 errors . push ( `unauthorized: cannot delete event ${ eventId } - wrong pubkey` ) ;
948923 continue ;
949924 }
950925
951- // Delete from D1
952- // Delete associated tags first (due to foreign key constraint)
953- await session . prepare (
954- "DELETE FROM tags WHERE event_id = ?"
955- ) . bind ( eventId ) . run ( ) ;
956-
957- // Delete from content_hashes if exists
958- await session . prepare (
959- "DELETE FROM content_hashes WHERE event_id = ?"
960- ) . bind ( eventId ) . run ( ) ;
961-
962- // Delete from event_tags_cache (both old and new)
963- await session . prepare (
964- "DELETE FROM event_tags_cache WHERE event_id = ?"
965- ) . bind ( eventId ) . run ( ) ;
966- await session . prepare (
967- "DELETE FROM event_tags_cache_multi WHERE event_id = ?"
968- ) . bind ( eventId ) . run ( ) ;
969-
970- // Now delete the event
971- const result = await session . prepare (
972- "DELETE FROM events WHERE id = ?"
973- ) . bind ( eventId ) . run ( ) ;
974-
975- if ( result . meta . changes > 0 ) {
976- console . log ( `Event ${ eventId } deleted from D1.` ) ;
977- deletedCount ++ ;
926+ idsToDelete . push ( eventId ) ;
927+ } catch ( error ) {
928+ console . error ( `Error checking event ${ eventId } :` , error ) ;
929+ errors . push ( `error checking ${ eventId } ` ) ;
930+ }
931+ }
932+
933+ // Second pass: batch delete all verified events in one D1 call
934+ if ( idsToDelete . length > 0 ) {
935+ try {
936+ const deleteStatements : ReturnType < typeof session . prepare > [ ] = [ ] ;
937+ for ( const eventId of idsToDelete ) {
938+ deleteStatements . push (
939+ session . prepare ( "DELETE FROM tags WHERE event_id = ?" ) . bind ( eventId ) ,
940+ session . prepare ( "DELETE FROM content_hashes WHERE event_id = ?" ) . bind ( eventId ) ,
941+ session . prepare ( "DELETE FROM event_tags_cache WHERE event_id = ?" ) . bind ( eventId ) ,
942+ session . prepare ( "DELETE FROM event_tags_cache_multi WHERE event_id = ?" ) . bind ( eventId ) ,
943+ session . prepare ( "DELETE FROM events WHERE id = ?" ) . bind ( eventId ) ,
944+ ) ;
945+ }
946+
947+ // Execute in chunks of 50 to stay under D1's 100 statement batch limit
948+ for ( let i = 0 ; i < deleteStatements . length ; i += 50 ) {
949+ await session . batch ( deleteStatements . slice ( i , i + 50 ) ) ;
978950 }
951+
952+ deletedCount = idsToDelete . length ;
953+ console . log ( `Batch deleted ${ deletedCount } events from D1.` ) ;
979954 } catch ( error ) {
980- console . error ( ` Error deleting event ${ eventId } :` , error ) ;
981- errors . push ( ` error deleting ${ eventId } ` ) ;
955+ console . error ( ' Error batch deleting events:' , error ) ;
956+ errors . push ( ' error batch deleting events' ) ;
982957 }
983958 }
984959
0 commit comments