Skip to content

Architecture

Tsunagiya is a mock library that makes existing Nostr client code testable without modification by replacing globalThis.WebSocket.

Overview


Component Structure

src/
├── pool.ts         MockPool       — Global manager, WebSocket replacement
├── relay.ts        MockRelay      — Per-URL virtual relay
├── websocket.ts    MockWebSocket  — WebSocket API-compatible mock
├── filter.ts       matchFilter etc — NIP-01 filter matching (pure functions)
├── auth.ts         AuthState      — NIP-42 AUTH challenge/response
├── event_kind.ts                  — Event kind classification (Regular/Replaceable/Ephemeral etc.)
├── logger.ts                      — Logger
└── types.ts                       — Type definitions

Class Relationship Diagram

MockPool (src/pool.ts)

A container that manages multiple MockRelay instances keyed by URL. The entry point for tests.

MemberTypeRole
#relaysMap<string, MockRelay>URL → MockRelay mapping
#originalWebSockettypeof WebSocket | nullStores original WebSocket for uninstall
#originalFetchtypeof fetch | nullStores original fetch for uninstall

Key methods:

  • relay(url, options?) — Register/retrieve a MockRelay (returns existing instance for same URL)
  • install() — Replaces globalThis.WebSocket and globalThis.fetch
  • uninstall() — Restores original implementations
  • reset() — Clears state of all relays

MockRelay (src/relay.ts)

A virtual Nostr relay operating per URL. Provides event store, filtering, custom handlers, assertion helpers, instability simulation, and NIP-42 AUTH.

FieldTypeRole
#storeNostrEvent[]Event store (persistent events)
#receivedReceivedMessage[]Log of received messages
#connectionsSet<MockWebSocket>List of active connections
#subscriptionsMap<MockWebSocket, Map<string, NostrFilter[]>>Subscriptions per connection
#authStateAuthStateNIP-42 authentication state
#pendingTimersSet<ReturnType<typeof setTimeout>>Pending timers (cleared on reset)

MockWebSocket (src/websocket.ts)

The replacement for globalThis.WebSocket. Extends EventTarget to emulate the WebSocket API.

MemberRole
static _resolveRelayURL → MockRelay resolver function set by MockPool
#relayThe MockRelay this socket is routed to
send(data)Forwards to relay._handleMessage()
_receiveMessage(data)Receive callback invoked by the relay
_forceClose(code, reason)Forced disconnect invoked by the relay

WebSocket readyState Transitions

filter.ts

Pure functions for NIP-01 filter matching. No side effects.

FunctionDescription
matchFilter(event, filter)Checks if an event matches a single filter (all conditions AND)
matchFilters(event, filters)Checks if an event matches any of multiple filters (OR between filters)
filterEvents(events, filter)Filters event array, sorts descending, applies limit

auth.ts (NIP-42)

Manages AUTH challenge/response per connection.

MemberRole
#validatorCustom validator function
#challengesConnection → challenge string mapping
#authenticatedSet of authenticated connections
sendChallenge(ws)Generates a random challenge and sends AUTH message
handleAuthResponse(ws, event, url)Validates kind:22242 AUTH response

WebSocket Interception Mechanism


Message Flow

Client → Relay (send)

Error Simulation Decision Flow

Relay → Client (receive)


Data Flow

Event Store

Event Kind Classification Flow

REQ Processing and Subscription Management

Subscription Data Structure

#subscriptions: Map<MockWebSocket, Map<string, NostrFilter[]>>

  ConnectionA ──→ { "sub1": [filter1, filter2],
                    "sub2": [filter3] }
  ConnectionB ──→ { "sub1": [filter4] }

NIP Processing Flows

NIP-42 AUTH Flow

NIP-09 Deletion Processing Flow

NIP-11 Relay Information Flow

Event Injection and Real-time Stream


Test Lifecycle

typescript
// 1. Initialize
const pool = new MockPool();
const relay = pool.relay("wss://relay.example.com");

// 2. Pre-load data and configuration
relay.store(event); // Pre-register events
relay.onREQ((subId, filters) =>
  // Custom handler
  customEvents
);

// 3. Replace WebSocket
pool.install();

try {
  // 4. Run test (call client code as-is)
  const ws = new WebSocket("wss://relay.example.com");
  // ...

  // 5. Assertions
  relay.hasEvent("abc123"); // Verify event was received
  relay.countREQs(); // Verify REQ count
} finally {
  // 6. Always restore (prevents interference between tests)
  pool.uninstall();
}

Test Helper Overview


Notes

  • Test interference: Since replacing globalThis.WebSocket is a global operation, always call pool.uninstall() in the finally block of each test.
  • No signature verification: As a testing library, event signatures are treated as plain strings (actual cryptographic processing is not implemented to avoid adding dependencies). If you need signature verification, implement it yourself in an onEVENT handler.
  • Async delivery: Even with latency 0, responses are delivered asynchronously via queueMicrotask (returning responses synchronously inside send() would cause some clients to malfunction).
  • Single instance: Only one MockPool instance can be installed at a time.

MIT License