Add relay package
This commit is contained in:
+2
-2
@@ -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
|
||||
|
||||
@@ -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<TrustedEvent>()
|
||||
export const repository = Repository.getSingleton()
|
||||
|
||||
export const relay = new Relay(repository)
|
||||
|
||||
|
||||
@@ -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]}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<SubscribeRequestWithHandlers> = {},
|
||||
) =>
|
||||
export const deriveZapperForPubkey = (pubkey: string, request: Partial<MultiRequestOptions> = {}) =>
|
||||
derived([zappersByLnurl, deriveProfile(pubkey, request)], ([$zappersByLnurl, $profile]) => {
|
||||
if (!$profile?.lnurl) {
|
||||
return undefined
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
build
|
||||
normalize-url
|
||||
Negentropy.ts
|
||||
__tests__
|
||||
@@ -0,0 +1,61 @@
|
||||
# @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))),
|
||||
])
|
||||
)
|
||||
},
|
||||
}))
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./repository.js"
|
||||
export * from "./relay.js"
|
||||
@@ -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<E extends HashedEvent = TrustedEvent> extends Emitter {
|
||||
subs = new Map<string, Filter[]>()
|
||||
|
||||
constructor(readonly repository: Repository<E>) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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<TrustedEvent>
|
||||
|
||||
export class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
|
||||
eventsById = new Map<string, E>()
|
||||
eventsByWrap = new Map<string, E>()
|
||||
@@ -20,6 +39,14 @@ export class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
|
||||
eventsByKind = new Map<number, E[]>()
|
||||
deletes = new Map<string, number>()
|
||||
|
||||
static getSingleton() {
|
||||
if (!repositorySingleton) {
|
||||
repositorySingleton = new Repository()
|
||||
}
|
||||
|
||||
return repositorySingleton
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"entryPoints": ["src/index.ts"]
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
"dependencies": {
|
||||
"@welshman/lib": "^0.1.0",
|
||||
"@welshman/util": "^0.1.0",
|
||||
"@welshman/relay": "^0.1.0",
|
||||
"svelte": "^4.2.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<E extends HashedEvent = TrustedEvent> extends Emitter {
|
||||
subs = new Map<string, Filter[]>()
|
||||
|
||||
constructor(readonly repository: Repository<E>) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user