Re-write net docs

This commit is contained in:
Jon Staab
2025-06-10 15:07:58 -07:00
parent 5b73c507b7
commit 1a81768e8e
22 changed files with 580 additions and 840 deletions
+8 -7
View File
@@ -57,15 +57,16 @@ export default defineConfig({
link: "/net/",
items: [
{text: "Context", link: "/net/context"},
{text: "Executor", link: "/net/executor"},
{text: "Subscribe", link: "/net/subscribe"},
{text: "Publish", link: "/net/publish"},
{text: "Sync", link: "/net/sync"},
{text: "Messages", link: "/net/message"},
{text: "Adapters", link: "/net/adapter"},
{text: "Sockets", link: "/net/socket"},
{text: "Pool", link: "/net/pool"},
{text: "Targets", link: "/net/targets"},
{text: "NIP 42 Auth", link: "/net/auth"},
{text: "Socket Policy", link: "/net/policy"},
{text: "Making Requests", link: "/net/request"},
{text: "Publishing Events", link: "/net/publish"},
{text: "Negentropy", link: "/net/diff"},
{text: "Tracker", link: "/net/tracker"},
{text: "Connection", link: "/net/connection"},
{text: "Socket", link: "/net/socket"},
],
},
{
+70
View File
@@ -0,0 +1,70 @@
# Adapter
Adapters provide a unified interface for communicating with relays. Adapters aren't meant to be used directly, but as an injection point for custom logic.
## Core Classes
### `AbstractAdapter`
Base class for all adapters. Handles events and cleanup.
- `send(message)` - Send message to relay
- `cleanup()` - Clean up resources
- Emits `AdapterEvent.Receive` when messages arrive
### Built-in Adapters
- `SocketAdapter(socket)` - WebSocket relay connections
- `LocalAdapter(relay)` - Local in-memory relays
- `MockAdapter(url, sendHandler)` - Testing with manual control
### Factory
`getAdapter(url, context?)` creates the appropriate adapter:
```typescript
const adapter = getAdapter('wss://relay.example.com')
adapter.on(AdapterEvent.Receive, (message, url) => {
console.log('Received:', message)
})
adapter.send(['REQ', 'sub1', {}])
adapter.cleanup()
```
## Custom Adapter Example
Custom adapters can be created against any target:
```typescript
class IPFSAdapter extends AbstractAdapter {
constructor(private url string) {
super()
// Set up an IPFS connection here
}
get urls() { return [this.url] }
get sockets() { return [] }
send(message: ClientMessage) {
// Handle messages as if the ipfs backend was a relay
}
}
```
Custom adapters can also be provided to several net utilities, including `publish` and `request`:
```typescript
request({
relays: ['ipfs://QmTy8w65yBXgyfG2ZBg5TrfB2hPjrDQH3RCQFJGkARStJb'],
filters: [{kinds: [1]}],
context: {
getAdapter: (url: string) => {
// getAdapter optionally returns an adapter. If none is returned, the stock adapters will be used.
if (url.startsWith('ipfs://')) {
return new IPFSAdapter(url)
}
},
},
})
```
+42
View File
@@ -0,0 +1,42 @@
# Auth
Handles NIP-42 relay authentication flow.
## Core Classes
### `AuthState`
Manages authentication state for a socket connection.
**Status Values:**
- `AuthStatus.None` - No authentication required/attempted
- `AuthStatus.Requested` - Relay requested authentication
- `AuthStatus.PendingSignature` - Waiting for user to sign auth event
- `AuthStatus.DeniedSignature` - User denied signing
- `AuthStatus.PendingResponse` - Waiting for relay response
- `AuthStatus.Forbidden` - Authentication failed
- `AuthStatus.Ok` - Authentication successful
**Methods:**
- `doAuth(sign)` - Authenticate with the relay using provided signing function
- `attemptAuth(sign)` - Attempt authentication with timeout handling
- `cleanup()` - Clean up event listeners
**Events:**
- `AuthStateEvent.Status` - Emitted when authentication status changes
## Example
```typescript
const authState = new AuthState(socket)
// Listen for auth status changes
authState.on(AuthStateEvent.Status, (status) => {
console.log('Auth status:', status)
})
// Attempt authentication when relay requests it
await authState.attemptAuth(async (template) => {
return await signer.signEvent(template)
})
```
-78
View File
@@ -1,78 +0,0 @@
# Connection
The `Connection` class is the core building block for relay communication in `@welshman/net`. It manages the complete lifecycle of a relay connection, including socket handling, message queuing, authentication, and statistics tracking.
## Overview
A Connection handles:
- WebSocket lifecycle
- Message queuing and throttling
- Connection state tracking
- Relay authentication
- Connection statistics
## Basic Usage
```typescript
import {Connection} from '@welshman/net'
// Create connection
const connection = new Connection("wss://relay.example.com")
// Listen for events
connection.on('event', (conn, subId, event) => {
console.log(`Got event from ${conn.url}`)
})
// Send a subscription
connection.send(["REQ", "my-sub", {kinds: [1], limit: 10}])
// Clean up when done
connection.cleanup()
```
## Handling Authentication
The `connection.open()` promise resolves when the WebSocket connection is fully established and ready for communication.
However, it's important to understand the authentication flow:
```typescript
import {Connection} from '@welshman/net'
const connection = new Connection("wss://relay.example.com")
// Basic open
await connection.open()
// Promise resolves when WebSocket is connected
// BUT might not be auth-ready yet!
// Complete open with auth handling
const openRelay = async (url: string) => {
const connection = new Connection(url)
// Open socket
await connection.open()
// Check if relay requires auth
if (connection.auth.status === 'requested') {
try {
// Handle auth challenge
await connection.auth.attempt(3000) // 3s timeout
} catch (e) {
console.error('Auth failed:', e)
return null
}
}
// NOW connection is fully ready
return connection
}
```
The key states after `open()` resolves:
- Socket is connected
- Messages can be queued
- BUT relay might request authentication
- AND authentication might fail
Always check `connection.auth.status` if you need to ensure the connection is fully authenticated before use.
+32 -52
View File
@@ -1,63 +1,43 @@
# Context
The Context system is the backbone of `@welshman/net`, providing global configuration and shared services that are essential for the package's operation. It defines how events are handled, validated, and routed throughout the application.
Provides global configuration and dependencies for the net package.
## Overview
## NetContext
- Global connection pool (`ctx.net.pool`)
- Event validation (`ctx.net.isValid`)
- Event handling (`ctx.net.onEvent`)
- Deletion tracking (`ctx.net.isDeleted`)
- Event signing (`ctx.net.signEvent`)
- Subscription optimization (`ctx.net.optimizeSubscriptions`)
Configuration object that defines how the net package operates:
## Basic Usage
- `pool: Pool` - Socket connection pool
- `repository: Repository` - Event storage and retrieval
- `isEventValid: (event, url) => boolean` - Event validation function
- `isEventDeleted: (event, url) => boolean` - Event deletion check
- `getAdapter?: (url, context) => AbstractAdapter` - Custom adapter factory
## Default Context
The `netContext` global provides sensible defaults:
- Uses singleton pool and repository instances
- Validates events using `verifyEvent`
- Checks deletions via repository
- No custom adapter factory
## Example
```typescript
import {ctx, setContext} from '@welshman/lib'
import {
getDefaultNetContext,
Pool,
hasValidSignature
} from '@welshman/net'
import {netContext} from '@welshman/net'
// Setup networking context
setContext({
net: getDefaultNetContext({
// Use shared pool
pool: new Pool(),
// Override event validation
netContext.isEventValid: (event, url) => {
return event.kind < 30000 && verifyEvent(event)
}
// Track events
onEvent: (url, event) => {
tracker.track(event.id, url)
repository.publish(event)
},
// Validate based on source
isValid: (url, event) => {
// Trust local relay
if (url === LOCAL_RELAY_URL) return true
// Validate signature for remote events
return hasValidSignature(event)
},
// Check deletion status
isDeleted: (url, event) =>
repository.isDeleted(event),
// Sign with current user
signEvent: async (event) =>
signer.get().sign(event)
})
// Use in requests
request({
filters: [{kinds: [1]}],
relays: ['wss://relay.example.com'],
context: {
...netContext,
// Request-specific overrides
}
})
// Now all package features will use these settings
subscribe(/*...*/) // Uses pool, validates events
publish(/*...*/) // Uses signEvent
```
The Context is used internally by most features in the package.
Without proper context configuration, core features like subscription, publishing, and event validation won't work correctly.
Think of it as the central configuration that defines how your nostr networking behaves.
+53
View File
@@ -0,0 +1,53 @@
# Diff
Efficient event synchronization using NIP-77 Negentropy protocol.
## Core Classes
### `Difference`
Handles negentropy synchronization with a single relay for a single filter.
**Events:**
- `DifferenceEvent.Message` - Emitted with `{have, need}` arrays and relay URL
- `DifferenceEvent.Error` - Emitted when sync fails
- `DifferenceEvent.Close` - Emitted when sync completes
**Methods:**
- `close()` - Stop synchronization and cleanup
## Functions
### `diff(options)`
Check what events each relay has or needs compared to local events.
**Returns:** Array of `{relay, have, need}` objects - `have` are events the relay has that you don't, `need` are events you have that the relay doesn't.
### `pull(options)`
Fetch missing events after comparing with relays.
**Returns:** Array of retrieved events.
### `push(options)`
Publish local events that relays are missing.
## Example
```typescript
// Check what events each relay has/needs
const results = await diff({
relays: ['wss://relay1.com', 'wss://relay2.com'],
filters: [{kinds: [1], authors: ['pubkey...']}],
events: localEvents
})
// Pull events that relays have but we don't
const newEvents = await pull({
relays: ['wss://relay1.com'],
filters: [{kinds: [1]}],
events: localEvents
})
```
-56
View File
@@ -1,56 +0,0 @@
# Executor
The Executor class orchestrates event delivery and subscription management across one or more [targets](/net/targets.md). It abstracts the complexity of handling multiple connections into a single interface.
## Overview
The Executor:
- Manages subscriptions
- Handles event publishing
- Supports NIP-77 (negentropy)
- Routes messages to appropriate targets
## Basic Usage
```typescript
import {Executor, Relays} from '@welshman/net'
// Create executor with relay target
const executor = new Executor(
new Relays([
connection1,
connection2
])
)
// Subscribe to events
const sub = executor.subscribe(
[{kinds: [1], limit: 10}],
{
onEvent: (url, event) => {
console.log(`Got event from ${url}`, event)
},
onEose: (url) => {
console.log(`EOSE from ${url}`)
}
}
)
// Publish event
const pub = executor.publish(
signedEvent,
{
onOk: (url, id, success, message) => {
console.log(`Published to ${url}: ${success ? 'OK' : message}`)
}
}
)
// Clean up
sub.unsubscribe()
executor.target.cleanup()
```
The Executor is used internally by higher-level APIs but can be used directly when you need fine-grained control over event routing and subscription management.
It's particularly useful when implementing custom targets or handling special relay configurations (like local relays or relay groups).
+2 -67
View File
@@ -11,71 +11,6 @@ Core networking layer for nostr applications, handling relay connections, messag
- **Publishing Tools** - Event broadcasting with status tracking
- **Sync Utilities** - NIP-77 (negentropy) event synchronization
- **Connection Pool** - Shared relay connection management
- **Targets** - Flexible message routing strategies
- **Custom Adapters** - Flexible adapter layer to support queries against any target
- **Event Tracking** - Monitor which relays have seen events
## Quick Example
```typescript
import {ctx, setContext} from '@welshman/lib'
import {type TrustedEvent, createEvent, NOTE} from '@welshman/util'
import {subscribe, publish, getDefaultNetContext} from '@welshman/net'
// Sets up customizable event valdation, handlers, etc
setContext(getDefaultNetContext())
// Send a subscription
const sub = subscribe({
relays: ['wss://relay.example.com/'],
filters: [{kinds: [1], limit: 1}],
closeOnEose: true,
timeout: 10000,
})
sub.on(SubscriptionEvent.Event, (url: string, event: TrustedEvent) => {
console.log(url, event)
sub.close()
})
// Publish an event
const pub = publish({
relays: ['wss://relay.example.com/'],
event: createEvent(NOTE, {content: 'hi'}),
})
pub.emitter.on('*', (status: PublishStatus, url: string) => {
console.log(status, url)
})
// The Tracker class can tell you which relays an event was read from or published to
console.log(ctx.net.tracker.getRelays(event.id))
```
The main reason this module exists is to support different backends via Executor and different `target` classes. For example, to add a local relay that automatically gets used:
```typescript
import {setContext} from '@welshman/lib'
import {LOCAL_RELAY_URL, Relay, Repository} from '@welshman/util'
import {getDefaultNetContext, Multi, Local, Relays, Executor} from '@welshman/net'
const repository = new Repository()
const relay = new Relay(repository)
setContext(getDefaultNetContext({
getExecutor: (relays: string[]) => {
return new Executor(
new Multi([
new Local(relay),
new Relays(remoteUrls.map(url => ctx.net.pool.get(url))),
])
)
},
}))
```
## Installation
```bash
npm install @welshman/net
```
- **Socket policies** - Default and customizable policies for NIP 42 AUTH, reconnects, etc
+52
View File
@@ -0,0 +1,52 @@
# Message
Type definitions and utilities for Nostr protocol messages.
## Relay Message Types
**Enums:**
- `RelayMessageType.Auth` - Authentication challenge
- `RelayMessageType.Closed` - Subscription closed
- `RelayMessageType.Eose` - End of stored events
- `RelayMessageType.Event` - Event data
- `RelayMessageType.NegErr` - Negentropy error
- `RelayMessageType.NegMsg` - Negentropy message
- `RelayMessageType.Ok` - Command result
**Type Definitions:**
- `RelayMessage` - Base relay message type
- `RelayAuth`, `RelayClosed`, `RelayEose`, `RelayEvent`, `RelayNegErr`, `RelayNegMsg`, `RelayOk` - Specific message types
- `RelayAuthPayload`, `RelayClosedPayload`, etc. - Payload types for each message
**Type Guards:**
- `isRelayAuth()`, `isRelayClosed()`, `isRelayEose()`, `isRelayEvent()`, `isRelayNegErr()`, `isRelayNegMsg()`, `isRelayOk()`
## Client Message Types
**Enums:**
- `ClientMessageType.Auth` - Authentication response
- `ClientMessageType.Close` - Close subscription
- `ClientMessageType.Event` - Publish event
- `ClientMessageType.NegClose` - Close negentropy
- `ClientMessageType.NegOpen` - Open negentropy
- `ClientMessageType.Req` - Request subscription
**Type Definitions:**
- `ClientMessage` - Base client message type
- `ClientAuth`, `ClientClose`, `ClientEvent`, `ClientNegClose`, `ClientNegOpen`, `ClientReq` - Specific message types
- `ClientAuthPayload`, `ClientClosePayload`, etc. - Payload types for each message
**Type Guards:**
- `isClientAuth()`, `isClientClose()`, `isClientEvent()`, `isClientNegClose()`, `isClientNegOpen()`, `isClientReq()`
## Example
```typescript
// Handle incoming relay message
const handleMessage = (message: RelayMessage) => {
if (isRelayEvent(message)) {
const [type, subId, event] = message
console.log('Event:', event.id)
}
}
```
+63
View File
@@ -0,0 +1,63 @@
# Policy
Socket policies provide automated behavior for socket connections. They are intended to be applied on socket creation via `makeSocket` or `PoolOptions.makeSocket`.
## Built-in Policies
### `socketPolicyAuthBuffer`
Buffers messages during authentication flow and replays them after successful auth.
### `socketPolicyConnectOnSend`
Auto-connects closed sockets when messages are sent (with error cooldown).
### `socketPolicyCloseOnTimeout`
Closes sockets after 30 seconds of inactivity.
### `socketPolicyReopenActive`
Reopens sockets that have pending requests or publishes.
## Custom Auth Policy
### `makeSocketPolicyAuth(options)`
Creates a policy that handles authentication challenges.
**Options:**
- `sign: (event) => Promise<SignedEvent>` - Signing function
- `shouldAuth?: (socket) => boolean` - Optional auth condition
## Default Policies
`defaultSocketPolicies` includes all built-in policies except auth (which requires configuration).
It's common to include the following code in order to enable automatic authentication on all connections:
```typescript
defaultSocketPolicies.push(
makeSocketPolicyAuth({
sign: (event: StampedEvent) => signer.sign(event),
shouldAuth: (socket: Socket) => true,
}),
)
```
## Example
It's possible to create custom policies simply by defining a function which returns a cleanup function:
```typescript
import {on} from "@welshman/lib"
import {SocketEvent, Socket, SocketStatus} from "@welshman/net"
const logStatusChangePolicy = (socket: Socket) => {
const unsubscribe = on(socket, SocketEvent.Status, (newStatus: SocketStatus) => {
console.log(socket.url, newStatus)
})
return unsubscribe
}
```
+30 -24
View File
@@ -1,37 +1,43 @@
# Pool
The Pool class manages a collection of relay connections, providing a centralized way to track and reuse connections across your application.
The Pool class manages a collection of websocket connections to relay servers, providing connection pooling and lifecycle management.
## Overview
## Classes
- Creates and caches connections
- Ensures single connection per relay
- Handles cleanup of unused connections
- Provides connection lookup
### Pool
## Usage
A connection pool that creates and manages Socket instances for different relay URLs.
**Methods:**
- `static get()` - Returns the singleton pool instance
- `has(url)` - Checks if a socket exists for the given URL
- `get(url)` - Gets or creates a socket for the given URL
- `subscribe(callback)` - Subscribes to new socket creation events
- `remove(url)` - Removes and cleans up a socket
- `clear()` - Removes all sockets from the pool
## Functions
### makeSocket(url, policies)
Creates a new Socket instance with the given URL and applies default policies.
## Example
```typescript
import {Pool} from '@welshman/net'
import {Pool} from "@welshman/net"
// Create pool
const pool = new Pool()
// Get the singleton - Pool can also be instantiated directly if you want multiple pools
const pool = Pool.get()
// Get or create connection
const connection = pool.get("wss://relay.example.com")
// Get a socket for a relay
const socket = pool.get("wss://relay.example.com")
// Check if relay is in pool
if (pool.has("wss://relay.example.com")) {
// Use existing connection
}
// Subscribe to new socket creation
const unsubscribe = pool.subscribe((socket) => {
console.log("New socket created:", socket.url)
})
// Remove connection
// Clean up
pool.remove("wss://relay.example.com")
// Clear all connections
pool.clear()
```
The Pool is typically used internally by the router and executor, but can be used directly for custom connection management.
It ensures efficient connection reuse across your application.
+42 -74
View File
@@ -1,85 +1,53 @@
# Publish
The `Publish` class handles event publishing to relays, managing publish status, relay responses, and error handling.
Utilities for publishing events to Nostr relays with status tracking and callback handling.
## Overview
## Enums
- Sends events to relays
- Tracks publish status per relay
- Handles OK/Error responses
- Manages timeouts
### PublishStatus
## Basic Usage
Status values for publish operations:
- `Sending` - Event is being sent
- `Pending` - Waiting for relay response
- `Success` - Event published successfully
- `Failure` - Event rejected by relay
- `Timeout` - Request timed out
- `Aborted` - Request was aborted
## Functions
### publishOne(options)
Publishes an event to a single relay and returns a promise that resolves with the publish status.
**Options:**
- `event` - The signed event to publish
- `relay` - Relay URL
- `signal?` - AbortSignal for cancellation
- `timeout?` - Timeout in milliseconds (default: 10000)
- `context?` - Adapter context
- Callback functions: `onSuccess`, `onFailure`, `onPending`, `onTimeout`, `onAborted`, `onComplete`
### publish(options)
Publishes an event to multiple relays in parallel and returns a status object mapping relay URLs to their publish status.
## Example
```typescript
import {`Publish`, `Publish`Status} from '@welshman/net'
import {publish, PublishStatus} from "@welshman/net"
const {Pending, Success, Failure, Timeout, Aborted} = `Publish`Status
// Basic `Publish`
const pub = `Publish`({
event: signedEvent,
relays: ["wss://relay.example.com"],
timeout: 3000 // 3s timeout
})
// Track status
pub.emitter.on('*', (status: `Publish`Status, url: string, message?: string) => {
switch (status) {
case Success:
console.log(``Publish`ed to ${url}`)
break
case Failure:
console.log(`Failed on ${url}: ${message}`)
break
case Timeout:
console.log(`Timeout on ${url}`)
break
}
})
```
## Real World Example
```typescript
const publishWithStatus = async (event: SignedEvent) => {
const pub = `Publish`({
event,
relays: ctx.app.router
.FromUser()
.getUrls(),
timeout: 5000
})
// Track per-relay status
const status = new Map<string, string>()
pub.emitter.on('*', (state: `Publish`Status, url: string) => {
status.set(url, state)
// Log progress
const counts = {
pending: 0,
success: 0,
failed: 0
}
for (const s of status.values()) {
counts[s] = (counts[s] || 0) + 1
}
console.log(
`Progress: ${counts.success}/${status.size}`,
`(${counts.failed} failed)`
)
})
// Wait for completion
return pub.result
const event = {
// ... signed event
}
```
Like [Subscribe](/net/subscribe.md), `Publish` uses [Pool](/net/pool.md) for connections and creates appropriate [Targets](/net/targets.md) via an [Executor](/net/executor.md), but focuses on event publishing rather than subscription management.
const statusByRelay = await publish({
event,
relays: ["wss://relay1.com", "wss://relay2.com"],
timeout: 5000,
onSuccess: (detail, relay) => console.log(`Published to ${relay}`),
onFailure: (detail, relay) => console.log(`Failed on ${relay}: ${detail}`)
})
Note: The base `@welshman/net` Publish class just handles network publishing.
For optimistic updates and repository integration, use Publish from `@welshman/app`.
console.log(statusByRelay) // { "wss://relay1.com": "success", "wss://relay2.com": "failure" }
```
+63
View File
@@ -0,0 +1,63 @@
# Request
Utilities for requesting events from Nostr relays with filtering, deduplication, and batching capabilities.
## Functions
### requestOne(options)
Requests events from a single relay using the given filters. Returns a promise that resolves with deduplicated events.
**Options:**
- `relay` - Relay URL
- `filters` - Array of Nostr filters
- `signal?` - AbortSignal for cancellation
- `tracker?` - Event tracker for deduplication
- `context?` - Adapter context
- `autoClose?` - Auto-close subscription after EOSE
- Validation functions: `isEventValid`, `isEventDeleted`
- Callback functions: `onEvent`, `onDeleted`, `onInvalid`, `onFiltered`, `onDuplicate`, `onDisconnect`, `onEose`, `onClose`
### request(options)
Requests events from multiple relays in parallel. Returns a promise that resolves with all events from all relays.
**Options:**
- `relays` - Array of relay URLs
- `filters` - Array of Nostr filters
- `threshold?` - Fraction of relays that must close before completing (default: 1)
- All other options from `requestOne`
### makeLoader(options)
Creates a batched loader function that delays and combines requests for efficiency.
**Options:**
- `delay` - Batch delay in milliseconds
- `timeout?` - Request timeout
- `threshold?` - Relay completion threshold
- Validation functions and context options
### load(filters, relays, options)
Pre-configured loader with 200ms delay, 3s timeout, and 0.5 threshold.
## Example
```typescript
import {request, load} from "@welshman/net"
// Simple request - without autoClose or a signal this will continue to stream indefinitely.
// Default policies (see policy.ts) will also re-send the subscription when sockets reconnect
const events = await request({
relays: ["wss://relay1.com", "wss://relay2.com"],
filters: [{kinds: [1], limit: 10}],
onEvent: (event, url) => console.log(`Event from ${url}:`, event.id)
})
// Batched loading (more efficient for multiple requests)
const profileEvents = await load({
relays: ["wss://relay1.com"],
filters: [{kinds: [0], authors: ["pubkey1", "pubkey2"]}]
})
```
+55 -62
View File
@@ -1,69 +1,62 @@
# Socket
The Socket class is exclusively used by the `Connection` class as its low-level WebSocket manager. It's not meant to be used directly by other classes.
Its sole purpose is to provide a reliable, manageable WebSocket connection with nostr-specific handling.
## Core Responsibilities
WebSocket wrapper for Nostr relay connections with status tracking, queuing, and authentication support. Not intended to be used directly, instead access sockets through the `Pool` interface.
## Enums
### SocketStatus
Connection status values:
- `Open` - Socket is connected and ready
- `Opening` - Socket is connecting
- `Closing` - Socket is closing
- `Closed` - Socket is closed
- `Error` - Socket encountered an error
### SocketEvent
Event types emitted by the socket:
- `Error` - Socket error occurred
- `Status` - Status changed
- `Send` - Message sent to relay
- `Sending` - Message queued for sending
- `Receive` - Message received from relay
- `Receiving` - Message queued for processing
## Classes
### Socket
WebSocket connection to a Nostr relay with queuing and authentication.
**Properties:**
- `url` - Relay URL
- `status` - Current socket status
- `auth` - Authentication state
**Methods:**
- `open()` - Opens the WebSocket connection
- `attemptToOpen()` - Opens connection if not already open
- `close()` - Closes the connection
- `cleanup()` - Closes connection and removes all listeners
- `send(message)` - Queues a message to send
## Example
```typescript
export class Socket {
// Track connection state
status: SocketStatus = "new" | "open" | "opening" | "closing" | "closed" | "error"
import {Socket, SocketEvent, SocketStatus} from "@welshman/net"
// Handle nostr message queue
worker: Worker<Message>
const socket = new Socket("wss://relay.example.com")
// Core operations
open = async () => {/* Initialize WebSocket */}
close = async () => {/* Clean shutdown */}
send = async (message: Message) => {/* Send with JSON serialization */}
}
socket.on(SocketEvent.Status, (status, url) => {
console.log(`Socket ${url} status: ${status}`)
})
socket.on(SocketEvent.Receive, (message, url) => {
console.log("Received:", message)
})
socket.open()
socket.send(["REQ", "sub-id", {kinds: [1], limit: 10}])
socket.cleanup()
```
Key features:
- State tracking
- Message queuing
- JSON serialization
- Error recovery
- Connection lifecycle
Think of it as a thin wrapper that turns raw WebSocket connections into something more suitable for nostr:
```typescript
// Raw WebSocket
ws.send(JSON.stringify(["REQ", "sub1", {kinds: [1]}]))
// With Socket
socket.send(["REQ", "sub1", {kinds: [1]}]) // Handles serialization
```
## Usage Chain
```typescript
// Hierarchy
Socket // WebSocket management
Connection // Uses Socket
Relay Target // Uses Connection
Executor // Uses Target
Subscribe // Uses Executor
Publish // Uses Executor
// In Connection.ts
export class Connection extends Emitter {
socket: Socket
constructor(url: string) {
this.socket = new Socket(this)
}
}
```
It's an internal implementation detail that you shouldn't need to use directly - always interact with the `Connection` class instead, which provides a higher-level interface.
```typescript
// DON'T use Socket directly
const socket = new Socket(/*...*/) // ❌
// DO use Connection
const connection = new Connection(url) // ✅
```
This encapsulation ensures consistent connection management across the library.
-115
View File
@@ -1,115 +0,0 @@
# Subscribe
The Subscribe class manages nostr subscriptions, handling subscription lifecycle, event filtering, and relay responses. It provides a unified interface for subscribing to events across multiple relays.
## Overview
The Subscription:
- Manages REQ/CLOSE lifecycle
- Handles EOSE responses
- Emits filtered events
- Tracks completion state
```typescript
import {subscribe, SubscriptionEvent} from '@welshman/net'
// Create subscription
const sub = subscribe({
filters: [{kinds: [1], limit: 10}],
relays: ["wss://relay.example.com"],
// Optional configurations
closeOnEose: true, // Close after all relays send EOSE
timeout: 3000, // Max time to wait
authTimeout: 300, // Time for auth negotiation
delay: 50 // Delay between batched requests
})
// Handle events
sub.on(SubscriptionEvent.Event, (url, event) => {
console.log(`Got event from ${url}:`, event)
})
sub.on(SubscriptionEvent.Eose, (url) => {
console.log(`Got EOSE from ${url}`)
})
sub.on(SubscriptionEvent.Complete, () => {
console.log('Subscription complete')
})
// Close when done
sub.close()
```
## Architecture
```typescript
import {subscribe, Pool, Executor, Relays} from '@welshman/net'
// Under the hood, subscribe:
// 1. Gets connections from global pool
// 2. Creates a target (usually Relays)
// 3. Uses Executor to manage subscription
// This is roughly equivalent to:
const manualSubscribe = (urls: string[]) => {
// Get connections from pool
const connections = urls.map(url =>
ctx.net.pool.get(url)
)
// Create target
const target = new Relays(connections)
// Create executor
const executor = new Executor(target)
// Subscribe via executor
return executor.subscribe(
[{kinds: [1], limit: 10}],
{
onEvent: (url, event) => {
console.log(`Got event from ${url}`)
}
}
)
}
```
## Real World Example
```typescript
// Combine local and remote relays
const loadProfile = async (pubkey: string) => {
// Get optimal relays
const relays = ctx.app.router
.ForPubkey(pubkey)
.getUrls()
const sub = subscribe({
filters: [{
kinds: [0],
authors: [pubkey],
limit: 1
}],
relays,
// This creates internally:
// 1. Connections via Pool
// 2. Multi target with Local + Relays
// 3. Executor to manage subscription
})
return new Promise(resolve => {
sub.on('event', (url, event) => {
resolve(event)
sub.close()
})
})
}
```
The Subscribe class abstracts away:
- Connection management (via Pool)
- Target creation and setup
- Executor orchestration
-70
View File
@@ -1,70 +0,0 @@
# Sync
The Sync utilities in `@welshman/net` provide methods for synchronizing events between relays and repositories, primarily using NIP-77 (Negentropy) when available, with fallback to traditional sync methods.
## Overview
```typescript
import {sync, pull, push} from '@welshman/net'
// Three main operations:
// 1. pull: Get events from relays
// 2. push: Send events to relays
// 3. sync: Bidirectional sync
```
These utilities are primarily used by:
- `Repository` for syncing with relays
- `FeedController` for initial feed loading
## Basic Usage
```typescript
import {sync, pull, getFilterSelections} from '@welshman/net'
// Sync user profile data
const syncProfiles = async (pubkeys: string[]) => {
await sync({
// What to sync
filters: [{
kinds: [0],
authors: pubkeys
}],
// Which relays
relays: ctx.app.router
.ForPubkeys(pubkeys)
.getUrls(),
// Local events to consider
events: repository.query([{
kinds: [0],
authors: pubkeys
}])
})
}
// Initial feed load with negentropy
const loadFeed = async () => {
await pull({
filters: [{
kinds: [1],
limit: 100
}],
relays: ctx.app.router
.ForUser()
.getUrls(),
events: [], // No local events yet
onEvent: (event) => {
// Handle new events
}
})
}
```
Sync operations:
- Use NIP-77 when supported by relay
- Fall back to traditional sync
- Handle bidirectional sync
- Support filtered sync
- Track sync progress
-137
View File
@@ -1,137 +0,0 @@
# Targets
The targets system provides different strategies for message routing.
Each target type implements a common interface for handling nostr messages but with different routing behaviors.
## Overview
Targets are used by the [Executor](/net/executor.md) class to:
- Route messages to connections
- Handle responses
- Manage connection lifecycles
- Combine multiple routing strategies
## Available Targets
### Echo Target
Simple target that echoes messages back. Useful for testing.
```typescript
import {Echo} from '@welshman/net'
const echo = new Echo()
echo.on('EVENT', (url, event) => {
console.log('Echo received:', event)
})
```
### Local Target
Connects to an in-memory relay implementation.
```typescript
import {Local} from '@welshman/net'
import {Repository, Relay} from '@welshman/util'
// Create local relay
const repository = new Repository()
const relay = new Relay(repository)
const local = new Local(relay)
// Use like any other target
local.send(['REQ', 'sub1', {kinds: [1]}])
```
### Relay Target
Single relay connection target.
```typescript
import {Relay} from '@welshman/net'
const target = new Relay(connection)
target.on('EVENT', (url, event) => {
console.log(`Event from ${url}:`, event)
})
```
### Relays Target
Manages multiple relay connections.
```typescript
import {Relays} from '@welshman/net'
const target = new Relays([
connection1,
connection2,
connection3
])
```
### Multi Target
Combines multiple targets into one.
```typescript
import {Multi, Local, Relays} from '@welshman/net'
// Create multi-target with local and remote relays
const target = new Multi([
new Local(localRelay),
new Relays(remoteConnections)
])
```
## Real World Example
Here's how Coracle might set up its relay infrastructure:
```typescript
import {
Executor,
Multi,
Local,
Relays
} from '@welshman/net'
import {Repository, Relay} from '@welshman/util'
// Setup
const setupRelayInfrastructure = () => {
// Create local repository & relay
const repository = new Repository()
const localRelay = new Relay(repository)
// Get remote connections from pool
const remoteConnections = [
pool.get("wss://relay1.example.com"),
pool.get("wss://relay2.example.com")
]
// Create multi-target executor
const executor = new Executor(
new Multi([
// Local relay for immediate responses
new Local(localRelay),
// Remote relays for network queries
new Relays(remoteConnections)
])
)
// Subscribe using combined target
const sub = executor.subscribe(
[{kinds: [1], limit: 10}],
{
onEvent: (url, event) => {
if (url === LOCAL_RELAY_URL) {
console.log('Got from cache:', event)
} else {
console.log('Got from network:', url, event)
}
}
}
)
return {executor, sub}
}
```
The target system allows for flexible relay configurations while maintaining a consistent interface for the rest of the application. This is particularly useful for:
- Caching with local relays
- Load balancing across relays
- Fallback strategies
- Testing and simulation
Each target type serves a specific purpose but can be combined using `Multi` for complex routing scenarios.
+55 -57
View File
@@ -1,72 +1,70 @@
# Tracker
The Tracker is a simple but crucial class that keeps track of which relays an event was seen on or published to. It's essential for relay selection and event source tracking.
Event tracker for managing which events have been seen from which relays, used for deduplication across multiple relay connections.
## Overview
## Classes
### Tracker
Tracks the relationship between event IDs and relay URLs to prevent duplicate processing.
**Properties:**
- `relaysById` - Map of event IDs to sets of relay URLs
- `idsByRelay` - Map of relay URLs to sets of event IDs
**Methods:**
- `getIds(relay)` - Gets all event IDs seen from a relay
- `getRelays(eventId)` - Gets all relays that have sent an event
- `hasRelay(eventId, relay)` - Checks if an event was seen from a relay
- `addRelay(eventId, relay)` - Records that an event was seen from a relay
- `removeRelay(eventId, relay)` - Removes the event-relay association
- `track(eventId, relay)` - Tracks an event and returns true if already seen
- `copy(eventId1, eventId2)` - Copies relay associations from one event to another
- `load(relaysById)` - Loads tracker state from a map
- `clear()` - Clears all tracked data
**Events:**
- `add` - Emitted when event-relay association is added
- `remove` - Emitted when event-relay association is removed
- `load` - Emitted when tracker state is loaded
- `clear` - Emitted when tracker is cleared
## Example
```typescript
import {Tracker} from '@welshman/net'
import {Tracker} from "@welshman/net"
const tracker = new Tracker()
// Track event source
tracker.track(eventId, relayUrl)
// Track events from different relays
const isDuplicate1 = tracker.track("event123", "wss://relay1.com") // false
const isDuplicate2 = tracker.track("event123", "wss://relay2.com") // false
const isDuplicate3 = tracker.track("event123", "wss://relay1.com") // true (duplicate)
// Get relays for event
const relays = tracker.getRelays(eventId) // Set<string>
// Get events from relay
const events = tracker.getIds(relayUrl) // Set<string>
// Check specific relay
const seen = tracker.hasRelay(eventId, relayUrl)
// Check which relays have sent an event
const relays = tracker.getRelays("event123") // Set(["wss://relay1.com", "wss://relay2.com"])
```
## Used By
If you're not using `@welshman/app`, you might want to track relays for all events that come through:
1. **Repository & Sync**
```typescript
// In sync operations
pull({
events,
relays,
onEvent: (event) => {
tracker.track(event.id, relay)
}
import {Pool, Tracker, SocketEvent, isRelayEvent} from "@welshman/net"
import {isEphemeralKind, isDVMKind, verifyEvent} from "@welshman/util"
import {Repository} from "@welshman/relay"
const tracker = new Tracker()
const repository = new Repository()
Pool.get().subscribe(socket => {
socket.on(SocketEvent.Receive, message => {
if (isRelayEvent(message)) {
const event = message[2]
if (!isEphemeralKind(event.kind) && !isDVMKind(event.kind) && verifyEvent(event)) {
tracker.track(event.id, socket.url)
repository.publish(event)
}
}
})
})
```
2. **Subscribe**
```typescript
// In @welshman/app subscribe
sub.on('event', (url, event) => {
// Track where we got the event
tracker.track(event.id, url)
})
```
3. **Publish**
```typescript
// In publish operations
pub.emitter.on('success', (url) => {
// Track where we published
tracker.track(event.id, url)
})
```
4. **Router**
```typescript
// Used for relay selection
const relays = tracker
.getRelays(event.id)
.filter(url =>
isHealthyRelay(url)
)
```
The Tracker:
- Maps events to their source relays
- Maps relays to their known events
- Helps optimize relay selection
Think of it as a memory of where events came from, helping make better decisions about where to find or publish events.
+2 -10
View File
@@ -1,19 +1,11 @@
import EventEmitter from "events"
import {on, poll, call} from "@welshman/lib"
import {SignedEvent, StampedEvent} from "@welshman/util"
import {makeEvent, CLIENT_AUTH} from "@welshman/util"
import {makeRelayAuth} from "@welshman/util"
import {isRelayAuth, isClientAuth, isRelayOk, RelayMessage} from "./message.js"
import {Socket, SocketStatus, SocketEvent} from "./socket.js"
import {Unsubscriber} from "./util.js"
export const makeAuthEvent = (url: string, challenge: string) =>
makeEvent(CLIENT_AUTH, {
tags: [
["relay", url],
["challenge", challenge],
],
})
export enum AuthStatus {
None = "auth:status:none",
Requested = "auth:status:requested",
@@ -108,7 +100,7 @@ export class AuthState extends EventEmitter {
this.setStatus(AuthStatus.PendingSignature)
const template = makeAuthEvent(this.socket.url, this.challenge)
const template = makeRelayAuth(this.socket.url, this.challenge)
const event = await sign(template)
if (event) {
+10
View File
@@ -0,0 +1,10 @@
import {makeEvent} from "./Events.js"
import {CLIENT_AUTH} from "./Kinds.js"
export const makeRelayAuth = (url: string, challenge: string) =>
makeEvent(CLIENT_AUTH, {
tags: [
["relay", url],
["challenge", challenge],
],
})
+1
View File
@@ -7,6 +7,7 @@ export * from "./Handler.js"
export * from "./Kinds.js"
export * from "./Links.js"
export * from "./List.js"
export * from "./Nip42.js"
export * from "./Nip86.js"
export * from "./Nip98.js"
export * from "./Profile.js"
-31
View File
@@ -68,9 +68,6 @@ importers:
'@types/throttle-debounce':
specifier: ^5.0.2
version: 5.0.2
'@welshman/dvm':
specifier: workspace:*
version: link:../dvm
'@welshman/feeds':
specifier: workspace:*
version: link:../feeds
@@ -131,31 +128,6 @@ importers:
specifier: ~5.8.0
version: 5.8.2
packages/dvm:
dependencies:
'@noble/hashes':
specifier: ^1.6.1
version: 1.7.1
'@welshman/lib':
specifier: workspace:*
version: link:../lib
'@welshman/net':
specifier: workspace:*
version: link:../net
'@welshman/signer':
specifier: workspace:*
version: link:../signer
'@welshman/util':
specifier: workspace:*
version: link:../util
devDependencies:
rimraf:
specifier: ~6.0.0
version: 6.0.1
typescript:
specifier: ~5.8.0
version: 5.8.2
packages/editor:
dependencies:
'@tiptap/core':
@@ -222,9 +194,6 @@ importers:
packages/feeds:
dependencies:
'@welshman/dvm':
specifier: workspace:*
version: link:../dvm
'@welshman/lib':
specifier: workspace:*
version: link:../lib