@ -1,31 +1,24 @@
@@ -1,31 +1,24 @@
/ * *
* Applesauce - core client wrapper
* Main interface for Nostr operations using applesauce - core and nostr - tools
* Nostr client using nostr - tools
* Main interface for Nostr operations using only nostr - tools
* /
// @ts-expect-error - applesauce-core types may not be available, but package works at runtime
import { EventStore } from 'applesauce-core' ;
// @ts-expect-error - applesauce-core types may not be available, but package works at runtime
import type { Filter } from 'applesauce-core/helpers' ;
import { Relay } from 'nostr-tools/relay' ;
import { Relay , type Filter , matchFilter } from 'nostr-tools' ;
import { config } from './config.js' ;
import type { NostrEvent } from '../../types/nostr.js' ;
import { cacheEvent , cacheEvents , getEvent , getEventsByKind , getEventsByPubkey } from '../cache/event-cache.js' ;
export interface PublishOptions {
relays? : string [ ] ;
skipRelayValidation? : boolean ;
}
class Applesauce Client {
class Nostr Client {
private initialized = false ;
private eventStore : EventStore ;
private relays : Map < string , Relay > = new Map ( ) ;
private subscriptions : Map < string , { relay : Relay ; sub : any } > = new Map ( ) ;
private nextSubId = 1 ;
constructor ( ) {
this . eventStore = new EventStore ( ) ;
}
private eventCache : Map < string , NostrEvent > = new Map ( ) ; // In-memory cache
/ * *
* Initialize the client
@ -54,8 +47,6 @@ class ApplesauceClient {
@@ -54,8 +47,6 @@ class ApplesauceClient {
try {
const relay = await Relay . connect ( url ) ;
this . relays . set ( url , relay ) ;
// Events will be added to store via subscriptions
} catch ( error ) {
console . error ( ` Failed to connect to relay ${ url } : ` , error ) ;
throw error ;
@ -73,6 +64,37 @@ class ApplesauceClient {
@@ -73,6 +64,37 @@ class ApplesauceClient {
}
}
/ * *
* Add event to cache
* /
private addToCache ( event : NostrEvent ) : void {
this . eventCache . set ( event . id , event ) ;
// Also cache to IndexedDB
cacheEvent ( event ) . catch ( ( error ) = > {
console . error ( 'Error caching event:' , error ) ;
} ) ;
}
/ * *
* Get events from cache that match filters
* /
private getCachedEvents ( filters : Filter [ ] ) : NostrEvent [ ] {
const results : NostrEvent [ ] = [ ] ;
const seen = new Set < string > ( ) ;
for ( const filter of filters ) {
for ( const event of this . eventCache . values ( ) ) {
if ( seen . has ( event . id ) ) continue ;
if ( matchFilter ( filter , event ) ) {
results . push ( event ) ;
seen . add ( event . id ) ;
}
}
}
return results ;
}
/ * *
* Publish an event to relays
* /
@ -86,8 +108,8 @@ class ApplesauceClient {
@@ -86,8 +108,8 @@ class ApplesauceClient {
failed : [ ] as Array < { relay : string ; error : string } >
} ;
// Add event to stor e first
this . eventStore . add ( event ) ;
// Add event to cach e first
this . addToCache ( event ) ;
// Publish to each relay
for ( const url of relays ) {
@ -176,8 +198,8 @@ class ApplesauceClient {
@@ -176,8 +198,8 @@ class ApplesauceClient {
const client = this ;
const sub = relay . subscribe ( filters , {
onevent ( event : NostrEvent ) {
// Add to stor e
client . eventStore . add ( event ) ;
// Add to cach e
client . addToCache ( event ) ;
// Call callback
onEvent ( event , url ) ;
} ,
@ -211,9 +233,35 @@ class ApplesauceClient {
@@ -211,9 +233,35 @@ class ApplesauceClient {
) : Promise < NostrEvent [ ] > {
const { useCache = true , cacheResults = true , onUpdate } = options || { } ;
// Query from event store first if cache is enabled
// Query from cache first if enabled
if ( useCache ) {
const cachedEvents = this . eventStore . getByFilters ( filters ) ;
// Try in-memory cache first
let cachedEvents = this . getCachedEvents ( filters ) ;
// If no results in memory, try IndexedDB
if ( cachedEvents . length === 0 ) {
try {
// Try to get from IndexedDB based on filter
for ( const filter of filters ) {
if ( filter . kinds && filter . kinds . length === 1 ) {
const dbEvents = await getEventsByKind ( filter . kinds [ 0 ] , filter . limit || 50 ) ;
cachedEvents . push ( . . . dbEvents ) ;
}
if ( filter . authors && filter . authors . length === 1 ) {
const dbEvents = await getEventsByPubkey ( filter . authors [ 0 ] , filter . limit || 50 ) ;
cachedEvents . push ( . . . dbEvents ) ;
}
}
// Add to in-memory cache
for ( const event of cachedEvents ) {
this . eventCache . set ( event . id , event ) ;
}
} catch ( error ) {
console . error ( 'Error loading from IndexedDB:' , error ) ;
}
}
if ( cachedEvents . length > 0 ) {
// Return cached events immediately
if ( onUpdate ) {
@ -266,6 +314,14 @@ class ApplesauceClient {
@@ -266,6 +314,14 @@ class ApplesauceClient {
}
const eventArrayValues = Array . from ( eventArray ) ;
// Cache results
if ( options . cacheResults && eventArrayValues . length > 0 ) {
cacheEvents ( eventArrayValues ) . catch ( ( error ) = > {
console . error ( 'Error caching events:' , error ) ;
} ) ;
}
if ( options . onUpdate ) {
options . onUpdate ( eventArrayValues ) ;
}
@ -307,9 +363,20 @@ class ApplesauceClient {
@@ -307,9 +363,20 @@ class ApplesauceClient {
* Get event by ID
* /
async getEventById ( id : string , relays : string [ ] ) : Promise < NostrEvent | null > {
// Try store first
const event = this . eventStore . getEvent ( id ) ;
if ( event ) return event ;
// Try in-memory cache first
const cached = this . eventCache . get ( id ) ;
if ( cached ) return cached ;
// Try IndexedDB
try {
const dbEvent = await getEvent ( id ) ;
if ( dbEvent ) {
this . eventCache . set ( dbEvent . id , dbEvent ) ;
return dbEvent ;
}
} catch ( error ) {
console . error ( 'Error loading from IndexedDB:' , error ) ;
}
// Fetch from relays
const filters : Filter [ ] = [ { ids : [ id ] } ] ;
@ -318,10 +385,10 @@ class ApplesauceClient {
@@ -318,10 +385,10 @@ class ApplesauceClient {
}
/ * *
* Get event store
* Get events by filters ( from cache only )
* /
getEventStore ( ) : EventStore {
return this . eventStore ;
getByFilters ( filters : Filter [ ] ) : NostrEvent [ ] {
return this . getCachedEvents ( filters ) ;
}
/ * *
@ -358,4 +425,4 @@ class ApplesauceClient {
@@ -358,4 +425,4 @@ class ApplesauceClient {
}
}
export const nostrClient = new Applesauce Client( ) ;
export const nostrClient = new Nostr Client( ) ;