Remove relay package, move everything into net
This commit is contained in:
@@ -87,11 +87,6 @@ export default defineConfig({
|
||||
{text: "NIP 59", link: "/signer/nip-59"},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "@welshman/relay",
|
||||
link: "/relay/",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
text: "@welshman/router",
|
||||
link: "/router/",
|
||||
|
||||
@@ -7,9 +7,6 @@ Welshman is modular - install only what you need:
|
||||
# Core nostr utilities (events, filters, tags)
|
||||
npm i @welshman/util
|
||||
|
||||
# In-memory event store and relay adapter
|
||||
npm i @welshman/relay
|
||||
|
||||
# Networking and relay management
|
||||
npm i @welshman/net
|
||||
|
||||
|
||||
@@ -30,9 +30,6 @@ features:
|
||||
- title: "@welshman/signer"
|
||||
details: Implementations of various nostr signing methods (NIP-01, NIP-07, NIP-46, NIP-55).
|
||||
link: "/signer"
|
||||
- title: "@welshman/relay"
|
||||
details: In-memory relay and event store.
|
||||
link: "/relay"
|
||||
- title: "@welshman/router"
|
||||
details: Tools for relay selection.
|
||||
link: "/router"
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
# @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
|
||||
```
|
||||
@@ -42,7 +42,7 @@ Creates a cached loader function with staleness checking and exponential backoff
|
||||
import {writable} from 'svelte/store'
|
||||
import {derived, readable} from "svelte/store"
|
||||
import {readProfile, PROFILE, PublishedProfile} from "@welshman/util"
|
||||
import {Repository} from "@welshman/relay"
|
||||
import {Repository} from "@welshman/net"
|
||||
import {deriveEventsMapped, collection, withGetter} from "@welshman/store"
|
||||
|
||||
const repository = new Repository()
|
||||
|
||||
@@ -39,7 +39,7 @@ Creates a reactive store that tracks whether an event is deleted by address.
|
||||
## Example
|
||||
|
||||
```typescript
|
||||
import {Repository} from "@welshman/relay"
|
||||
import {Repository} from "@welshman/net"
|
||||
import {deriveEventsMapped, deriveEvents} from "@welshman/store"
|
||||
import {readProfile, PROFILE} from "@welshman/util"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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,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} = {}) =>
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"dependencies": {
|
||||
"@welshman/lib": "workspace:*",
|
||||
"@welshman/net": "workspace:*",
|
||||
"@welshman/relay": "workspace:*",
|
||||
"@welshman/router": "workspace:*",
|
||||
"@welshman/signer": "workspace:*",
|
||||
"@welshman/util": "workspace:*",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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
@@ -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,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 = {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
build
|
||||
normalize-url
|
||||
Negentropy.ts
|
||||
__tests__
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from "./relay.js"
|
||||
export * from "./repository.js"
|
||||
export * from "./tracker.js"
|
||||
export * from "./wrapManager.js"
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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/**/*"
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
"dependencies": {
|
||||
"@welshman/lib": "workspace:*",
|
||||
"@welshman/util": "workspace:*",
|
||||
"@welshman/relay": "workspace:*"
|
||||
"@welshman/net": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "~6.0.0",
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"dependencies": {
|
||||
"@welshman/lib": "workspace:*",
|
||||
"@welshman/util": "workspace:*",
|
||||
"@welshman/relay": "workspace:*",
|
||||
"@welshman/net": "workspace:*",
|
||||
"svelte": "^4.2.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Generated
+4
-32
@@ -74,9 +74,6 @@ importers:
|
||||
'@welshman/net':
|
||||
specifier: workspace:*
|
||||
version: link:../net
|
||||
'@welshman/relay':
|
||||
specifier: workspace:*
|
||||
version: link:../relay
|
||||
'@welshman/router':
|
||||
specifier: workspace:*
|
||||
version: link:../router
|
||||
@@ -194,9 +191,6 @@ importers:
|
||||
'@welshman/net':
|
||||
specifier: workspace:*
|
||||
version: link:../net
|
||||
'@welshman/relay':
|
||||
specifier: workspace:*
|
||||
version: link:../relay
|
||||
'@welshman/router':
|
||||
specifier: workspace:*
|
||||
version: link:../router
|
||||
@@ -241,9 +235,6 @@ importers:
|
||||
'@welshman/lib':
|
||||
specifier: workspace:*
|
||||
version: link:../lib
|
||||
'@welshman/relay':
|
||||
specifier: workspace:*
|
||||
version: link:../relay
|
||||
'@welshman/util':
|
||||
specifier: workspace:*
|
||||
version: link:../util
|
||||
@@ -261,33 +252,14 @@ importers:
|
||||
specifier: ~5.8.0
|
||||
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:
|
||||
dependencies:
|
||||
'@welshman/lib':
|
||||
specifier: workspace:*
|
||||
version: link:../lib
|
||||
'@welshman/relay':
|
||||
'@welshman/net':
|
||||
specifier: workspace:*
|
||||
version: link:../relay
|
||||
version: link:../net
|
||||
'@welshman/util':
|
||||
specifier: workspace:*
|
||||
version: link:../util
|
||||
@@ -338,9 +310,9 @@ importers:
|
||||
'@welshman/lib':
|
||||
specifier: workspace:*
|
||||
version: link:../lib
|
||||
'@welshman/relay':
|
||||
'@welshman/net':
|
||||
specifier: workspace:*
|
||||
version: link:../relay
|
||||
version: link:../net
|
||||
'@welshman/util':
|
||||
specifier: workspace:*
|
||||
version: link:../util
|
||||
|
||||
Reference in New Issue
Block a user