22// Nostr is a simple protocol over WS: send ["REQ", subId, filter], receive
33// ["EVENT", subId, event]. Node 24 has a built-in global WebSocket.
44//
5- // Subscribes to kind 0 (profile), 3 (follows / social graph), 10002 (relay
6- // list) and upserts each raw event into Mongo.
5+ // Ingestion is composed of "hoses" (see src/hoses/), each owning a set of
6+ // event kinds. Which hoses run is a switch — the `HOSES` env (comma list of
7+ // names; default: all registered) — so a deploy can run a single hose without
8+ // double-writing against the legacy firehose processes during the migration.
79import { upsertEvent , connect } from './db.js' ;
810import profilesHose from './hoses/profiles.js' ;
911import 'dotenv/config' ;
1012
1113const RELAYS = ( process . env . RELAYS || 'wss://relay.damus.io,wss://nos.lol,wss://relay.primal.net' )
1214 . split ( ',' ) . map ( ( s ) => s . trim ( ) ) . filter ( Boolean ) ;
1315
14- // Registered hoses (phase 1: profiles only). Each owns its kinds + ingest.
15- // Kinds with no hose fall back to the legacy raw upsert until their own phase.
16- const HOSES = [ profilesHose ] ;
17- const hoseFor = ( kind ) => HOSES . find ( ( h ) => h . kinds . includes ( kind ) ) ;
18- const KINDS = [ ...new Set ( [ ...HOSES . flatMap ( ( h ) => h . kinds ) , 3 , 10002 ] ) ] ;
16+ // All hoses that exist (grows each phase). `planIngest` selects which run.
17+ const ALL_HOSES = [ profilesHose ] ;
18+
19+ // Kinds not yet migrated to a hose: 3 (follows), 10002 (relay lists). They use
20+ // the legacy raw-upsert path, on by default for local parity. Set
21+ // INDEX_LEGACY_KINDS=0 to turn it off so a single-hose deploy never writes
22+ // collections still owned by the legacy firehose/followshose processes.
23+ const LEGACY_KINDS = [ 3 , 10002 ] ;
24+
25+ /**
26+ * Resolve the ingest plan from the registered hoses + env switches. Pure (env
27+ * passed in) so it's unit-testable. Returns the active hoses, a kind→hose
28+ * lookup, the union of kinds to subscribe to, and the legacy-fallback state.
29+ */
30+ export function planIngest ( allHoses = ALL_HOSES , env = process . env ) {
31+ const want = ( env . HOSES || allHoses . map ( ( h ) => h . name ) . join ( ',' ) )
32+ . split ( ',' ) . map ( ( s ) => s . trim ( ) ) . filter ( Boolean ) ;
33+ const hoses = allHoses . filter ( ( h ) => want . includes ( h . name ) ) ;
34+ const unknown = want . filter ( ( n ) => ! allHoses . some ( ( h ) => h . name === n ) ) ;
35+ const legacy = env . INDEX_LEGACY_KINDS !== '0' && env . INDEX_LEGACY_KINDS !== 'false' ;
36+ const hoseFor = ( kind ) => hoses . find ( ( h ) => h . kinds . includes ( kind ) ) ;
37+ const legacyKinds = legacy ? LEGACY_KINDS . filter ( ( k ) => ! hoseFor ( k ) ) : [ ] ;
38+ const kinds = [ ...new Set ( [ ...hoses . flatMap ( ( h ) => h . kinds ) , ...legacyKinds ] ) ] ;
39+ return { hoses, hoseFor, kinds, legacy, legacyKinds, unknown } ;
40+ }
41+
1942const SUB_ID = 'beacon' ;
2043const RECONNECT_MS = 3000 ;
2144
22- function connectRelay ( url , onEvent ) {
45+ function connectRelay ( url , kinds , onEvent ) {
2346 let ws , closed = false , timer ;
2447 const open = ( ) => {
2548 if ( closed ) return ;
2649 ws = new WebSocket ( url ) ;
27- ws . addEventListener ( 'open' , ( ) => ws . send ( JSON . stringify ( [ 'REQ' , SUB_ID , { kinds : KINDS } ] ) ) ) ;
50+ ws . addEventListener ( 'open' , ( ) => ws . send ( JSON . stringify ( [ 'REQ' , SUB_ID , { kinds } ] ) ) ) ;
2851 ws . addEventListener ( 'message' , ( m ) => {
2952 try {
3053 const msg = JSON . parse ( typeof m . data === 'string' ? m . data : m . data . toString ( ) ) ;
@@ -40,18 +63,24 @@ function connectRelay(url, onEvent) {
4063
4164export async function runIndexer ( ) {
4265 const db = await connect ( ) ;
43- for ( const h of HOSES ) await h . ensureIndexes ( db ) ;
44- console . log ( `[beacon] indexing kinds ${ KINDS . join ( ',' ) } from ${ RELAYS . length } relays` ) ;
66+ const { hoses, hoseFor, kinds, legacy, unknown } = planIngest ( ) ;
67+ if ( unknown . length ) console . warn ( `[beacon] unknown hose(s) in HOSES, ignored: ${ unknown . join ( ', ' ) } ` ) ;
68+ if ( ! kinds . length ) { console . warn ( '[beacon] no hoses enabled and no legacy kinds — nothing to index' ) ; return { stop ( ) { } } ; }
69+ for ( const h of hoses ) await h . ensureIndexes ( db ) ;
70+ console . log ( `[beacon] hoses: [${ hoses . map ( ( h ) => h . name ) . join ( ', ' ) || 'none' } ] legacy kinds: ${ legacy ? 'on' : 'off' } subscribing kinds ${ kinds . join ( ',' ) } from ${ RELAYS . length } relays` ) ;
4571 const onEvent = async ( event ) => {
4672 try {
4773 const hose = hoseFor ( event . kind ) ;
48- const stored = hose ? await hose . ingest ( event , db ) : await upsertEvent ( event ) ;
74+ let stored ;
75+ if ( hose ) stored = await hose . ingest ( event , db ) ;
76+ else if ( legacy ) stored = await upsertEvent ( event ) ;
77+ else return ; // not an enabled kind
4978 if ( stored ) console . log ( `[beacon] kind ${ event . kind } ${ String ( event . pubkey ) . slice ( 0 , 12 ) } …` ) ;
5079 } catch ( e ) {
5180 console . error ( '[beacon] ingest error:' , e . message ) ;
5281 }
5382 } ;
54- const stoppers = RELAYS . map ( ( url ) => connectRelay ( url , onEvent ) ) ;
83+ const stoppers = RELAYS . map ( ( url ) => connectRelay ( url , kinds , onEvent ) ) ;
5584 const stop = ( ) => stoppers . forEach ( ( s ) => s ( ) ) ;
5685 process . on ( 'SIGINT' , ( ) => { stop ( ) ; process . exit ( 0 ) ; } ) ;
5786 process . on ( 'SIGTERM' , ( ) => { stop ( ) ; process . exit ( 0 ) ; } ) ;
0 commit comments