diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 88ec8f9..eb21a5d 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -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"}, ], }, { diff --git a/docs/net/adapter.md b/docs/net/adapter.md new file mode 100644 index 0000000..a9a73f5 --- /dev/null +++ b/docs/net/adapter.md @@ -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) + } + }, + }, +}) +``` diff --git a/docs/net/auth.md b/docs/net/auth.md new file mode 100644 index 0000000..22f8771 --- /dev/null +++ b/docs/net/auth.md @@ -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) +}) +``` diff --git a/docs/net/connection.md b/docs/net/connection.md deleted file mode 100644 index 8c0edee..0000000 --- a/docs/net/connection.md +++ /dev/null @@ -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. diff --git a/docs/net/context.md b/docs/net/context.md index edc571d..e2f408b 100644 --- a/docs/net/context.md +++ b/docs/net/context.md @@ -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. diff --git a/docs/net/diff.md b/docs/net/diff.md new file mode 100644 index 0000000..ab40533 --- /dev/null +++ b/docs/net/diff.md @@ -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 +}) +``` diff --git a/docs/net/executor.md b/docs/net/executor.md deleted file mode 100644 index bde6d5b..0000000 --- a/docs/net/executor.md +++ /dev/null @@ -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). diff --git a/docs/net/index.md b/docs/net/index.md index e38dccd..aa36aae 100644 --- a/docs/net/index.md +++ b/docs/net/index.md @@ -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 diff --git a/docs/net/message.md b/docs/net/message.md new file mode 100644 index 0000000..c8fbca4 --- /dev/null +++ b/docs/net/message.md @@ -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) + } +} +``` diff --git a/docs/net/policy.md b/docs/net/policy.md new file mode 100644 index 0000000..988e95b --- /dev/null +++ b/docs/net/policy.md @@ -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` - 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 +} +``` diff --git a/docs/net/pool.md b/docs/net/pool.md index 2500f45..a9b7abf 100644 --- a/docs/net/pool.md +++ b/docs/net/pool.md @@ -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. diff --git a/docs/net/publish.md b/docs/net/publish.md index bf15426..902b4ca 100644 --- a/docs/net/publish.md +++ b/docs/net/publish.md @@ -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() - - 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" } +``` \ No newline at end of file diff --git a/docs/net/request.md b/docs/net/request.md new file mode 100644 index 0000000..69d9cdb --- /dev/null +++ b/docs/net/request.md @@ -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"]}] +}) +``` diff --git a/docs/net/socket.md b/docs/net/socket.md index 9b9ea5c..9c25146 100644 --- a/docs/net/socket.md +++ b/docs/net/socket.md @@ -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 +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. diff --git a/docs/net/subscribe.md b/docs/net/subscribe.md deleted file mode 100644 index c821160..0000000 --- a/docs/net/subscribe.md +++ /dev/null @@ -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 diff --git a/docs/net/sync.md b/docs/net/sync.md deleted file mode 100644 index acae89c..0000000 --- a/docs/net/sync.md +++ /dev/null @@ -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 diff --git a/docs/net/targets.md b/docs/net/targets.md deleted file mode 100644 index 6185566..0000000 --- a/docs/net/targets.md +++ /dev/null @@ -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. diff --git a/docs/net/tracker.md b/docs/net/tracker.md index 8bc3e36..fe4c75a 100644 --- a/docs/net/tracker.md +++ b/docs/net/tracker.md @@ -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 - -// Get events from relay -const events = tracker.getIds(relayUrl) // Set - -// 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. diff --git a/packages/net/src/auth.ts b/packages/net/src/auth.ts index f69b470..6a74d05 100644 --- a/packages/net/src/auth.ts +++ b/packages/net/src/auth.ts @@ -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) { diff --git a/packages/util/src/Nip42.ts b/packages/util/src/Nip42.ts new file mode 100644 index 0000000..86949d0 --- /dev/null +++ b/packages/util/src/Nip42.ts @@ -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], + ], + }) diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 77143b0..e92c46b 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -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" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96147eb..62fc62f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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