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 definitionsClass Relationship Diagram
MockPool (src/pool.ts)
A container that manages multiple MockRelay instances keyed by URL. The entry point for tests.
| Member | Type | Role |
|---|---|---|
#relays | Map<string, MockRelay> | URL → MockRelay mapping |
#originalWebSocket | typeof WebSocket | null | Stores original WebSocket for uninstall |
#originalFetch | typeof fetch | null | Stores original fetch for uninstall |
Key methods:
relay(url, options?)— Register/retrieve a MockRelay (returns existing instance for same URL)install()— ReplacesglobalThis.WebSocketandglobalThis.fetchuninstall()— Restores original implementationsreset()— 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.
| Field | Type | Role |
|---|---|---|
#store | NostrEvent[] | Event store (persistent events) |
#received | ReceivedMessage[] | Log of received messages |
#connections | Set<MockWebSocket> | List of active connections |
#subscriptions | Map<MockWebSocket, Map<string, NostrFilter[]>> | Subscriptions per connection |
#authState | AuthState | NIP-42 authentication state |
#pendingTimers | Set<ReturnType<typeof setTimeout>> | Pending timers (cleared on reset) |
MockWebSocket (src/websocket.ts)
The replacement for globalThis.WebSocket. Extends EventTarget to emulate the WebSocket API.
| Member | Role |
|---|---|
static _resolveRelay | URL → MockRelay resolver function set by MockPool |
#relay | The 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.
| Function | Description |
|---|---|
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.
| Member | Role |
|---|---|
#validator | Custom validator function |
#challenges | Connection → challenge string mapping |
#authenticated | Set 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.WebSocketis a global operation, always callpool.uninstall()in thefinallyblock 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
onEVENThandler. - Async delivery: Even with latency 0, responses are delivered asynchronously via
queueMicrotask(returning responses synchronously insidesend()would cause some clients to malfunction). - Single instance: Only one
MockPoolinstance can beinstalled at a time.