Remove relay package, move everything into net

This commit is contained in:
Jon Staab
2025-10-20 13:09:53 -07:00
parent 88650fb166
commit 0be540c0d2
42 changed files with 128 additions and 528 deletions
-5
View File
@@ -87,11 +87,6 @@ export default defineConfig({
{text: "NIP 59", link: "/signer/nip-59"}, {text: "NIP 59", link: "/signer/nip-59"},
], ],
}, },
{
text: "@welshman/relay",
link: "/relay/",
items: [],
},
{ {
text: "@welshman/router", text: "@welshman/router",
link: "/router/", link: "/router/",
-3
View File
@@ -7,9 +7,6 @@ Welshman is modular - install only what you need:
# Core nostr utilities (events, filters, tags) # Core nostr utilities (events, filters, tags)
npm i @welshman/util npm i @welshman/util
# In-memory event store and relay adapter
npm i @welshman/relay
# Networking and relay management # Networking and relay management
npm i @welshman/net npm i @welshman/net
-3
View File
@@ -30,9 +30,6 @@ features:
- title: "@welshman/signer" - title: "@welshman/signer"
details: Implementations of various nostr signing methods (NIP-01, NIP-07, NIP-46, NIP-55). details: Implementations of various nostr signing methods (NIP-01, NIP-07, NIP-46, NIP-55).
link: "/signer" link: "/signer"
- title: "@welshman/relay"
details: In-memory relay and event store.
link: "/relay"
- title: "@welshman/router" - title: "@welshman/router"
details: Tools for relay selection. details: Tools for relay selection.
link: "/router" link: "/router"
-143
View File
@@ -1,143 +0,0 @@
# @welshman/relay
[![version](https://badgen.net/npm/v/@welshman/relay)](https://npmjs.com/package/@welshman/relay)
A few utilites for storing nostr events in memory.
## What's Included
- **Event Store** - A Repository class which stores events in memory
- **Relay Adapter** - A LocalRelay class which adapts nostr messages to the repository
- **Event Tracker** - A Tracker class for managing which events have been seen from which relays
- **Gift Wrap Manager** - A WrapManager class for tracking and unwrapping NIP-59 gift wrapped events
## Quick Example
```typescript
import {Repository, LocalRelay} from "@welshman/relay"
// Create an in-memory event repository
const repository = Repository.get()
// Publish events directly to the repository
const textNote = {
id: "event123",
pubkey: "author-pubkey",
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: "Hello, world!",
sig: "signature"
}
repository.publish(textNote)
// Query events using filters
const recentNotes = repository.query([{kinds: [1], limit: 10}])
console.log(`Found ${recentNotes.length} text notes`)
// Listen for repository updates
repository.on("update", ({added, removed}) => {
console.log(`Added ${added.length} events, removed ${removed.size} events`)
})
// Create a local relay that adapts Nostr messages to the repository
const relay = new LocalRelay(repository)
// Listen for relay messages
relay.on("EVENT", (subId, event) => {
console.log(`Received event ${event.id} for subscription ${subId}`)
})
relay.on("OK", (eventId, success, message) => {
console.log(`Event ${eventId} ${success ? "accepted" : "rejected"}: ${message}`)
})
// Use relay protocol to publish and subscribe
relay.send("EVENT", {
id: "event456",
pubkey: "another-author",
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [["t", "welshman"]],
content: "Using LocalRelay!",
sig: "signature"
})
// Subscribe to events with hashtag
relay.send("REQ", "tagged", {kinds: [1], "#t": ["welshman"]})
```
### Tracking Events Across Relays
```typescript
import {Tracker} from "@welshman/relay"
const tracker = new Tracker()
// 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)
// Check which relays have sent an event
const relays = tracker.getRelays("event123") // Set(["wss://relay1.com", "wss://relay2.com"])
// Copy relay tracking from one event to another (useful for wrapped events)
tracker.copy("wrap-event-id", "rumor-event-id")
```
### Managing Gift Wrapped Events
The WrapManager handles NIP-59 gift wrapped events, automatically unwrapping incoming wrapped events and tracking the relationship between wraps and their inner rumors.
```typescript
import {Repository, LocalRelay, Tracker, WrapManager} from "@welshman/relay"
import {ISigner} from "@welshman/signer"
const repository = Repository.get()
const relay = new LocalRelay(repository)
const tracker = new Tracker()
// Create a wrap manager with a function to get signers for different pubkeys
const wrapManager = new WrapManager({
relay,
tracker,
getSigner: (pubkey: string) => {
// Return the appropriate signer for this pubkey
return mySignerMap.get(pubkey)
}
})
// When you publish a wrapped event, track it
wrapManager.add({
recipient: recipientPubkey,
wrap: wrappedEvent,
rumor: innerEvent
})
// When you receive a wrapped event, unwrap it
await wrapManager.unwrap(receivedWrapEvent)
// The rumor will be automatically published to the repository
// and relay tracking will be copied from the wrap to the rumor
// Remove wraps by various criteria
wrapManager.remove(wrapId)
wrapManager.removeByRumorId(rumorId)
// Listen for wrap manager events
wrapManager.on("add", (wrapItem) => {
console.log("Wrap added:", wrapItem)
})
wrapManager.on("remove", (wrapItem) => {
console.log("Wrap removed:", wrapItem)
})
```
## Installation
```bash
npm install @welshman/relay
```
+1 -1
View File
@@ -42,7 +42,7 @@ Creates a cached loader function with staleness checking and exponential backoff
import {writable} from 'svelte/store' import {writable} from 'svelte/store'
import {derived, readable} from "svelte/store" import {derived, readable} from "svelte/store"
import {readProfile, PROFILE, PublishedProfile} from "@welshman/util" import {readProfile, PROFILE, PublishedProfile} from "@welshman/util"
import {Repository} from "@welshman/relay" import {Repository} from "@welshman/net"
import {deriveEventsMapped, collection, withGetter} from "@welshman/store" import {deriveEventsMapped, collection, withGetter} from "@welshman/store"
const repository = new Repository() const repository = new Repository()
+2 -2
View File
@@ -39,7 +39,7 @@ Creates a reactive store that tracks whether an event is deleted by address.
## Example ## Example
```typescript ```typescript
import {Repository} from "@welshman/relay" import {Repository} from "@welshman/net"
import {deriveEventsMapped, deriveEvents} from "@welshman/store" import {deriveEventsMapped, deriveEvents} from "@welshman/store"
import {readProfile, PROFILE} from "@welshman/util" import {readProfile, PROFILE} from "@welshman/util"
@@ -71,4 +71,4 @@ profiles.subscribe(profiles => {
// Add some events to the repository // Add some events to the repository
repository.publish(someTextNoteEvent) repository.publish(someTextNoteEvent)
repository.publish(someProfileEvent) repository.publish(someProfileEvent)
``` ```
+1 -2
View File
@@ -1,6 +1,5 @@
import {PublishStatus} from "@welshman/net" import {PublishStatus, LOCAL_RELAY_URL} from "@welshman/net"
import {NOTE, DIRECT_MESSAGE, WRAP, makeEvent} from "@welshman/util" import {NOTE, DIRECT_MESSAGE, WRAP, makeEvent} from "@welshman/util"
import {LOCAL_RELAY_URL} from "@welshman/relay"
import {getPubkey, makeSecret, prep} from "@welshman/signer" import {getPubkey, makeSecret, prep} from "@welshman/signer"
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest" import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
import {repository, tracker} from "../src/core" import {repository, tracker} from "../src/core"
-1
View File
@@ -23,7 +23,6 @@
"@types/throttle-debounce": "^5.0.2", "@types/throttle-debounce": "^5.0.2",
"@welshman/feeds": "workspace:*", "@welshman/feeds": "workspace:*",
"@welshman/lib": "workspace:*", "@welshman/lib": "workspace:*",
"@welshman/relay": "workspace:*",
"@welshman/router": "workspace:*", "@welshman/router": "workspace:*",
"@welshman/net": "workspace:*", "@welshman/net": "workspace:*",
"@welshman/signer": "workspace:*", "@welshman/signer": "workspace:*",
+1 -1
View File
@@ -208,7 +208,7 @@ export type SendWrappedOptions = Omit<ThunkOptions, "event" | "relays"> & {
recipients: string[] recipients: string[]
} }
export const sendWrapped = async ({event, recipients, ...options}: SendWrappedOptions) => export const sendWrapped = ({event, recipients, ...options}: SendWrappedOptions) =>
new MergedThunk( new MergedThunk(
uniq(recipients).map(recipient => { uniq(recipients).map(recipient => {
const relays = Router.get().PubkeyInbox(recipient).getUrls() const relays = Router.get().PubkeyInbox(recipient).getUrls()
+1 -3
View File
@@ -1,13 +1,11 @@
import {throttle} from "@welshman/lib" import {throttle} from "@welshman/lib"
import {Repository, LocalRelay, Tracker} from "@welshman/relay" import {Repository, Tracker} from "@welshman/net"
import {custom} from "@welshman/store" import {custom} from "@welshman/store"
export const tracker = new Tracker() export const tracker = new Tracker()
export const repository = Repository.get() export const repository = Repository.get()
export const relay = new LocalRelay(repository)
// Adapt objects to stores // Adapt objects to stores
export const makeRepositoryStore = ({throttle: t = 300}: {throttle?: number} = {}) => export const makeRepositoryStore = ({throttle: t = 300}: {throttle?: number} = {}) =>
+3 -3
View File
@@ -13,8 +13,8 @@ import {
getPubkey, getPubkey,
ISigner, ISigner,
} from "@welshman/signer" } from "@welshman/signer"
import {WrapManager} from "@welshman/relay" import {WrapManager} from "@welshman/net"
import {relay, tracker} from "./core.js" import {tracker, repository} from "./core.js"
export enum SessionMethod { export enum SessionMethod {
Nip01 = "nip01", Nip01 = "nip01",
@@ -279,7 +279,7 @@ export const nip44EncryptToSelf = (payload: string) => {
// Gift wrap utilities // Gift wrap utilities
export const wrapManager = new WrapManager({relay, tracker}) export const wrapManager = new WrapManager({repository, tracker})
export const shouldUnwrap = withGetter(writable(false)) export const shouldUnwrap = withGetter(writable(false))
-1
View File
@@ -6,7 +6,6 @@
"paths": { "paths": {
"@welshman/feeds": ["../feeds/src/index.js"], "@welshman/feeds": ["../feeds/src/index.js"],
"@welshman/lib": ["../lib/src/index.js"], "@welshman/lib": ["../lib/src/index.js"],
"@welshman/relay": ["../relay/src/index.js"],
"@welshman/net": ["../net/src/index.js"], "@welshman/net": ["../net/src/index.js"],
"@welshman/signer": ["../signer/src/index.js"], "@welshman/signer": ["../signer/src/index.js"],
"@welshman/store": ["../store/src/index.js"], "@welshman/store": ["../store/src/index.js"],
-1
View File
@@ -22,7 +22,6 @@
"dependencies": { "dependencies": {
"@welshman/lib": "workspace:*", "@welshman/lib": "workspace:*",
"@welshman/net": "workspace:*", "@welshman/net": "workspace:*",
"@welshman/relay": "workspace:*",
"@welshman/router": "workspace:*", "@welshman/router": "workspace:*",
"@welshman/signer": "workspace:*", "@welshman/signer": "workspace:*",
"@welshman/util": "workspace:*", "@welshman/util": "workspace:*",
+1 -1
View File
@@ -11,7 +11,7 @@ import {
now, now,
} from "@welshman/lib" } from "@welshman/lib"
import {EPOCH, trimFilters, guessFilterDelta, TrustedEvent, Filter} from "@welshman/util" import {EPOCH, trimFilters, guessFilterDelta, TrustedEvent, Filter} from "@welshman/util"
import {Tracker} from "@welshman/relay" import {Tracker} from "@welshman/net"
import {Feed, FeedType, RequestItem} from "./core.js" import {Feed, FeedType, RequestItem} from "./core.js"
import {FeedCompiler, FeedCompilerOptions} from "./compiler.js" import {FeedCompiler, FeedCompilerOptions} from "./compiler.js"
import {requestPage} from "./request.js" import {requestPage} from "./request.js"
+1 -2
View File
@@ -10,9 +10,8 @@ import {
RELAYS, RELAYS,
} from "@welshman/util" } from "@welshman/util"
import {Nip01Signer, ISigner} from "@welshman/signer" import {Nip01Signer, ISigner} from "@welshman/signer"
import {LOCAL_RELAY_URL, Tracker} from "@welshman/relay"
import {Router, getFilterSelections, addMinimalFallbacks} from "@welshman/router" import {Router, getFilterSelections, addMinimalFallbacks} from "@welshman/router"
import {AdapterContext, request, publish} from "@welshman/net" import {LOCAL_RELAY_URL, Tracker, AdapterContext, request, publish} from "@welshman/net"
export type RequestPageOptions = { export type RequestPageOptions = {
filters: Filter[] filters: Filter[]
+3
View File
@@ -5,6 +5,9 @@
"outDir": "./dist", "outDir": "./dist",
"paths": { "paths": {
"@welshman/lib": ["../lib/src/index.js"], "@welshman/lib": ["../lib/src/index.js"],
"@welshman/net": ["../net/src/index.js"],
"@welshman/router": ["../router/src/index.js"],
"@welshman/signer": ["../signer/src/index.js"],
"@welshman/util": ["../util/src/index.js"] "@welshman/util": ["../util/src/index.js"]
} }
}, },
+24 -24
View File
@@ -1,7 +1,8 @@
import EventEmitter from "events"
import {describe, expect, it, vi, beforeEach, afterEach} from "vitest" import {describe, expect, it, vi, beforeEach, afterEach} from "vitest"
import {LocalRelay, Repository, LOCAL_RELAY_URL} from "@welshman/relay" import {makeEvent} from "@welshman/util"
import {prep, getPubkey, makeSecret} from "@welshman/signer"
import {AdapterEvent, SocketAdapter, LocalAdapter, getAdapter} from "../src/adapter" import {AdapterEvent, SocketAdapter, LocalAdapter, getAdapter} from "../src/adapter"
import {Repository, LOCAL_RELAY_URL} from "../src/repository"
import {ClientMessage, RelayMessage} from "../src/message" import {ClientMessage, RelayMessage} from "../src/message"
import {Socket, SocketEvent} from "../src/socket" import {Socket, SocketEvent} from "../src/socket"
import {Pool} from "../src/pool" import {Pool} from "../src/pool"
@@ -69,17 +70,13 @@ describe("SocketAdapter", () => {
}) })
describe("LocalAdapter", () => { describe("LocalAdapter", () => {
let relay: LocalRelay & EventEmitter let repository: Repository
let adapter: LocalAdapter let adapter: LocalAdapter
beforeEach(() => { beforeEach(() => {
const mockRelay = new EventEmitter() repository = new Repository()
Object.assign(mockRelay, { adapter = new LocalAdapter(repository)
send: vi.fn(), vi.useFakeTimers()
removeAllListeners: vi.fn(),
})
relay = mockRelay as unknown as LocalRelay & EventEmitter
adapter = new LocalAdapter(relay)
}) })
afterEach(() => { afterEach(() => {
@@ -88,32 +85,35 @@ describe("LocalAdapter", () => {
}) })
it("should initialize with correct relay", () => { it("should initialize with correct relay", () => {
expect(adapter.relay).toBe(relay)
expect(adapter.urls).toEqual([LOCAL_RELAY_URL]) expect(adapter.urls).toEqual([LOCAL_RELAY_URL])
expect(adapter.sockets).toEqual([]) expect(adapter.sockets).toEqual([])
}) })
it("should forward received messages", () => { it("should forward received messages", () => {
const receiveSpy = vi.fn() const receiveSpy = vi.fn()
const pubkey = getPubkey(makeSecret())
const event = prep(makeEvent(1), pubkey)
adapter.send(["REQ", "r1", {kinds: [1]}])
adapter.send(["REQ", "r2", {kinds: [2]}])
adapter.on(AdapterEvent.Receive, receiveSpy) adapter.on(AdapterEvent.Receive, receiveSpy)
repository.publish(event)
const message: RelayMessage = ["EVENT", "123", {id: "123", kind: 1}] expect(receiveSpy).toHaveBeenCalledTimes(1)
relay.emit("*", ...message) expect(receiveSpy).toHaveBeenCalledWith(["EVENT", "r1", event], LOCAL_RELAY_URL)
expect(receiveSpy).toHaveBeenCalledWith(message, LOCAL_RELAY_URL)
}) })
it("should send messages to relay", () => { it("should send messages to relay", async () => {
const message: ClientMessage = ["EVENT", {id: "123", kind: 1}] const publishSpy = vi.spyOn(repository, "publish")
adapter.send(message) const pubkey = getPubkey(makeSecret())
const event = prep(makeEvent(1), pubkey)
expect(relay.send).toHaveBeenCalledWith("EVENT", message[1]) adapter.send(["EVENT", event])
})
it("should cleanup properly", () => { await vi.runAllTimersAsync()
const removeListenersSpy = vi.spyOn(adapter, "removeAllListeners")
adapter.cleanup() expect(publishSpy).toHaveBeenCalledTimes(1)
expect(removeListenersSpy).toHaveBeenCalled() expect(publishSpy).toHaveBeenCalledWith(event)
}) })
}) })
-1
View File
@@ -21,7 +21,6 @@
}, },
"dependencies": { "dependencies": {
"@welshman/lib": "workspace:*", "@welshman/lib": "workspace:*",
"@welshman/relay": "workspace:*",
"@welshman/util": "workspace:*", "@welshman/util": "workspace:*",
"events": "^3.3.0", "events": "^3.3.0",
"isomorphic-ws": "^5.0.0" "isomorphic-ws": "^5.0.0"
+59 -10
View File
@@ -1,8 +1,16 @@
import EventEmitter from "events" import EventEmitter from "events"
import {call, mergeRight, on} from "@welshman/lib" import {call, sleep, mergeRight, on} from "@welshman/lib"
import {isRelayUrl} from "@welshman/util" import {isRelayUrl, matchFilters, Filter} from "@welshman/util"
import {LocalRelay, LOCAL_RELAY_URL} from "@welshman/relay" import {LOCAL_RELAY_URL, Repository} from "./repository"
import {RelayMessage, ClientMessage} from "./message.js" import {
RelayMessage,
RelayMessageType,
ClientMessage,
ClientMessageType,
ClientEvent,
ClientReq,
ClientClose,
} from "./message.js"
import {Socket, SocketEvent} from "./socket.js" import {Socket, SocketEvent} from "./socket.js"
import {Unsubscriber} from "./util.js" import {Unsubscriber} from "./util.js"
import {netContext, NetContext} from "./context.js" import {netContext, NetContext} from "./context.js"
@@ -53,12 +61,20 @@ export class SocketAdapter extends AbstractAdapter {
} }
export class LocalAdapter extends AbstractAdapter { export class LocalAdapter extends AbstractAdapter {
constructor(readonly relay: LocalRelay) { subs = new Map<string, Filter[]>()
constructor(readonly repository: Repository) {
super() super()
this._unsubscribers.push( this._unsubscribers.push(
on(relay, "*", (...message: RelayMessage) => { on(repository, "update", ({added}) => {
this.emit(AdapterEvent.Receive, message, LOCAL_RELAY_URL) for (const [subId, filters] of this.subs.entries()) {
for (const event of added) {
if (matchFilters(filters, event)) {
this.#receive([RelayMessageType.Event, subId, event])
}
}
}
}), }),
) )
} }
@@ -72,9 +88,42 @@ export class LocalAdapter extends AbstractAdapter {
} }
send(message: ClientMessage) { send(message: ClientMessage) {
const [type, ...rest] = message switch (message[0]) {
case ClientMessageType.Event:
return this.#handleEVENT(message as ClientEvent)
case ClientMessageType.Close:
return this.#handleCLOSE(message as ClientClose)
case ClientMessageType.Req:
return this.#handleREQ(message as ClientReq)
}
}
this.relay.send(type, ...rest) #receive(message: RelayMessage) {
this.emit(AdapterEvent.Receive, message, LOCAL_RELAY_URL)
}
#handleEVENT([_, event]: ClientEvent) {
this.repository.publish(event)
// Callers generally expect async relays
sleep(1).then(() => this.#receive([RelayMessageType.Ok, event.id, true, ""]))
}
#handleCLOSE([_, subId]: ClientClose) {
this.subs.delete(subId)
}
#handleREQ([_, subId, ...filters]: ClientReq) {
this.subs.set(subId, filters)
// Callers generally expect async relays
sleep(1).then(() => {
for (const event of this.repository.query(filters)) {
this.#receive([RelayMessageType.Event, subId, event])
}
this.#receive([RelayMessageType.Eose, subId])
})
} }
} }
@@ -113,7 +162,7 @@ export const getAdapter = (url: string, adapterContext: AdapterContext = {}) =>
} }
if (url === LOCAL_RELAY_URL) { if (url === LOCAL_RELAY_URL) {
return new LocalAdapter(new LocalRelay(context.repository)) return new LocalAdapter(context.repository)
} }
if (isRelayUrl(url)) { if (isRelayUrl(url)) {
+1 -1
View File
@@ -1,6 +1,6 @@
import {Repository} from "@welshman/relay"
import {verifyEvent, TrustedEvent} from "@welshman/util" import {verifyEvent, TrustedEvent} from "@welshman/util"
import {AbstractAdapter} from "./adapter.js" import {AbstractAdapter} from "./adapter.js"
import {Repository} from "./repository.js"
import {Pool} from "./pool.js" import {Pool} from "./pool.js"
export type NetContext = { export type NetContext = {
+3
View File
@@ -8,4 +8,7 @@ export * from "./policy.js"
export * from "./pool.js" export * from "./pool.js"
export * from "./publish.js" export * from "./publish.js"
export * from "./socket.js" export * from "./socket.js"
export * from "./repository.js"
export * from "./request.js" export * from "./request.js"
export * from "./tracker.js"
export * from "./wrapManager.js"
+1 -1
View File
@@ -18,11 +18,11 @@ import {
deduplicateEvents, deduplicateEvents,
getFilterResultCardinality, getFilterResultCardinality,
} from "@welshman/util" } from "@welshman/util"
import {Tracker} from "@welshman/relay"
import {RelayMessage, ClientMessageType, isRelayEvent, isRelayEose} from "./message.js" import {RelayMessage, ClientMessageType, isRelayEvent, isRelayEose} from "./message.js"
import {getAdapter, AdapterContext, AdapterEvent} from "./adapter.js" import {getAdapter, AdapterContext, AdapterEvent} from "./adapter.js"
import {SocketEvent, SocketStatus} from "./socket.js" import {SocketEvent, SocketStatus} from "./socket.js"
import {netContext} from "./context.js" import {netContext} from "./context.js"
import {Tracker} from "./tracker.js"
export type BaseRequestOptions = { export type BaseRequestOptions = {
signal?: AbortSignal signal?: AbortSignal
@@ -1,7 +1,7 @@
import {Emitter, remove, omit} from "@welshman/lib" import {Emitter, remove, omit} from "@welshman/lib"
import {HashedEvent, SignedEvent} from "@welshman/util" import {HashedEvent, SignedEvent} from "@welshman/util"
import {Tracker} from "./tracker.js" import {Tracker} from "./tracker.js"
import {LocalRelay} from "./relay.js" import {Repository} from "./repository.js"
export type WrapItem = Omit<HashedEvent, "content"> & { export type WrapItem = Omit<HashedEvent, "content"> & {
rumorId: string rumorId: string
@@ -11,24 +11,30 @@ export type WrapItem = Omit<HashedEvent, "content"> & {
export type WrapReference = string[] export type WrapReference = string[]
export type WrapManagerOptions = { export type WrapManagerOptions = {
relay: LocalRelay repository: Repository
tracker: Tracker tracker: Tracker
} }
export class WrapManager extends Emitter { export class WrapManager extends Emitter {
_wrapIndex = new Map<string, WrapItem>() _wrapIndex = new Map<string, WrapItem>()
_rumorIndex = new Map<string, WrapReference>() _rumorIndex = new Map<string, WrapReference>()
_recipientIndex = new Map<string, WrapReference>()
constructor(readonly options: WrapManagerOptions) { constructor(readonly options: WrapManagerOptions) {
super() super()
} }
getRumor = (id: string) => { // Reading/exporting
const wrapItem = this._wrapIndex.get(id)
dump = () => Array.from(this._wrapIndex.values())
getWraps = (rumorId: string) =>
this._rumorIndex.get(rumorId).map(wrapId => this._wrapIndex.get(wrapId)!)
getRumor = (wrapId: string) => {
const wrapItem = this._wrapIndex.get(wrapId)
if (wrapItem) { if (wrapItem) {
return this.options.relay.repository.getEvent(wrapItem.rumorId) return this.options.repository.getEvent(wrapItem.rumorId)
} }
} }
@@ -55,8 +61,8 @@ export class WrapManager extends Emitter {
this._add(wrapItem) this._add(wrapItem)
// Send via our relay so that listeners get notified // Save to our repository
this.options.relay.send("EVENT", rumor) this.options.repository.publish(rumor)
// Mark the rumor as having come from the wrap's urls // Mark the rumor as having come from the wrap's urls
this.options.tracker.copy(wrap.id, rumor.id) this.options.tracker.copy(wrap.id, rumor.id)
@@ -71,7 +77,7 @@ export class WrapManager extends Emitter {
if (wrapItem) { if (wrapItem) {
this._remove(wrapItem) this._remove(wrapItem)
this.options.relay.repository.removeEvent(wrapItem.rumorId) this.options.repository.removeEvent(wrapItem.rumorId)
this.emit("remove", wrapItem) this.emit("remove", wrapItem)
} }
} }
+1 -2
View File
@@ -5,8 +5,7 @@
"outDir": "./dist", "outDir": "./dist",
"paths": { "paths": {
"@welshman/lib": ["../lib/src/index.js"], "@welshman/lib": ["../lib/src/index.js"],
"@welshman/util": ["../util/src/index.js"], "@welshman/util": ["../util/src/index.js"]
"@welshman/relay": ["../relay/src/index.js"]
} }
}, },
-4
View File
@@ -1,4 +0,0 @@
build
normalize-url
Negentropy.ts
__tests__
-156
View File
@@ -1,156 +0,0 @@
import {describe, it, expect, beforeEach, vi, afterEach} from "vitest"
import {now} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {LocalRelay} from "../src/relay"
import {Repository} from "../src/repository"
describe("LocalRelay", () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
// Realistic Nostr data
const pubkey = "ee".repeat(32)
const id = "ff".repeat(32)
const sig = "00".repeat(64)
const currentTime = now()
const createEvent = (overrides = {}): TrustedEvent => ({
id: id,
pubkey: pubkey,
created_at: currentTime,
kind: 1,
tags: [],
content: "Hello Nostr!",
sig: sig,
...overrides,
})
describe("LocalRelay class", () => {
let relay: LocalRelay
let repository: Repository<TrustedEvent>
beforeEach(() => {
repository = new Repository<TrustedEvent>()
relay = new LocalRelay(repository)
})
describe("EVENT handling", () => {
it("should publish events to repository", async () => {
const event = createEvent()
const publishSpy = vi.spyOn(repository, "publish")
relay.send("EVENT", event)
expect(publishSpy).toHaveBeenCalledWith(event)
// Should emit OK
const okHandler = vi.fn()
relay.on("OK", okHandler)
// Wait for async operations
await vi.runAllTimersAsync()
expect(okHandler).toHaveBeenCalledWith(event.id, true, "")
})
it("should notify matching subscribers", async () => {
const event = createEvent()
const subId = "test-sub"
const filter = {kinds: [1]}
relay.send("REQ", subId, filter)
const eventHandler = vi.fn()
relay.on("EVENT", eventHandler)
relay.send("EVENT", event)
await vi.runAllTimersAsync()
expect(eventHandler).toHaveBeenCalledWith(subId, event)
})
it("should not notify for deleted events", async () => {
const event = createEvent()
repository.removeEvent(event.id)
const eventHandler = vi.fn()
relay.on("EVENT", eventHandler)
relay.send("EVENT", event)
await vi.runAllTimersAsync()
expect(eventHandler).not.toHaveBeenCalled()
})
})
describe("REQ handling", () => {
it("should handle subscription requests", async () => {
const event = createEvent()
repository.publish(event)
const subId = "test-sub"
const filter = {kinds: [1]}
const eventHandler = vi.fn()
const eoseHandler = vi.fn()
relay.on("EVENT", eventHandler)
relay.on("EOSE", eoseHandler)
relay.send("REQ", subId, filter)
await vi.runAllTimersAsync()
expect(eventHandler).toHaveBeenCalledWith(subId, event)
expect(eoseHandler).toHaveBeenCalledWith(subId)
})
it("should handle multiple filters", async () => {
const event1 = createEvent({kind: 1})
const event2 = createEvent({kind: 2, id: "ee".repeat(31)})
repository.publish(event1)
repository.publish(event2)
const subId = "test-sub"
const filters = [{kinds: [1]}, {kinds: [2]}]
const eventHandler = vi.fn()
relay.on("EVENT", eventHandler)
relay.send("REQ", subId, ...filters)
await vi.runAllTimersAsync()
expect(eventHandler).toHaveBeenCalledTimes(2)
})
})
describe("CLOSE handling", () => {
it("should close subscriptions", async () => {
const subId = "test-sub"
relay.send("REQ", subId, {kinds: [1]})
relay.send("CLOSE", subId)
await vi.runAllTimersAsync()
const event = createEvent()
const eventHandler = vi.fn()
relay.on("EVENT", eventHandler)
relay.send("EVENT", event)
await vi.runAllTimersAsync()
expect(eventHandler).not.toHaveBeenCalled()
})
})
})
})
-31
View File
@@ -1,31 +0,0 @@
{
"name": "@welshman/relay",
"version": "0.5.4",
"author": "hodlbod",
"license": "MIT",
"description": "An in-memory nostr relay implementation.",
"publishConfig": {
"access": "public"
},
"type": "module",
"main": "dist/relay/src/index.js",
"types": "dist/relay/src/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "pnpm run clean && pnpm run compile --force",
"clean": "rimraf ./dist",
"compile": "tsc -b tsconfig.build.json",
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"@welshman/lib": "workspace:*",
"@welshman/util": "workspace:*",
"@welshman/signer": "workspace:*"
},
"devDependencies": {
"rimraf": "~6.0.0",
"typescript": "~5.8.0"
}
}
-4
View File
@@ -1,4 +0,0 @@
export * from "./relay.js"
export * from "./repository.js"
export * from "./tracker.js"
export * from "./wrapManager.js"
-56
View File
@@ -1,56 +0,0 @@
import {Emitter, sleep} from "@welshman/lib"
import {Filter, TrustedEvent, matchFilters} from "@welshman/util"
import {Repository} from "./repository.js"
export class LocalRelay extends Emitter {
subs = new Map<string, Filter[]>()
constructor(readonly repository: Repository) {
super()
}
send(type: string, ...message: any[]) {
switch (type) {
case "EVENT":
return this.handleEVENT(message as [TrustedEvent])
case "CLOSE":
return this.handleCLOSE(message as [string])
case "REQ":
return this.handleREQ(message as [string, ...Filter[]])
}
}
handleEVENT([event]: [TrustedEvent]) {
this.repository.publish(event)
// Callers generally expect async relays
void sleep(1).then(() => {
this.emit("OK", event.id, true, "")
if (!this.repository.isDeleted(event)) {
for (const [subId, filters] of this.subs.entries()) {
if (matchFilters(filters, event)) {
this.emit("EVENT", subId, event)
}
}
}
})
}
handleCLOSE([subId]: [string]) {
this.subs.delete(subId)
}
handleREQ([subId, ...filters]: [string, ...Filter[]]) {
this.subs.set(subId, filters)
// Callers generally expect async relays
void sleep(1).then(() => {
for (const event of this.repository.query(filters)) {
this.emit("EVENT", subId, event)
}
this.emit("EOSE", subId)
})
}
}
-15
View File
@@ -1,15 +0,0 @@
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"outDir": "./dist",
"paths": {
"@welshman/lib": ["../lib/src/index.js"],
"@welshman/util": ["../util/src/index.js"]
}
},
"include": [
"src/**/*"
]
}
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}
+1 -1
View File
@@ -22,7 +22,7 @@
"dependencies": { "dependencies": {
"@welshman/lib": "workspace:*", "@welshman/lib": "workspace:*",
"@welshman/util": "workspace:*", "@welshman/util": "workspace:*",
"@welshman/relay": "workspace:*" "@welshman/net": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"rimraf": "~6.0.0", "rimraf": "~6.0.0",
+1 -1
View File
@@ -35,7 +35,7 @@ import {
getPubkeyTags, getPubkeyTags,
RelayMode, RelayMode,
} from "@welshman/util" } from "@welshman/util"
import {Repository} from "@welshman/relay" import {Repository} from "@welshman/net"
export const INDEXED_KINDS = [PROFILE, RELAYS, INBOX_RELAYS, FOLLOWS] export const INDEXED_KINDS = [PROFILE, RELAYS, INBOX_RELAYS, FOLLOWS]
-1
View File
@@ -6,7 +6,6 @@
"paths": { "paths": {
"@welshman/lib": ["../lib/src/index.js"], "@welshman/lib": ["../lib/src/index.js"],
"@welshman/util": ["../util/src/index.js"], "@welshman/util": ["../util/src/index.js"],
"@welshman/relay": ["../relay/src/index.js"],
"@welshman/net": ["../net/src/index.js"] "@welshman/net": ["../net/src/index.js"]
} }
}, },
+1 -1
View File
@@ -1,5 +1,5 @@
import {TrustedEvent} from "@welshman/util" import {TrustedEvent} from "@welshman/util"
import {Repository} from "@welshman/relay" import {Repository} from "@welshman/net"
import {get} from "svelte/store" import {get} from "svelte/store"
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest" import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
import { import {
+1 -1
View File
@@ -22,7 +22,7 @@
"dependencies": { "dependencies": {
"@welshman/lib": "workspace:*", "@welshman/lib": "workspace:*",
"@welshman/util": "workspace:*", "@welshman/util": "workspace:*",
"@welshman/relay": "workspace:*", "@welshman/net": "workspace:*",
"svelte": "^4.2.18" "svelte": "^4.2.18"
}, },
"devDependencies": { "devDependencies": {
+1 -1
View File
@@ -1,7 +1,7 @@
import {derived} from "svelte/store" import {derived} from "svelte/store"
import {sortBy, identity, ensurePlural, removeNil, batch, partition, first} from "@welshman/lib" import {sortBy, identity, ensurePlural, removeNil, batch, partition, first} from "@welshman/lib"
import {Repository} from "@welshman/relay"
import {matchFilters, getIdAndAddress, getIdFilters, Filter, TrustedEvent} from "@welshman/util" import {matchFilters, getIdAndAddress, getIdFilters, Filter, TrustedEvent} from "@welshman/util"
import {Repository} from "@welshman/net"
import {custom} from "./custom.js" import {custom} from "./custom.js"
export type DeriveEventsMappedOptions<T> = { export type DeriveEventsMappedOptions<T> = {
+1 -1
View File
@@ -6,7 +6,7 @@
"paths": { "paths": {
"@welshman/lib": ["../lib/src/index.js"], "@welshman/lib": ["../lib/src/index.js"],
"@welshman/util": ["../util/src/index.js"], "@welshman/util": ["../util/src/index.js"],
"@welshman/relay": ["../relay/src/index.js"] "@welshman/net": ["../net/src/index.js"]
} }
}, },
+4 -32
View File
@@ -74,9 +74,6 @@ importers:
'@welshman/net': '@welshman/net':
specifier: workspace:* specifier: workspace:*
version: link:../net version: link:../net
'@welshman/relay':
specifier: workspace:*
version: link:../relay
'@welshman/router': '@welshman/router':
specifier: workspace:* specifier: workspace:*
version: link:../router version: link:../router
@@ -194,9 +191,6 @@ importers:
'@welshman/net': '@welshman/net':
specifier: workspace:* specifier: workspace:*
version: link:../net version: link:../net
'@welshman/relay':
specifier: workspace:*
version: link:../relay
'@welshman/router': '@welshman/router':
specifier: workspace:* specifier: workspace:*
version: link:../router version: link:../router
@@ -241,9 +235,6 @@ importers:
'@welshman/lib': '@welshman/lib':
specifier: workspace:* specifier: workspace:*
version: link:../lib version: link:../lib
'@welshman/relay':
specifier: workspace:*
version: link:../relay
'@welshman/util': '@welshman/util':
specifier: workspace:* specifier: workspace:*
version: link:../util version: link:../util
@@ -261,33 +252,14 @@ importers:
specifier: ~5.8.0 specifier: ~5.8.0
version: 5.8.2 version: 5.8.2
packages/relay:
dependencies:
'@welshman/lib':
specifier: workspace:*
version: link:../lib
'@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/router: packages/router:
dependencies: dependencies:
'@welshman/lib': '@welshman/lib':
specifier: workspace:* specifier: workspace:*
version: link:../lib version: link:../lib
'@welshman/relay': '@welshman/net':
specifier: workspace:* specifier: workspace:*
version: link:../relay version: link:../net
'@welshman/util': '@welshman/util':
specifier: workspace:* specifier: workspace:*
version: link:../util version: link:../util
@@ -338,9 +310,9 @@ importers:
'@welshman/lib': '@welshman/lib':
specifier: workspace:* specifier: workspace:*
version: link:../lib version: link:../lib
'@welshman/relay': '@welshman/net':
specifier: workspace:* specifier: workspace:*
version: link:../relay version: link:../net
'@welshman/util': '@welshman/util':
specifier: workspace:* specifier: workspace:*
version: link:../util version: link:../util