Skip to content

Latest commit

 

History

History
346 lines (242 loc) · 12.8 KB

File metadata and controls

346 lines (242 loc) · 12.8 KB

@nostrwatch/route66

Relay aggregation and state management system for NIP-66 monitor data.

npm version License Status Runtime

Overview

@nostrwatch/route66 aggregates relay (a WebSocket server that stores and forwards Nostr events) status data from NIP-66 (the Nostr Implementation Possibility for relay monitoring) monitors and exposes it through a consistent querying API. It sits between raw relay checks (performed by @nostrwatch/nocap) and the application layer — the GUI and REST API consume route66, not raw check events.

route66 uses a pluggable adapter architecture for both data caching and WebSocket connections. A cache adapter stores and queries relay state locally; a WebSocket adapter handles connections to monitoring relays that publish NIP-66 events. Both dimensions are independently swappable, allowing route66 to work in browser, Node.js, or worker thread environments.

StateManager provides shared singleton state, lifecycle events, and local storage access. ChronicleService provides time-series relay history from Kind 1066 delta events. MonitorService tracks which monitors are active and manages subscription filters.

Installation

pnpm add @nostrwatch/route66

Or with npm:

npm install @nostrwatch/route66

Quick Start

import {Route66} from '@nostrwatch/route66'
import {NostrSqliteAdapter} from '@nostrwatch/route66-cacheadapter-nostrsqlite'
import {NostrToolsAdapter} from '@nostrwatch/route66-wsadapter-nostrtools'

const cacheAdapter = new NostrSqliteAdapter()
const websocketAdapter = new NostrToolsAdapter()

const r66 = new Route66({cacheAdapter, websocketAdapter})
await r66.boot()
await r66.ready()

const relays = await r66.relays?.getRelays({network: 'clearnet'})
console.log(relays)
// { 'wss://relay.damus.io': { record: { url: '...', network: 'clearnet', ... } }, ... }

API

Route66

The main entry point. Wraps the cache and WebSocket adapters and exposes RelayService, MonitorService, and StateManager through a unified interface.

class Route66 {
  constructor(adapters: IAdaptersArgument)
  boot(): Promise<void>
  ready(): Promise<Route66>
  shutdown(): Promise<void>
  restart(): Promise<void>
  on(event: string, listener: (...args: any[]) => void): void
  once(event: string, listener: (...args: any[]) => void): void
  off(event: string, listener: (...args: any[]) => void): void
  destroy(): void
  get relays(): RelayService | undefined
  get monitors(): MonitorService | undefined
  get cache(): ICacheAdapter | undefined
  get websocket(): IWebsocketAdapter | undefined
  get state(): typeof StateManager
  get initialized(): boolean
}

constructor(adapters)

Creates a Route66 instance. adapters must implement IAdaptersArgument:

Field Type Description
cacheAdapter ICacheAdapter Stores and queries relay state locally
websocketAdapter IWebsocketAdapter Handles WebSocket connections to monitoring relays

boot()

Initializes workers, services, and waits for adapters to signal readiness. Call once after construction. Equivalent to calling init() followed by ready().

ready()

Resolves when initialization is complete. Safe to call multiple times; resolves immediately if already initialized.

shutdown()

Gracefully shuts down all adapters and workers.

relays

Returns the RelayService instance, which provides relay state queries. Available after boot().

monitors

Returns the MonitorService instance, which manages active monitor subscriptions. Available after boot().


StateManager

A static singleton that provides shared state, event emission, and local storage access across all route66 services.

class StateManager {
  static get abortController(): AbortController
  static get abortSignal(): AbortSignal
  static abort(): void
  static aborted(): boolean
  static resetAbort(): void
  static emit(event: string, ...args: any[]): void
  static on(event: string, listener: (...args: any[]) => void): void
  static once(event: string, listener: (...args: any[]) => void): void
  static off(event: string, listener?: (...args: any[]) => void): void
  static set(key: string, value: any): void
  static get(key: string): any
  static remove(key: string): void
  static clear(): void
  static size(): number
}

abort()

Cancels all in-flight async operations that respect StateManager.abortSignal. Emits an 'abort' event.

resetAbort()

Creates a new AbortController, re-enabling operations after a previous abort() call.

emit(event, ...args)

Emits a named event to all listeners registered with on() and once().

set(key, value) / get(key) / remove(key) / clear()

Local storage operations, namespaced to the 'state' prefix. Used internally by services to persist lightweight state across page reloads.


ChronicleService

Provides relay history and time series data from Kind 1066 delta events (NIP-66 append-only history log).

class ChronicleService {
  constructor(adapters: IAdaptersArgument, options?: ChronicleServiceOptions)
  syncRelay(relay: string): Promise<void>
  getTimeSeriesData(options: TimeSeriesOptions): Promise<TimeSeriesPoint[]>
  getUptimePeriods(relay: string, since?: number, until?: number): Promise<UptimePeriod[]>
}

ChronicleServiceOptions

Option Type Default Description
autoSync boolean false Automatically subscribe to Kind 1066 events for queried relays
syncRelays string[] [] Relay URLs to fetch Kind 1066 events from

