|
|
|
@ -1,31 +1,24 @@ |
|
|
|
/** |
|
|
|
/** |
|
|
|
* Applesauce-core client wrapper |
|
|
|
* Nostr client using nostr-tools |
|
|
|
* Main interface for Nostr operations using applesauce-core and 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 { Relay, type Filter, matchFilter } from 'nostr-tools'; |
|
|
|
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 { config } from './config.js'; |
|
|
|
import { config } from './config.js'; |
|
|
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
|
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
|
|
|
|
|
|
import { cacheEvent, cacheEvents, getEvent, getEventsByKind, getEventsByPubkey } from '../cache/event-cache.js'; |
|
|
|
|
|
|
|
|
|
|
|
export interface PublishOptions { |
|
|
|
export interface PublishOptions { |
|
|
|
relays?: string[]; |
|
|
|
relays?: string[]; |
|
|
|
skipRelayValidation?: boolean; |
|
|
|
skipRelayValidation?: boolean; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class ApplesauceClient { |
|
|
|
class NostrClient { |
|
|
|
private initialized = false; |
|
|
|
private initialized = false; |
|
|
|
private eventStore: EventStore; |
|
|
|
|
|
|
|
private relays: Map<string, Relay> = new Map(); |
|
|
|
private relays: Map<string, Relay> = new Map(); |
|
|
|
private subscriptions: Map<string, { relay: Relay; sub: any }> = new Map(); |
|
|
|
private subscriptions: Map<string, { relay: Relay; sub: any }> = new Map(); |
|
|
|
private nextSubId = 1; |
|
|
|
private nextSubId = 1; |
|
|
|
|
|
|
|
private eventCache: Map<string, NostrEvent> = new Map(); // In-memory cache
|
|
|
|
constructor() { |
|
|
|
|
|
|
|
this.eventStore = new EventStore(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Initialize the client |
|
|
|
* Initialize the client |
|
|
|
@ -54,8 +47,6 @@ class ApplesauceClient { |
|
|
|
try { |
|
|
|
try { |
|
|
|
const relay = await Relay.connect(url); |
|
|
|
const relay = await Relay.connect(url); |
|
|
|
this.relays.set(url, relay); |
|
|
|
this.relays.set(url, relay); |
|
|
|
|
|
|
|
|
|
|
|
// Events will be added to store via subscriptions
|
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
} catch (error) { |
|
|
|
console.error(`Failed to connect to relay ${url}:`, error); |
|
|
|
console.error(`Failed to connect to relay ${url}:`, error); |
|
|
|
throw error; |
|
|
|
throw error; |
|
|
|
@ -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 |
|
|
|
* Publish an event to relays |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
@ -86,8 +108,8 @@ class ApplesauceClient { |
|
|
|
failed: [] as Array<{ relay: string; error: string }> |
|
|
|
failed: [] as Array<{ relay: string; error: string }> |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// Add event to store first
|
|
|
|
// Add event to cache first
|
|
|
|
this.eventStore.add(event); |
|
|
|
this.addToCache(event); |
|
|
|
|
|
|
|
|
|
|
|
// Publish to each relay
|
|
|
|
// Publish to each relay
|
|
|
|
for (const url of relays) { |
|
|
|
for (const url of relays) { |
|
|
|
@ -176,8 +198,8 @@ class ApplesauceClient { |
|
|
|
const client = this; |
|
|
|
const client = this; |
|
|
|
const sub = relay.subscribe(filters, { |
|
|
|
const sub = relay.subscribe(filters, { |
|
|
|
onevent(event: NostrEvent) { |
|
|
|
onevent(event: NostrEvent) { |
|
|
|
// Add to store
|
|
|
|
// Add to cache
|
|
|
|
client.eventStore.add(event); |
|
|
|
client.addToCache(event); |
|
|
|
// Call callback
|
|
|
|
// Call callback
|
|
|
|
onEvent(event, url); |
|
|
|
onEvent(event, url); |
|
|
|
}, |
|
|
|
}, |
|
|
|
@ -211,9 +233,35 @@ class ApplesauceClient { |
|
|
|
): Promise<NostrEvent[]> { |
|
|
|
): Promise<NostrEvent[]> { |
|
|
|
const { useCache = true, cacheResults = true, onUpdate } = options || {}; |
|
|
|
const { useCache = true, cacheResults = true, onUpdate } = options || {}; |
|
|
|
|
|
|
|
|
|
|
|
// Query from event store first if cache is enabled
|
|
|
|
// Query from cache first if enabled
|
|
|
|
if (useCache) { |
|
|
|
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) { |
|
|
|
if (cachedEvents.length > 0) { |
|
|
|
// Return cached events immediately
|
|
|
|
// Return cached events immediately
|
|
|
|
if (onUpdate) { |
|
|
|
if (onUpdate) { |
|
|
|
@ -266,6 +314,14 @@ class ApplesauceClient { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const eventArrayValues = Array.from(eventArray); |
|
|
|
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) { |
|
|
|
if (options.onUpdate) { |
|
|
|
options.onUpdate(eventArrayValues); |
|
|
|
options.onUpdate(eventArrayValues); |
|
|
|
} |
|
|
|
} |
|
|
|
@ -307,9 +363,20 @@ class ApplesauceClient { |
|
|
|
* Get event by ID |
|
|
|
* Get event by ID |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
async getEventById(id: string, relays: string[]): Promise<NostrEvent | null> { |
|
|
|
async getEventById(id: string, relays: string[]): Promise<NostrEvent | null> { |
|
|
|
// Try store first
|
|
|
|
// Try in-memory cache first
|
|
|
|
const event = this.eventStore.getEvent(id); |
|
|
|
const cached = this.eventCache.get(id); |
|
|
|
if (event) return event; |
|
|
|
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
|
|
|
|
// Fetch from relays
|
|
|
|
const filters: Filter[] = [{ ids: [id] }]; |
|
|
|
const filters: Filter[] = [{ ids: [id] }]; |
|
|
|
@ -318,10 +385,10 @@ class ApplesauceClient { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Get event store |
|
|
|
* Get events by filters (from cache only) |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
getEventStore(): EventStore { |
|
|
|
getByFilters(filters: Filter[]): NostrEvent[] { |
|
|
|
return this.eventStore; |
|
|
|
return this.getCachedEvents(filters); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
@ -358,4 +425,4 @@ class ApplesauceClient { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export const nostrClient = new ApplesauceClient(); |
|
|
|
export const nostrClient = new NostrClient(); |