description: "Use this skill when working with @welshman/net: relay connections, request/publish flows, auth, relay pool management, adapters, policies, or low-level nostr network I/O."
`@welshman/net` is the core networking layer for welshman-based nostr apps. It manages WebSocket relay connections, subscriptions, event publishing, NIP-42 auth, and NIP-77 negentropy sync. It sits below `@welshman/app` (which owns an `App` instance and its plugins) and depends on `@welshman/util` for event types and `@welshman/lib` for utilities.
| `socket.auth` | `AuthState` instance for NIP-42 on this connection |
### Request
| Export | Description |
|--------|-------------|
| `requestOne(options)` | Subscribe to a single relay; returns `Promise<TrustedEvent[]>` |
| `request(options)` | Subscribe to multiple relays in parallel; returns `Promise<TrustedEvent[]>` |
| `makeLoader(options)` | Creates a batching `load` function with configurable delay/timeout/threshold |
| `load(options)` | Pre-built loader: 200 ms batch delay, 3 s timeout, 0.5 threshold. Simpler than `request()` when you just want events — auto-closes after EOSE, timeout, or disconnect; resolves when half the relays' subscriptions have closed; returns a `Promise<TrustedEvent[]>`. When used with `@welshman/app`, received events auto-flow into the repository and tracker. |
`request` / `requestOne` options (key fields):
-`relay` / `relays` — relay URL(s)
-`filters` — array of nostr `Filter` objects
-`autoClose?: boolean` — close subscription after EOSE or on socket disconnect
-`signal?: AbortSignal` — cancellation
-`tracker?: Tracker` — cross-relay deduplication (shared automatically by `request`)
-`threshold?: number` — fraction of relays that must close before the promise resolves (default `1`)
Without `autoClose` or a `signal`, `requestOne` streams indefinitely — the returned promise only resolves if the relay sends CLOSED for all active subscription IDs. Default policies also re-send the REQ when sockets reconnect.
### Publish
| Export | Description |
|--------|-------------|
| `publish(options)` | Publishes to multiple relays; resolves to `PublishResultsByRelay` |
| `publishOne(options)` | Publishes to a single relay; resolves to `PublishResult` |
| `AuthStateEvent.Status` | Emitted when auth status changes |
| `makeSocketPolicyAuth(options)` | Creates a socket policy that auto-handles auth challenges |
| `defaultSocketPolicies` | Mutable array of policies applied to every new socket |
### Policies
| Export | Description |
|--------|-------------|
| `socketPolicyPing` | Sends a PING frame every 30 s when the socket is open and idle, to keep the connection alive |
| `socketPolicyAuthBuffer` | Buffers outgoing messages during auth and replays after success |
| `socketPolicyConnectOnSend` | Auto-opens closed sockets when a message is queued |
| `socketPolicyCloseInactive` | Closes idle sockets after 30 s (when no pending work remains); if the socket closes with pending work it delays and reopens, replaying queued messages |
| `defaultSocketPolicies` | Array of the four above; passed to every socket created by `Pool` |
A `SocketPolicy` is `(socket: Socket) => Unsubscriber`.
### Repository
| Export | Description |
|--------|-------------|
| `Repository` | In-memory indexed event store with delete/expiry support |
| `Repository.get()` | Returns the singleton instance |
| `repository.publish(event)` | Stores an event; returns `false` if duplicate/stale |
| `mergeRepositoryUpdates(updates)` | Merges an array of `RepositoryUpdate` objects into one |
Emits `"update"` with `RepositoryUpdate` (`{ added: TrustedEvent[], removed: Set<string> }`) on every change.
> **Prefer `LOCAL_RELAY_URL` over direct repository access.** Rather than calling `repository.query()` or `repository.publish()` directly, pass `LOCAL_RELAY_URL` as a relay URL to the standard `load()`, `request()`, and `publish()` functions. This keeps local reads/writes going through the same policy, deduplication, and tracking pipeline as remote relay operations. Direct repository access is appropriate only for bulk startup (`repository.load()`) and low-level introspection (`repository.getEvent()`, `repository.isDeleted()`).
### Tracker
| Export | Description |
|--------|-------------|
| `Tracker` | Bidirectional map of `eventId ↔ Set<relayUrl>` |
| `tracker.track(eventId, relay)` | Records relay; returns `true` if the event was already seen |
| `tracker.getRelays(eventId)` | Set of relay URLs that have sent this event |
| `tracker.getIds(relay)` | Set of event ids seen from a relay |
| `tracker.copy(id1, id2)` | Copies relay associations from one id to another (used for gift wraps) |
| `tracker.load(relaysById)` | Bulk-replaces all relay mappings from a `Map<string, Set<string>>`; emits `"load"` |
| `tracker.clear()` | Removes all relay mappings; emits `"clear"` |
| `RelayMessageType` | Enum of relay→client message types |
| `ClientMessageType` | Enum of client→relay message types |
| `isRelayEvent()`, `isRelayEose()`, `isRelayOk()`, `isRelayAuth()`, etc. | Type guards for relay messages |
| `isClientReq()`, `isClientEvent()`, etc. | Type guards for client messages |
### WrapManager
| Export | Description |
|--------|-------------|
| `WrapManager` | Tracks NIP-59 gift wrap → rumor relationships; stores decrypted rumors in the repository and copies relay tracking from the wrap to the rumor |
---
## Common Patterns
### Connect to a relay and stream events
```typescript
import {Pool, SocketEvent, SocketStatus} from '@welshman/net'
shouldAuth: (socket) => true, // auth on every relay
}),
)
```
### Custom socket policies
A `SocketPolicy` is `(socket: Socket) => Unsubscriber`. It receives the socket when it is created, attaches listeners or patches socket methods, and returns a cleanup function. Push custom policies onto `defaultSocketPolicies` before any sockets are opened.
```typescript
import {writable} from 'svelte/store'
import {on} from '@welshman/lib'
import {defaultSocketPolicies, SocketEvent, isRelayEvent} from '@welshman/net'
import type {Socket, RelayMessage} from '@welshman/net'
// Track how many events each relay has delivered this session
return unsub // called when the socket is destroyed
}
defaultSocketPolicies.push(eventCountPolicy)
```
The same structure applies to more advanced patterns — patch `socket.open` to block connections, listen to `SocketEvent.Sending`/`SocketEvent.Receiving` to intercept messages before they are processed, or manipulate `socket._recvQueue` directly to suppress or replay messages.
### Custom adapter (e.g. non-WebSocket backend)
```typescript
import {AbstractAdapter, AdapterEvent, request} from '@welshman/net'
import type {ClientMessage} from '@welshman/net'
class MyAdapter extends AbstractAdapter {
constructor(private url: string) {
super()
// set up your transport here
}
get urls() { return [this.url] }
get sockets() { return [] }
send(message: ClientMessage) {
// forward message to your backend; call this.emit(AdapterEvent.Receive, replyMsg, this.url) when data arrives
}
}
request({
relays: ['myscheme://some-id'],
filters: [{kinds: [1]}],
autoClose: true,
context: {
getAdapter: (url) => url.startsWith('myscheme://') ? new MyAdapter(url) : undefined,
},
})
```
### Use LOCAL_RELAY_URL to read/write the local repository
Pass `LOCAL_RELAY_URL` as a relay to the standard net functions so local operations go through the same pipeline as remote ones (policies, deduplication, tracker):
```typescript
import {load, publish, request, LOCAL_RELAY_URL} from '@welshman/net'
import {now} from '@welshman/lib'
// Read from the local repository the same way you'd read from a remote relay
onEvent: (event) => console.log('new local event', event.id),
})
```
Direct `repository` API calls (`repository.load()`, `repository.getEvent()`, `repository.isDeleted()`, `repository.dump()`) are still appropriate for bulk startup and low-level introspection — but for routine reads and writes prefer `LOCAL_RELAY_URL`.
- **`@welshman/util`** — provides `TrustedEvent`, `SignedEvent`, `Filter`, `verifyEvent`, `matchFilters`, `getAddress`, etc. All event objects flowing through `@welshman/net` are `TrustedEvent` (already verified).
- **`@welshman/lib`** — utility helpers (`Emitter`, `batcher`, `defer`, `on`, etc.) used internally; `Emitter` (from `@welshman/lib`) is the base class for `Tracker`, `Repository`, and `WrapManager`. `Socket`, `AuthState`, `AbstractAdapter`, and `Difference` extend node's built-in `EventEmitter` directly.
- **`@welshman/app`** — wraps `@welshman/net` in an `App` instance whose plugins own routing, collections, and publishing. Most app-level code should go through `app.use(Network)`; drop down to `@welshman/net` only for raw relay I/O or when building without an `App`.
- **`NetContext`** — passed explicitly per call. Each `App` owns its own pool and repository, which keeps one identity's data out of another's.
- **Use `LOCAL_RELAY_URL`, not direct repository calls, for routine reads/writes.** Passing `LOCAL_RELAY_URL` to `load()`, `publish()`, or `request()` routes through the normal net pipeline (policies, deduplication, tracker). Calling `repository.query()` / `repository.publish()` directly bypasses all of that. Reserve the direct API for bulk startup (`repository.load()`), introspection (`getEvent`, `isDeleted`, `dump`), and listening to `"update"` events.
- **`request()` without `autoClose` or `signal` never resolves.** Always pass `autoClose: true` or an `AbortSignal` when you just want a one-shot fetch. Use `load()` for the common case.
- **`load()` sets `autoClose: true` internally** and uses a 0.5 relay threshold; it resolves when half the relays' subscriptions have closed (typically after EOSE, timeout, or disconnect) — useful when some relays are slow or offline.
- **Relay URL normalization** happens inside `Pool.get(url)` via `normalizeRelayUrl`. Pass raw URLs everywhere; the pool handles canonicalization.
- **`defaultSocketPolicies` is mutable.** Push policies before any sockets are created. Sockets created before a policy is pushed will not have it applied.
- **`socketPolicyCloseInactive` only replays pending work on unexpected close.** It reopens and replays queued messages when a socket closes while work is pending — it does not proactively open sockets when new work is queued (that is `socketPolicyConnectOnSend`'s job). After `pool.remove(url)` the socket is cleaned up including its policy listeners, so `socketPolicyCloseInactive` can no longer reopen it.
- **`Pool.get(url)` lazily creates a new socket on every call after `pool.remove(url)`.** Calling `pool.remove(url)` forgets the URL and cleans up the socket — any subsequent `pool.get(url)` will construct a fresh socket. Call `pool.remove()` only when you want the pool to forget the URL entirely, not merely to disconnect temporarily.
- **`Tracker` is shared across relays in `request()`.** This means `onDuplicate` fires for events received from more than one relay — expected behavior for cross-relay deduplication.
- **`Repository.publish()` returns `false` for stale replaceable events.** If a newer version of a replaceable event is already stored, the older one is silently dropped.
- **`WrapManager` stores the decrypted rumor in the `Repository`** and copies relay tracking from the gift-wrap event id to the rumor id. Keep a reference to the `WrapManager` instance alongside your `Repository` and `Tracker` singletons.
- **`makeSocketPolicyAuth` requires a `sign` function** that returns a `Promise<SignedEvent>`. If the user cancels signing, have the `sign` function throw or reject; `doAuth` will catch the failure via `tryCatch` and automatically transition to `AuthStatus.DeniedSignature`, preventing infinite retry loops.
- **Each filter in `filters` array generates a separate REQ** inside `requestOne`. For large filter arrays consider merging them with `unionFilters` from `@welshman/util` before calling `request`.
- **`repository.load()` replaces all events, not appends.** It clears internal indexes first, then re-inserts every event. Emit a single batched `"update"` diff — do not call it repeatedly for incremental updates; use `repository.publish(event)` for that.
- **`RepositoryUpdate.removed` is `Set<string>`, not an array.** Iterate with `for...of` or `Array.from(removed)`. The `batch()` helper from `@welshman/lib` delivers updates as `RepositoryUpdate[]` to your flush callback — merge them yourself or use `mergeRepositoryUpdates`.
- **`tracker.load()` takes `Map<string, Set<string>>`** (the same type as `tracker.relaysById`). Load it after `repository.load()` so you can filter out orphaned event ids.