TimeSeriesOptions

Option Type Description
relay string Relay URL to query
since number Start of time range (unix timestamp in seconds)
until number End of time range (unix timestamp in seconds)
type 'rtt' | 'uptime' | 'changes' Type of time series data
aggregate.bucketSize number Bucket size in seconds for aggregation
aggregate.fn 'avg' | 'min' | 'max' | 'sum' Aggregation function

UptimePeriod

Field Type Description
start number Period start timestamp
end number | null Period end timestamp (null if ongoing)
online boolean Whether relay was online during this period
rtt number (optional) Round-trip time in milliseconds

Example:

import {ChronicleService} from '@nostrwatch/route66'

const chronicle = new ChronicleService(r66.adapters, {
  autoSync: true,
  syncRelays: ['wss://relay.nostr.watch']
})

await chronicle.syncRelay('wss://relay.damus.io')

const rttData = await chronicle.getTimeSeriesData({
  relay: 'wss://relay.damus.io',
  type: 'rtt',
  since: Math.floor(Date.now() / 1000) - 86400
})
console.log(rttData)
// [{ timestamp: 1709000000, value: 120 }, ...]

MonitorService

Manages subscriptions to NIP-66 monitors (Kind 10166 announcements) and tracks which monitors are currently active.

class MonitorService {
  get array(): Monitor[]
  get map(): Map<string, Monitor>
  get enabledMonitors(): Monitor[]
  get activeEnabledMonitors(): Monitor[]
  get disabledMonitors(): Monitor[]
  ready(): Promise<void>
}

MonitorService is created automatically when Route66 is initialized with both a cache and WebSocket adapter. Access it via r66.monitors.

Adapter Pattern

route66 has two independent adapter dimensions: cache adapters store and query relay state data, and WebSocket adapters connect to monitoring relays over Nostr. Both are provided at construction time via IAdaptersArgument.

Cache Adapters

Cache adapters implement ICacheAdapter and extend the CacheAdapter base class. They store NIP-66 relay check events locally and answer queries from RelayService.

Interface to implement:

import {CacheAdapter, type ICacheAdapter} from '@nostrwatch/route66'
import type {IEvent} from '@nostrwatch/route66'

export class MyCacheAdapter extends CacheAdapter implements ICacheAdapter {
  readonly slug = 'MyCacheAdapter'

  async ready(): Promise<void> {
    // Signal that the adapter is initialized and ready to serve queries
  }

  async REQ(filters: any[]): Promise<IEvent[]> {
    // Execute a Nostr REQ query and return matching events
    return []
  }

  async COUNT(filters: any[]): Promise<number> {
    // Return the count of events matching the given filters
    return 0
  }

  async DELETE(filters: any[]): Promise<string[]> {
    // Delete events matching the given filters; return deleted event IDs
    return []
  }

  async DUMP(): Promise<Uint8Array> {
    // Export the full event store as a binary blob
    return new Uint8Array()
  }

  async CLOSE(subId: string): Promise<boolean> {
    // Close a subscription by ID; return true if closed successfully
    return true
  }

  async WIPE(): Promise<boolean> {
    // Clear all stored events; return true if successful
    return true
  }

  async addEvent(event: IEvent): Promise<void> {
    // Insert a single event; no-op if already present
  }

  async addEvents(events: IEvent[]): Promise<void> {
    // Batch insert events
  }

  async putEvent(event: IEvent): Promise<void> {
    // Upsert a single event (insert or replace)
  }
}

Registration:

import {Route66} from '@nostrwatch/route66'

const cacheAdapter = new MyCacheAdapter()
const r66 = new Route66({cacheAdapter, websocketAdapter})

Existing cache adapters:

WebSocket Adapters

WebSocket adapters implement IWebsocketAdapter and extend the WebsocketAdapter base class. They open connections to Nostr monitoring relays and deliver incoming NIP-66 events to the rest of route66.

Registration:

import {Route66} from '@nostrwatch/route66'

const websocketAdapter = new MyWebsocketAdapter()
const r66 = new Route66({cacheAdapter, websocketAdapter})

Existing WebSocket adapters:

Reference implementations for both adapter types are in libraries/route66/adapters/.

Known Limitations

  • Hardcoded filter limit: MonitorService caps subscriptions at 10 filters (MAX_FILTERS = 10) regardless of what the relay actually supports. Relays that allow more filters will not be fully utilized; relays that allow fewer may reject subscriptions silently. Split subscriptions across multiple MonitorService instances as a workaround. See CONCERNS.md — Hardcoded Filter Limits.

  • Default relays hardcoded in source: Default relay URLs in RelayService and MonitorService are set in the constructor rather than loaded from configuration. Changing the default relay list requires a code change. Pass an explicit relay list to the respective service constructor as a workaround. See CONCERNS.md — Default Relay Configuration.

  • Incomplete RTT extraction: RTT values are not extracted from chronicle period metadata in ChronicleService. Uptime and downtime records may be missing RTT data. No workaround is available at this time. See CONCERNS.md — Incomplete RTT Extraction.

Agent Skills

No agent skills defined yet for this package.

Related Packages

License

MIT