From cfd2e3aac76eb44c4e3c6eceed3c2c79ef663499 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Mon, 31 Mar 2025 10:16:28 -0700 Subject: [PATCH] Add relay package --- build_and_link.sh | 4 +- packages/app/src/core.ts | 3 +- packages/app/src/sync.ts | 19 ++---- packages/app/src/thunk.ts | 4 +- packages/app/src/zappers.ts | 9 +-- packages/net/package.json | 1 + packages/net/src/adapter.ts | 11 ++-- packages/relay/.eslintignore | 4 ++ packages/relay/README.md | 61 +++++++++++++++++ .../__tests__/relay.test.ts} | 0 .../__tests__/repository.test.ts} | 0 packages/relay/package.json | 32 +++++++++ packages/relay/src/index.ts | 2 + packages/relay/src/relay.ts | 56 ++++++++++++++++ .../Repository.ts => relay/src/repository.ts} | 43 +++++++++--- packages/relay/tsconfig.json | 14 ++++ packages/relay/typedoc.json | 3 + packages/store/package.json | 1 + packages/store/src/index.ts | 7 +- packages/util/src/Relay.ts | 65 +------------------ packages/util/src/index.ts | 1 - 21 files changed, 234 insertions(+), 106 deletions(-) create mode 100644 packages/relay/.eslintignore create mode 100644 packages/relay/README.md rename packages/{util/__tests__/Relay.test.ts => relay/__tests__/relay.test.ts} (100%) rename packages/{util/__tests__/Repository.test.ts => relay/__tests__/repository.test.ts} (100%) create mode 100644 packages/relay/package.json create mode 100644 packages/relay/src/index.ts create mode 100644 packages/relay/src/relay.ts rename packages/{util/src/Repository.ts => relay/src/repository.ts} (94%) create mode 100644 packages/relay/tsconfig.json create mode 100644 packages/relay/typedoc.json diff --git a/build_and_link.sh b/build_and_link.sh index f9cc374..71f252b 100755 --- a/build_and_link.sh +++ b/build_and_link.sh @@ -12,7 +12,7 @@ for downstream in $(./get_packages.py); do if [[ ! -z $v ]]; then mkdir -p packages/$downstream/node_modules/@welshman/$upstream - cp -r packages/$upstream/build packages/$downstream/node_modules/@welshman/$upstream > /dev/null 2>&1 - cp -r packages/$upstream/build node_modules/@welshman/$upstream > /dev/null 2>&1 + cp -r packages/$upstream/* packages/$downstream/node_modules/@welshman/$upstream > /dev/null 2>&1 + cp -r packages/$upstream/* node_modules/@welshman/$upstream > /dev/null 2>&1 fi done diff --git a/packages/app/src/core.ts b/packages/app/src/core.ts index 66c439f..668f7b0 100644 --- a/packages/app/src/core.ts +++ b/packages/app/src/core.ts @@ -1,10 +1,9 @@ import {throttle} from "@welshman/lib" import {Repository, Relay} from "@welshman/util" -import type {TrustedEvent} from "@welshman/util" import {Tracker} from "@welshman/net" import {custom} from "@welshman/store" -export const repository = new Repository() +export const repository = Repository.getSingleton() export const relay = new Relay(repository) diff --git a/packages/app/src/sync.ts b/packages/app/src/sync.ts index 66c84d3..baa2feb 100644 --- a/packages/app/src/sync.ts +++ b/packages/app/src/sync.ts @@ -35,7 +35,9 @@ export const pull = async ({relays, filters}: AppSyncOpts) => { relays.map(async relay => { await (hasNegentropy(relay) ? basePull({filters, events, relays: [relay]}) - : pullWithoutNegentropy({filters, relays: [relay]})) + : new Promise(resolve => { + new SingleRequest({filters, relay, closeOnEose: true}).on(RequestEvent.Close, resolve) + }) }), ) } @@ -47,19 +49,10 @@ export const push = async ({relays, filters}: AppSyncOpts) => { relays.map(async relay => { await (hasNegentropy(relay) ? basePush({filters, events, relays: [relay]}) - : pushWithoutNegentropy({events, relays: [relay]})) + : new Promise(resolve => { + new SinglePublish({events, relay}).on(PublishEvent.Complete, resolve) + })) }), ) -} -export const sync = async ({relays, filters}: AppSyncOpts) => { - const events = query(filters).filter(isSignedEvent) - await Promise.all( - relays.map(async relay => { - await (hasNegentropy(relay) - ? baseSync({filters, events, relays: [relay]}) - : syncWithoutNegentropy({filters, events, relays: [relay]})) - }), - ) -} diff --git a/packages/app/src/thunk.ts b/packages/app/src/thunk.ts index 695b272..99af692 100644 --- a/packages/app/src/thunk.ts +++ b/packages/app/src/thunk.ts @@ -18,7 +18,7 @@ import { isUnwrappedEvent, isSignedEvent, } from "@welshman/util" -import {publish, PublishStatus} from "@welshman/net" +import {MultiPublish, PublishStatus} from "@welshman/net" import {repository, tracker} from "./core.js" import {pubkey, getSession, getSigner} from "./session.js" @@ -225,7 +225,7 @@ thunkWorker.addGlobalHandler((thunk: Thunk) => { } // Send it off - const pub = publish({event: signedEvent, relays: thunk.request.relays}) + const pub = new MultiPublish({event: signedEvent, relays: thunk.request.relays}) // Copy the signature over since we had deferred it const savedEvent = repository.getEvent(signedEvent.id) as SignedEvent diff --git a/packages/app/src/zappers.ts b/packages/app/src/zappers.ts index 37267b3..1957f03 100644 --- a/packages/app/src/zappers.ts +++ b/packages/app/src/zappers.ts @@ -1,6 +1,6 @@ import {writable, derived} from "svelte/store" -import {type Zapper} from "@welshman/util" -import {type SubscribeRequestWithHandlers} from "@welshman/net" +import {Zapper} from "@welshman/util" +import {MultiRequestOptions} from "@welshman/net" import { ctx, identity, @@ -80,10 +80,7 @@ export const { }), }) -export const deriveZapperForPubkey = ( - pubkey: string, - request: Partial = {}, -) => +export const deriveZapperForPubkey = (pubkey: string, request: Partial = {}) => derived([zappersByLnurl, deriveProfile(pubkey, request)], ([$zappersByLnurl, $profile]) => { if (!$profile?.lnurl) { return undefined diff --git a/packages/net/package.json b/packages/net/package.json index b445183..89f4f3e 100644 --- a/packages/net/package.json +++ b/packages/net/package.json @@ -28,6 +28,7 @@ "dependencies": { "@welshman/lib": "^0.1.0", "@welshman/util": "^0.1.0", + "@welshman/relay": "^0.1.0", "isomorphic-ws": "^5.0.0", "nostr-tools": "^2.11.0", "typed-emitter": "^2.1.0" diff --git a/packages/net/src/adapter.ts b/packages/net/src/adapter.ts index f5ce979..83fcf86 100644 --- a/packages/net/src/adapter.ts +++ b/packages/net/src/adapter.ts @@ -1,6 +1,7 @@ import EventEmitter from "events" import {call, on} from "@welshman/lib" -import {Relay, LOCAL_RELAY_URL, isRelayUrl} from "@welshman/util" +import {isRelayUrl} from "@welshman/util" +import {LocalRelay, LOCAL_RELAY_URL, Repository} from "@welshman/relay" import {RelayMessage, ClientMessage} from "./message.js" import {Socket, SocketEvent} from "./socket.js" import {TypedEmitter, Unsubscriber} from "./util.js" @@ -52,7 +53,7 @@ export class SocketAdapter extends AbstractAdapter { } export class LocalAdapter extends AbstractAdapter { - constructor(readonly relay: Relay) { + constructor(readonly relay: LocalRelay) { super() this._unsubscribers.push( @@ -91,7 +92,7 @@ export class EmptyAdapter extends AbstractAdapter { export type AdapterContext = { pool?: Pool - relay?: Relay + relay?: LocalRelay getAdapter?: (url: string, context: AdapterContext) => AbstractAdapter } @@ -105,7 +106,9 @@ export const getAdapter = (url: string, context: AdapterContext = {}) => { } if (url === LOCAL_RELAY_URL) { - return context.relay ? new LocalAdapter(context.relay) : new EmptyAdapter() + const relay = context.relay || new LocalRelay(Repository.getSingleton()) + + return new LocalAdapter(relay) } if (isRelayUrl(url)) { diff --git a/packages/relay/.eslintignore b/packages/relay/.eslintignore new file mode 100644 index 0000000..22d79d5 --- /dev/null +++ b/packages/relay/.eslintignore @@ -0,0 +1,4 @@ +build +normalize-url +Negentropy.ts +__tests__ diff --git a/packages/relay/README.md b/packages/relay/README.md new file mode 100644 index 0000000..b543670 --- /dev/null +++ b/packages/relay/README.md @@ -0,0 +1,61 @@ +# @welshman/net [![version](https://badgen.net/npm/v/@welshman/net)](https://npmjs.com/package/@welshman/net) + +Utilities having to do with connection management and nostr messages. + +```typescript +import {ctx, setContext} from '@welshman/lib' +import {type TrustedEvent, createEvent, NOTE} from '@welshman/util' +import {subscribe, publish, getDefaultNetContext} from '@welshman/net' + +// Sets up customizable event valdation, handlers, etc +setContext(getDefaultNetContext()) + +// Send a subscription +const sub = subscribe({ + relays: ['wss://relay.example.com/'], + filters: [{kinds: [1], limit: 1}], + closeOnEose: true, + timeout: 10000, +}) + +sub.on(SubscriptionEvent.Event, (url: string, event: TrustedEvent) => { + console.log(url, event) + sub.close() +}) + +// Publish an event +const pub = publish({ + relays: ['wss://relay.example.com/'], + event: createEvent(NOTE, {content: 'hi'}), +}) + +pub.emitter.on('*', (status: PublishStatus, url: string) => { + console.log(status, url) +}) + +// The Tracker class can tell you which relays an event was read from or published to +console.log(ctx.net.tracker.getRelays(event.id)) +``` + +The main reason this module exists is to support different backends via Executor and different `target` classes. For example, to add a local relay that automatically gets used: + +```typescript +import {setContext} from '@welshman/lib' +import {LOCAL_RELAY_URL, Relay, Repository} from '@welshman/util' +import {getDefaultNetContext, Multi, Local, Relays, Executor} from '@welshman/net' + +const repository = new Repository() + +const relay = new Relay(repository) + +setContext(getDefaultNetContext({ + getExecutor: (relays: string[]) => { + return new Executor( + new Multi([ + new Local(relay), + new Relays(remoteUrls.map(url => ctx.net.pool.get(url))), + ]) + ) + }, +})) +``` diff --git a/packages/util/__tests__/Relay.test.ts b/packages/relay/__tests__/relay.test.ts similarity index 100% rename from packages/util/__tests__/Relay.test.ts rename to packages/relay/__tests__/relay.test.ts diff --git a/packages/util/__tests__/Repository.test.ts b/packages/relay/__tests__/repository.test.ts similarity index 100% rename from packages/util/__tests__/Repository.test.ts rename to packages/relay/__tests__/repository.test.ts diff --git a/packages/relay/package.json b/packages/relay/package.json new file mode 100644 index 0000000..a86ea88 --- /dev/null +++ b/packages/relay/package.json @@ -0,0 +1,32 @@ +{ + "name": "@welshman/relay", + "version": "0.1.0", + "author": "hodlbod", + "license": "MIT", + "description": "An in-memory nostr relay implementation.", + "publishConfig": { + "access": "public" + }, + "type": "module", + "files": [ + "build" + ], + "types": "./build/src/index.d.ts", + "exports": { + ".": { + "types": "./build/src/index.d.ts", + "import": "./build/src/index.js", + "require": "./build/src/index.js" + } + }, + "scripts": { + "pub": "npm run lint && npm run build && npm publish", + "build": "gts clean && tsc", + "lint": "gts lint", + "fix": "gts fix" + }, + "dependencies": { + "@welshman/lib": "^0.1.0", + "@welshman/util": "^0.1.0" + } +} diff --git a/packages/relay/src/index.ts b/packages/relay/src/index.ts new file mode 100644 index 0000000..d5a3a5f --- /dev/null +++ b/packages/relay/src/index.ts @@ -0,0 +1,2 @@ +export * from "./repository.js" +export * from "./relay.js" diff --git a/packages/relay/src/relay.ts b/packages/relay/src/relay.ts new file mode 100644 index 0000000..00a6144 --- /dev/null +++ b/packages/relay/src/relay.ts @@ -0,0 +1,56 @@ +import {Emitter, sleep} from "@welshman/lib" +import {Filter, TrustedEvent, HashedEvent, matchFilters} from "@welshman/util" +import {Repository} from "./repository.js" + +export class LocalRelay extends Emitter { + subs = new Map() + + constructor(readonly repository: Repository) { + super() + } + + send(type: string, ...message: any[]) { + switch (type) { + case "EVENT": + return this.handleEVENT(message as [E]) + case "CLOSE": + return this.handleCLOSE(message as [string]) + case "REQ": + return this.handleREQ(message as [string, ...Filter[]]) + } + } + + handleEVENT([event]: [E]) { + 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) + }) + } +} diff --git a/packages/util/src/Repository.ts b/packages/relay/src/repository.ts similarity index 94% rename from packages/util/src/Repository.ts rename to packages/relay/src/repository.ts index e232eb3..189fdbf 100644 --- a/packages/util/src/Repository.ts +++ b/packages/relay/src/repository.ts @@ -1,15 +1,34 @@ -import {flatten, pluck, Emitter, sortBy, inc, chunk, uniq, omit, now, range} from "@welshman/lib" -import {DELETE} from "./Kinds.js" -import {EPOCH, matchFilter} from "./Filters.js" -import {isReplaceable, isUnwrappedEvent} from "./Events.js" -import {getAddress} from "./Address.js" -import type {Filter} from "./Filters.js" -import type {TrustedEvent, HashedEvent} from "./Events.js" +import { + DAY, + Emitter, + flatten, + pluck, + sortBy, + inc, + chunk, + uniq, + omit, + now, + range, +} from "@welshman/lib" +import { + DELETE, + EPOCH, + matchFilter, + isReplaceable, + isUnwrappedEvent, + getAddress, + Filter, + TrustedEvent, + HashedEvent, +} from "@welshman/util" -export const DAY = 86400 +export const LOCAL_RELAY_URL = "local://welshman.relay/" const getDay = (ts: number) => Math.floor(ts / DAY) +export let repositorySingleton: Repository + export class Repository extends Emitter { eventsById = new Map() eventsByWrap = new Map() @@ -20,6 +39,14 @@ export class Repository extends Emitter { eventsByKind = new Map() deletes = new Map() + static getSingleton() { + if (!repositorySingleton) { + repositorySingleton = new Repository() + } + + return repositorySingleton + } + constructor() { super() diff --git a/packages/relay/tsconfig.json b/packages/relay/tsconfig.json new file mode 100644 index 0000000..97e6372 --- /dev/null +++ b/packages/relay/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../node_modules/gts/tsconfig-google.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "build", + "module": "nodenext", + "moduleResolution": "nodenext", + "lib": ["esnext", "dom"] + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/packages/relay/typedoc.json b/packages/relay/typedoc.json new file mode 100644 index 0000000..35fed2c --- /dev/null +++ b/packages/relay/typedoc.json @@ -0,0 +1,3 @@ +{ + "entryPoints": ["src/index.ts"] +} diff --git a/packages/store/package.json b/packages/store/package.json index b73c34a..c73a6d6 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -28,6 +28,7 @@ "dependencies": { "@welshman/lib": "^0.1.0", "@welshman/util": "^0.1.0", + "@welshman/relay": "^0.1.0", "svelte": "^4.2.18" } } diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 7c03d52..238c0b1 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -10,10 +10,9 @@ import { partition, first, } from "@welshman/lib" -import type {Maybe} from "@welshman/lib" -import type {Repository} from "@welshman/util" -import {matchFilters, getIdAndAddress, getIdFilters} from "@welshman/util" -import type {Filter, TrustedEvent} from "@welshman/util" +import {Maybe} from "@welshman/lib" +import {Repository} from "@welshman/relay" +import {matchFilters, getIdAndAddress, getIdFilters, Filter, TrustedEvent} from "@welshman/util" // Sync with localstorage diff --git a/packages/util/src/Relay.ts b/packages/util/src/Relay.ts index 6569970..418a45f 100644 --- a/packages/util/src/Relay.ts +++ b/packages/util/src/Relay.ts @@ -1,15 +1,7 @@ -import {last, Emitter, normalizeUrl, sleep, stripProtocol} from "@welshman/lib" -import {matchFilters} from "./Filters.js" -import type {Repository} from "./Repository.js" -import type {Filter} from "./Filters.js" -import type {HashedEvent, TrustedEvent} from "./Events.js" +import {last, normalizeUrl, stripProtocol} from "@welshman/lib" // Constants and types -export const LOCAL_RELAY_URL = "local://welshman.relay/" - -export const BOGUS_RELAY_URL = "bogus://welshman.relay/" - export type RelayProfile = { url: string icon?: string @@ -83,58 +75,3 @@ export const displayRelayUrl = (url: string) => last(url.split("://")).replace(/ export const displayRelayProfile = (profile?: RelayProfile, fallback = "") => profile?.name || fallback - -// In-memory relay implementation backed by Repository - -export class Relay extends Emitter { - subs = new Map() - - constructor(readonly repository: Repository) { - super() - } - - send(type: string, ...message: any[]) { - switch (type) { - case "EVENT": - return this.handleEVENT(message as [E]) - case "CLOSE": - return this.handleCLOSE(message as [string]) - case "REQ": - return this.handleREQ(message as [string, ...Filter[]]) - } - } - - handleEVENT([event]: [E]) { - 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) - }) - } -} diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index cb2a4c6..1f1ed02 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -8,6 +8,5 @@ export * from "./Links.js" export * from "./List.js" export * from "./Profile.js" export * from "./Relay.js" -export * from "./Repository.js" export * from "./Tags.js" export * from "./Zaps.js"