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
+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 {LOCAL_RELAY_URL} from "@welshman/relay"
import {getPubkey, makeSecret, prep} from "@welshman/signer"
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
import {repository, tracker} from "../src/core"
-1
View File
@@ -23,7 +23,6 @@
"@types/throttle-debounce": "^5.0.2",
"@welshman/feeds": "workspace:*",
"@welshman/lib": "workspace:*",
"@welshman/relay": "workspace:*",
"@welshman/router": "workspace:*",
"@welshman/net": "workspace:*",
"@welshman/signer": "workspace:*",
+1 -1
View File
@@ -208,7 +208,7 @@ export type SendWrappedOptions = Omit<ThunkOptions, "event" | "relays"> & {
recipients: string[]
}
export const sendWrapped = async ({event, recipients, ...options}: SendWrappedOptions) =>
export const sendWrapped = ({event, recipients, ...options}: SendWrappedOptions) =>
new MergedThunk(
uniq(recipients).map(recipient => {
const relays = Router.get().PubkeyInbox(recipient).getUrls()
+1 -3
View File
@@ -1,13 +1,11 @@
import {throttle} from "@welshman/lib"
import {Repository, LocalRelay, Tracker} from "@welshman/relay"
import {Repository, Tracker} from "@welshman/net"
import {custom} from "@welshman/store"
export const tracker = new Tracker()
export const repository = Repository.get()
export const relay = new LocalRelay(repository)
// Adapt objects to stores
export const makeRepositoryStore = ({throttle: t = 300}: {throttle?: number} = {}) =>
+3 -3
View File
@@ -13,8 +13,8 @@ import {
getPubkey,
ISigner,
} from "@welshman/signer"
import {WrapManager} from "@welshman/relay"
import {relay, tracker} from "./core.js"
import {WrapManager} from "@welshman/net"
import {tracker, repository} from "./core.js"
export enum SessionMethod {
Nip01 = "nip01",
@@ -279,7 +279,7 @@ export const nip44EncryptToSelf = (payload: string) => {
// Gift wrap utilities
export const wrapManager = new WrapManager({relay, tracker})
export const wrapManager = new WrapManager({repository, tracker})
export const shouldUnwrap = withGetter(writable(false))
-1
View File
@@ -6,7 +6,6 @@
"paths": {
"@welshman/feeds": ["../feeds/src/index.js"],
"@welshman/lib": ["../lib/src/index.js"],
"@welshman/relay": ["../relay/src/index.js"],
"@welshman/net": ["../net/src/index.js"],
"@welshman/signer": ["../signer/src/index.js"],
"@welshman/store": ["../store/src/index.js"],
-1
View File
@@ -22,7 +22,6 @@
"dependencies": {
"@welshman/lib": "workspace:*",
"@welshman/net": "workspace:*",
"@welshman/relay": "workspace:*",
"@welshman/router": "workspace:*",
"@welshman/signer": "workspace:*",
"@welshman/util": "workspace:*",
+1 -1
View File
@@ -11,7 +11,7 @@ import {
now,
} from "@welshman/lib"
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 {FeedCompiler, FeedCompilerOptions} from "./compiler.js"
import {requestPage} from "./request.js"
+1 -2
View File
@@ -10,9 +10,8 @@ import {
RELAYS,
} from "@welshman/util"
import {Nip01Signer, ISigner} from "@welshman/signer"
import {LOCAL_RELAY_URL, Tracker} from "@welshman/relay"
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 = {
filters: Filter[]
+3
View File
@@ -5,6 +5,9 @@
"outDir": "./dist",
"paths": {
"@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"]
}
},
+24 -24
View File
@@ -1,7 +1,8 @@
import EventEmitter from "events"
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 {Repository, LOCAL_RELAY_URL} from "../src/repository"
import {ClientMessage, RelayMessage} from "../src/message"
import {Socket, SocketEvent} from "../src/socket"
import {Pool} from "../src/pool"
@@ -69,17 +70,13 @@ describe("SocketAdapter", () => {
})
describe("LocalAdapter", () => {
let relay: LocalRelay & EventEmitter
let repository: Repository
let adapter: LocalAdapter
beforeEach(() => {
const mockRelay = new EventEmitter()
Object.assign(mockRelay, {
send: vi.fn(),
removeAllListeners: vi.fn(),
})
relay = mockRelay as unknown as LocalRelay & EventEmitter
adapter = new LocalAdapter(relay)
repository = new Repository()
adapter = new LocalAdapter(repository)
vi.useFakeTimers()
})
afterEach(() => {
@@ -88,32 +85,35 @@ describe("LocalAdapter", () => {
})
it("should initialize with correct relay", () => {
expect(adapter.relay).toBe(relay)
expect(adapter.urls).toEqual([LOCAL_RELAY_URL])
expect(adapter.sockets).toEqual([])
})
it("should forward received messages", () => {
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)
repository.publish(event)
const message: RelayMessage = ["EVENT", "123", {id: "123", kind: 1}]
relay.emit("*", ...message)
expect(receiveSpy).toHaveBeenCalledWith(message, LOCAL_RELAY_URL)
expect(receiveSpy).toHaveBeenCalledTimes(1)
expect(receiveSpy).toHaveBeenCalledWith(["EVENT", "r1", event], LOCAL_RELAY_URL)
})
it("should send messages to relay", () => {
const message: ClientMessage = ["EVENT", {id: "123", kind: 1}]
adapter.send(message)
it("should send messages to relay", async () => {
const publishSpy = vi.spyOn(repository, "publish")
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", () => {
const removeListenersSpy = vi.spyOn(adapter, "removeAllListeners")
adapter.cleanup()
expect(removeListenersSpy).toHaveBeenCalled()
await vi.runAllTimersAsync()
expect(publishSpy).toHaveBeenCalledTimes(1)
expect(publishSpy).toHaveBeenCalledWith(event)
})
})
-1
View File
@@ -21,7 +21,6 @@
},
"dependencies": {
"@welshman/lib": "workspace:*",
"@welshman/relay": "workspace:*",
"@welshman/util": "workspace:*",
"events": "^3.3.0",
"isomorphic-ws": "^5.0.0"
+59 -10
View File
@@ -1,8 +1,16 @@
import EventEmitter from "events"
import {call, mergeRight, on} from "@welshman/lib"
import {isRelayUrl} from "@welshman/util"
import {LocalRelay, LOCAL_RELAY_URL} from "@welshman/relay"
import {RelayMessage, ClientMessage} from "./message.js"
import {call, sleep, mergeRight, on} from "@welshman/lib"
import {isRelayUrl, matchFilters, Filter} from "@welshman/util"
import {LOCAL_RELAY_URL, Repository} from "./repository"
import {
RelayMessage,
RelayMessageType,
ClientMessage,
ClientMessageType,
ClientEvent,
ClientReq,
ClientClose,
} from "./message.js"
import {Socket, SocketEvent} from "./socket.js"
import {Unsubscriber} from "./util.js"
import {netContext, NetContext} from "./context.js"
@@ -53,12 +61,20 @@ export class SocketAdapter extends AbstractAdapter {
}
export class LocalAdapter extends AbstractAdapter {
constructor(readonly relay: LocalRelay) {
subs = new Map<string, Filter[]>()
constructor(readonly repository: Repository) {
super()
this._unsubscribers.push(
on(relay, "*", (...message: RelayMessage) => {
this.emit(AdapterEvent.Receive, message, LOCAL_RELAY_URL)
on(repository, "update", ({added}) => {
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) {
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) {
return new LocalAdapter(new LocalRelay(context.repository))
return new LocalAdapter(context.repository)
}
if (isRelayUrl(url)) {
+1 -1
View File
@@ -1,6 +1,6 @@
import {Repository} from "@welshman/relay"
import {verifyEvent, TrustedEvent} from "@welshman/util"
import {AbstractAdapter} from "./adapter.js"
import {Repository} from "./repository.js"
import {Pool} from "./pool.js"
export type NetContext = {
+3
View File
@@ -8,4 +8,7 @@ export * from "./policy.js"
export * from "./pool.js"
export * from "./publish.js"
export * from "./socket.js"
export * from "./repository.js"
export * from "./request.js"
export * from "./tracker.js"
export * from "./wrapManager.js"
+1 -1
View File
@@ -18,11 +18,11 @@ import {
deduplicateEvents,
getFilterResultCardinality,
} from "@welshman/util"
import {Tracker} from "@welshman/relay"
import {RelayMessage, ClientMessageType, isRelayEvent, isRelayEose} from "./message.js"
import {getAdapter, AdapterContext, AdapterEvent} from "./adapter.js"
import {SocketEvent, SocketStatus} from "./socket.js"
import {netContext} from "./context.js"
import {Tracker} from "./tracker.js"
export type BaseRequestOptions = {
signal?: AbortSignal
@@ -1,7 +1,7 @@
import {Emitter, remove, omit} from "@welshman/lib"
import {HashedEvent, SignedEvent} from "@welshman/util"
import {Tracker} from "./tracker.js"
import {LocalRelay} from "./relay.js"
import {Repository} from "./repository.js"
export type WrapItem = Omit<HashedEvent, "content"> & {
rumorId: string
@@ -11,24 +11,30 @@ export type WrapItem = Omit<HashedEvent, "content"> & {
export type WrapReference = string[]
export type WrapManagerOptions = {
relay: LocalRelay
repository: Repository
tracker: Tracker
}
export class WrapManager extends Emitter {
_wrapIndex = new Map<string, WrapItem>()
_rumorIndex = new Map<string, WrapReference>()
_recipientIndex = new Map<string, WrapReference>()
constructor(readonly options: WrapManagerOptions) {
super()
}
getRumor = (id: string) => {
const wrapItem = this._wrapIndex.get(id)
// Reading/exporting
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) {
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)
// Send via our relay so that listeners get notified
this.options.relay.send("EVENT", rumor)
// Save to our repository
this.options.repository.publish(rumor)
// Mark the rumor as having come from the wrap's urls
this.options.tracker.copy(wrap.id, rumor.id)
@@ -71,7 +77,7 @@ export class WrapManager extends Emitter {
if (wrapItem) {
this._remove(wrapItem)
this.options.relay.repository.removeEvent(wrapItem.rumorId)
this.options.repository.removeEvent(wrapItem.rumorId)
this.emit("remove", wrapItem)
}
}
+1 -2
View File
@@ -5,8 +5,7 @@
"outDir": "./dist",
"paths": {
"@welshman/lib": ["../lib/src/index.js"],
"@welshman/util": ["../util/src/index.js"],
"@welshman/relay": ["../relay/src/index.js"]
"@welshman/util": ["../util/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": {
"@welshman/lib": "workspace:*",
"@welshman/util": "workspace:*",
"@welshman/relay": "workspace:*"
"@welshman/net": "workspace:*"
},
"devDependencies": {
"rimraf": "~6.0.0",
+1 -1
View File
@@ -35,7 +35,7 @@ import {
getPubkeyTags,
RelayMode,
} from "@welshman/util"
import {Repository} from "@welshman/relay"
import {Repository} from "@welshman/net"
export const INDEXED_KINDS = [PROFILE, RELAYS, INBOX_RELAYS, FOLLOWS]
-1
View File
@@ -6,7 +6,6 @@
"paths": {
"@welshman/lib": ["../lib/src/index.js"],
"@welshman/util": ["../util/src/index.js"],
"@welshman/relay": ["../relay/src/index.js"],
"@welshman/net": ["../net/src/index.js"]
}
},
+1 -1
View File
@@ -1,5 +1,5 @@
import {TrustedEvent} from "@welshman/util"
import {Repository} from "@welshman/relay"
import {Repository} from "@welshman/net"
import {get} from "svelte/store"
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
import {
+1 -1
View File
@@ -22,7 +22,7 @@
"dependencies": {
"@welshman/lib": "workspace:*",
"@welshman/util": "workspace:*",
"@welshman/relay": "workspace:*",
"@welshman/net": "workspace:*",
"svelte": "^4.2.18"
},
"devDependencies": {
+1 -1
View File
@@ -1,7 +1,7 @@
import {derived} from "svelte/store"
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 {Repository} from "@welshman/net"
import {custom} from "./custom.js"
export type DeriveEventsMappedOptions<T> = {
+1 -1
View File
@@ -6,7 +6,7 @@
"paths": {
"@welshman/lib": ["../lib/src/index.js"],
"@welshman/util": ["../util/src/index.js"],
"@welshman/relay": ["../relay/src/index.js"]
"@welshman/net": ["../net/src/index.js"]
}
},