Auto register client plugins
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import {BLOCKED_RELAYS, asDecryptedEvent, readList, getRelaysFromList} from "@welshman/util"
|
import {BLOCKED_RELAYS, asDecryptedEvent, readList, getRelaysFromList} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import {RelayLists} from "./relayLists.js"
|
||||||
import type {RelayLists} from "./relayLists.js"
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kind-10006 blocked-relay lists, keyed by pubkey. Loaded via the outbox model,
|
* Kind-10006 blocked-relay lists, keyed by pubkey. Loaded via the outbox model,
|
||||||
@@ -10,10 +10,7 @@ import type {RelayLists} from "./relayLists.js"
|
|||||||
* blocked relays are never selected.
|
* blocked relays are never selected.
|
||||||
*/
|
*/
|
||||||
export class BlockedRelayLists extends RepositoryCollection<ReturnType<typeof readList>> {
|
export class BlockedRelayLists extends RepositoryCollection<ReturnType<typeof readList>> {
|
||||||
constructor(
|
constructor(ctx: IClient) {
|
||||||
ctx: ClientContext,
|
|
||||||
readonly relayLists: RelayLists,
|
|
||||||
) {
|
|
||||||
super(ctx, {
|
super(ctx, {
|
||||||
filters: [{kinds: [BLOCKED_RELAYS]}],
|
filters: [{kinds: [BLOCKED_RELAYS]}],
|
||||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||||
@@ -22,7 +19,7 @@ export class BlockedRelayLists extends RepositoryCollection<ReturnType<typeof re
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetch(pubkey: string, relayHints: string[] = []) {
|
fetch(pubkey: string, relayHints: string[] = []) {
|
||||||
return this.relayLists.makeOutboxLoader(BLOCKED_RELAYS)(pubkey, relayHints)
|
return this.ctx.use(RelayLists).makeOutboxLoader(BLOCKED_RELAYS)(pubkey, relayHints)
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlockedRelays = (pubkey: string) => getRelaysFromList(this.get(pubkey))
|
getBlockedRelays = (pubkey: string) => getRelaysFromList(this.get(pubkey))
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import {BLOSSOM_SERVERS, asDecryptedEvent, readList} from "@welshman/util"
|
import {BLOSSOM_SERVERS, asDecryptedEvent, readList} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import {RelayLists} from "./relayLists.js"
|
||||||
import type {RelayLists} from "./relayLists.js"
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blossom server lists (kind 10063), keyed by pubkey. Loaded via the outbox
|
* Blossom server lists (kind 10063), keyed by pubkey. Loaded via the outbox
|
||||||
* model (the author's write relays), so it depends on the relay-list collection.
|
* model (the author's write relays), so it depends on the relay-list collection.
|
||||||
*/
|
*/
|
||||||
export class BlossomServerLists extends RepositoryCollection<ReturnType<typeof readList>> {
|
export class BlossomServerLists extends RepositoryCollection<ReturnType<typeof readList>> {
|
||||||
constructor(
|
constructor(ctx: IClient) {
|
||||||
ctx: ClientContext,
|
|
||||||
readonly relayLists: RelayLists,
|
|
||||||
) {
|
|
||||||
super(ctx, {
|
super(ctx, {
|
||||||
filters: [{kinds: [BLOSSOM_SERVERS]}],
|
filters: [{kinds: [BLOSSOM_SERVERS]}],
|
||||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||||
@@ -21,6 +18,6 @@ export class BlossomServerLists extends RepositoryCollection<ReturnType<typeof r
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetch(pubkey: string, relayHints: string[] = []) {
|
fetch(pubkey: string, relayHints: string[] = []) {
|
||||||
return this.relayLists.makeOutboxLoader(BLOSSOM_SERVERS)(pubkey, relayHints)
|
return this.ctx.use(RelayLists).makeOutboxLoader(BLOSSOM_SERVERS)(pubkey, relayHints)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-173
@@ -1,131 +1,60 @@
|
|||||||
import type {Readable, Unsubscriber} from "svelte/store"
|
import type {Unsubscriber} from "svelte/store"
|
||||||
import {isDVMKind, isEphemeralKind} from "@welshman/util"
|
import {call} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import {Pool, Socket, Tracker, Repository, WrapManager, defaultSocketPolicies} from "@welshman/net"
|
||||||
import {
|
import type {AdapterContext, AdapterFactory, SocketPolicy} from "@welshman/net"
|
||||||
Pool,
|
|
||||||
Socket,
|
|
||||||
SocketEvent,
|
|
||||||
Tracker,
|
|
||||||
Repository,
|
|
||||||
WrapManager,
|
|
||||||
defaultSocketPolicies,
|
|
||||||
netContext,
|
|
||||||
isRelayEvent,
|
|
||||||
request,
|
|
||||||
publish,
|
|
||||||
diff,
|
|
||||||
pull,
|
|
||||||
push,
|
|
||||||
makeLoader,
|
|
||||||
} from "@welshman/net"
|
|
||||||
import type {
|
|
||||||
AdapterContext,
|
|
||||||
AdapterFactory,
|
|
||||||
SocketPolicy,
|
|
||||||
RelayMessage,
|
|
||||||
Loader,
|
|
||||||
LoaderOptions,
|
|
||||||
RequestOptions,
|
|
||||||
PublishOptions,
|
|
||||||
DiffOptions,
|
|
||||||
PullOptions,
|
|
||||||
PushOptions,
|
|
||||||
} from "@welshman/net"
|
|
||||||
import {
|
|
||||||
getEventsById,
|
|
||||||
deriveEventsById,
|
|
||||||
deriveEvents,
|
|
||||||
makeDeriveEvent,
|
|
||||||
getEventsByIdByUrl,
|
|
||||||
deriveEventsByIdByUrl,
|
|
||||||
getEventsByIdForUrl,
|
|
||||||
deriveEventsByIdForUrl,
|
|
||||||
deriveItemsByKey,
|
|
||||||
deriveIsDeleted,
|
|
||||||
} from "@welshman/store"
|
|
||||||
import type {
|
|
||||||
EventsByIdOptions,
|
|
||||||
EventOptions,
|
|
||||||
EventsByIdByUrlOptions,
|
|
||||||
EventsByIdForUrlOptions,
|
|
||||||
ItemsByKey,
|
|
||||||
ItemsByKeyOptions,
|
|
||||||
} from "@welshman/store"
|
|
||||||
import type {User} from "./user.js"
|
import type {User} from "./user.js"
|
||||||
|
import type {ClientPolicy} from "./policies.js"
|
||||||
|
|
||||||
|
export type ClientConfig = {
|
||||||
|
dufflepudUrl?: string
|
||||||
|
getDefaultRelays?: () => string[]
|
||||||
|
getIndexerRelays?: () => string[]
|
||||||
|
getSearchRelays?: () => string[]
|
||||||
|
}
|
||||||
|
|
||||||
export type ClientOptions = {
|
export type ClientOptions = {
|
||||||
user?: User
|
user?: User
|
||||||
|
config?: ClientConfig
|
||||||
getAdapter?: AdapterFactory
|
getAdapter?: AdapterFactory
|
||||||
socketPolicies?: SocketPolicy[]
|
socketPolicies?: SocketPolicy[]
|
||||||
|
policies?: ClientPolicy[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export interface IClient {
|
||||||
* The narrow seam that data modules (plugins) depend on instead of the concrete
|
|
||||||
* `Client`. Because plugins reference only `ClientContext`, the client never
|
|
||||||
* imports them and they never create a dependency cycle back to the client —
|
|
||||||
* the dependency is strictly one-way (plugin -> context).
|
|
||||||
*
|
|
||||||
* `Client implements ClientContext`, so the two stay in sync by construction.
|
|
||||||
*/
|
|
||||||
export interface ClientContext {
|
|
||||||
user?: User
|
user?: User
|
||||||
|
config: ClientConfig
|
||||||
|
use: <T>(Ctor: new (ctx: IClient) => T) => T
|
||||||
|
netContext: AdapterContext
|
||||||
pool: Pool
|
pool: Pool
|
||||||
tracker: Tracker
|
tracker: Tracker
|
||||||
repository: Repository
|
repository: Repository
|
||||||
wrapManager: WrapManager
|
wrapManager: WrapManager
|
||||||
|
|
||||||
// Net utilities, with this client's context baked in
|
|
||||||
request: (options: Omit<RequestOptions, "context">) => ReturnType<typeof request>
|
|
||||||
publish: (options: Omit<PublishOptions, "context">) => ReturnType<typeof publish>
|
|
||||||
diff: (options: Omit<DiffOptions, "context">) => ReturnType<typeof diff>
|
|
||||||
pull: (options: Omit<PullOptions, "context">) => ReturnType<typeof pull>
|
|
||||||
push: (options: Omit<PushOptions, "context">) => ReturnType<typeof push>
|
|
||||||
makeLoader: (options: Omit<LoaderOptions, "context">) => Loader
|
|
||||||
load: Loader
|
|
||||||
|
|
||||||
// Store utilities, with this client's repository/tracker baked in
|
|
||||||
getEventsById: (options: Omit<EventsByIdOptions, "repository">) => ReturnType<typeof getEventsById>
|
|
||||||
deriveEventsById: (
|
|
||||||
options: Omit<EventsByIdOptions, "repository">,
|
|
||||||
) => ReturnType<typeof deriveEventsById>
|
|
||||||
deriveEvents: (options: Omit<EventsByIdOptions, "repository">) => ReturnType<typeof deriveEvents>
|
|
||||||
makeDeriveEvent: (options: Omit<EventOptions, "repository">) => ReturnType<typeof makeDeriveEvent>
|
|
||||||
getEventsByIdByUrl: (
|
|
||||||
options: Omit<EventsByIdByUrlOptions, "tracker" | "repository">,
|
|
||||||
) => ReturnType<typeof getEventsByIdByUrl>
|
|
||||||
deriveEventsByIdByUrl: (
|
|
||||||
options: Omit<EventsByIdByUrlOptions, "tracker" | "repository">,
|
|
||||||
) => ReturnType<typeof deriveEventsByIdByUrl>
|
|
||||||
getEventsByIdForUrl: (
|
|
||||||
options: Omit<EventsByIdForUrlOptions, "tracker" | "repository">,
|
|
||||||
) => ReturnType<typeof getEventsByIdForUrl>
|
|
||||||
deriveEventsByIdForUrl: (
|
|
||||||
options: Omit<EventsByIdForUrlOptions, "tracker" | "repository">,
|
|
||||||
) => ReturnType<typeof deriveEventsByIdForUrl>
|
|
||||||
deriveItemsByKey: <T>(options: Omit<ItemsByKeyOptions<T>, "repository">) => Readable<ItemsByKey<T>>
|
|
||||||
deriveIsDeleted: (event: TrustedEvent) => ReturnType<typeof deriveIsDeleted>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The core of an application instance. Owns the primitives a single identity
|
* The core of an application instance. Owns the primitives a single identity
|
||||||
* needs (so data never bleeds across sessions) — a private repository, a socket
|
* needs (so data never bleeds across sessions) — a private repository, a socket
|
||||||
* pool, a tracker, a wrap manager — plus net/store helpers bound to them.
|
* pool, a tracker, a wrap manager — and a `use` registry that resolves data
|
||||||
*
|
* modules (including net/store helpers) on demand.
|
||||||
* Data modules are NOT fields on the client; they are composed separately (see
|
|
||||||
* `createApp`) and reach the client through the `ClientContext` seam.
|
|
||||||
*/
|
*/
|
||||||
export class Client implements ClientContext {
|
export class Client implements IClient {
|
||||||
user?: User
|
user?: User
|
||||||
|
config: ClientConfig
|
||||||
|
netContext: AdapterContext
|
||||||
pool: Pool
|
pool: Pool
|
||||||
tracker: Tracker
|
tracker: Tracker
|
||||||
repository: Repository
|
repository: Repository
|
||||||
wrapManager: WrapManager
|
wrapManager: WrapManager
|
||||||
netContext: AdapterContext
|
|
||||||
load: Loader
|
// Per-client singletons of data modules, keyed by constructor. Owned by the
|
||||||
ingestCleanup: Unsubscriber
|
// client (so it's GC'd with the client — no WeakMap needed), this is what
|
||||||
|
// `use` memoizes against.
|
||||||
|
private singletons = new Map<Function, unknown>()
|
||||||
|
private policyCleanups: Unsubscriber[] = []
|
||||||
|
|
||||||
constructor(options: ClientOptions = {}) {
|
constructor(options: ClientOptions = {}) {
|
||||||
this.user = options.user
|
this.user = options.user
|
||||||
|
this.config = options.config ?? {}
|
||||||
this.pool = new Pool({
|
this.pool = new Pool({
|
||||||
makeSocket: (url: string) => {
|
makeSocket: (url: string) => {
|
||||||
let socketPolicies = options.socketPolicies ?? defaultSocketPolicies
|
let socketPolicies = options.socketPolicies ?? defaultSocketPolicies
|
||||||
@@ -148,89 +77,30 @@ export class Client implements ClientContext {
|
|||||||
repository: this.repository,
|
repository: this.repository,
|
||||||
getAdapter: options.getAdapter,
|
getAdapter: options.getAdapter,
|
||||||
}
|
}
|
||||||
this.load = this.makeLoader({
|
|
||||||
delay: 200,
|
|
||||||
timeout: 3000,
|
|
||||||
threshold: 0.5,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Ingest every event received on any socket into this client's repository.
|
// Apply policies last, once the primitives and `use` registry exist. They
|
||||||
// The net layer doesn't do this for us, and it's how all the repository-
|
// own all side effects; their cleanups run on `cleanup()`.
|
||||||
// backed collections (and gift-wrap unwrapping) get populated.
|
this.policyCleanups = (options.policies ?? []).map(policy => policy(this))
|
||||||
this.ingestCleanup = this.pool.subscribe(socket => {
|
|
||||||
const onReceive = (message: RelayMessage) => this.ingest(message, socket.url)
|
|
||||||
|
|
||||||
socket.on(SocketEvent.Receive, onReceive)
|
|
||||||
|
|
||||||
return () => socket.off(SocketEvent.Receive, onReceive)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ingest = (message: RelayMessage, url: string) => {
|
// Resolve the per-client singleton of a data module, constructing it on first
|
||||||
if (!isRelayEvent(message)) return
|
// use. This is how modules reach their dependencies (e.g. ctx.use(RelayLists)),
|
||||||
|
// replacing constructor injection and letting cycles resolve lazily.
|
||||||
|
use = <T>(Ctor: new (ctx: IClient) => T): T => {
|
||||||
|
let instance = this.singletons.get(Ctor) as T | undefined
|
||||||
|
|
||||||
const event = message[2]
|
if (!instance) {
|
||||||
|
this.singletons.set(Ctor, (instance = new Ctor(this)))
|
||||||
|
}
|
||||||
|
|
||||||
if (isDVMKind(event.kind) || isEphemeralKind(event.kind)) return
|
return instance
|
||||||
if (!netContext.isEventValid(event, url)) return
|
|
||||||
|
|
||||||
this.tracker.track(event.id, url)
|
|
||||||
this.repository.publish(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
this.ingestCleanup()
|
this.policyCleanups.forEach(call)
|
||||||
this.pool.clear()
|
this.pool.clear()
|
||||||
this.tracker.clear()
|
this.tracker.clear()
|
||||||
this.repository.clear()
|
this.repository.clear()
|
||||||
this.wrapManager.clear()
|
this.wrapManager.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Net utilities, bound to this client's context
|
|
||||||
|
|
||||||
request = (options: Omit<RequestOptions, "context">) =>
|
|
||||||
request({...options, context: this.netContext})
|
|
||||||
|
|
||||||
publish = (options: Omit<PublishOptions, "context">) =>
|
|
||||||
publish({...options, context: this.netContext})
|
|
||||||
|
|
||||||
diff = (options: Omit<DiffOptions, "context">) => diff({...options, context: this.netContext})
|
|
||||||
|
|
||||||
pull = (options: Omit<PullOptions, "context">) => pull({...options, context: this.netContext})
|
|
||||||
|
|
||||||
push = (options: Omit<PushOptions, "context">) => push({...options, context: this.netContext})
|
|
||||||
|
|
||||||
makeLoader = (options: Omit<LoaderOptions, "context">): Loader =>
|
|
||||||
makeLoader({...options, context: this.netContext})
|
|
||||||
|
|
||||||
// Store utilities, bound to this client's repository/tracker
|
|
||||||
|
|
||||||
getEventsById = (options: Omit<EventsByIdOptions, "repository">) =>
|
|
||||||
getEventsById({...options, repository: this.repository})
|
|
||||||
|
|
||||||
deriveEventsById = (options: Omit<EventsByIdOptions, "repository">) =>
|
|
||||||
deriveEventsById({...options, repository: this.repository})
|
|
||||||
|
|
||||||
deriveEvents = (options: Omit<EventsByIdOptions, "repository">) =>
|
|
||||||
deriveEvents({...options, repository: this.repository})
|
|
||||||
|
|
||||||
makeDeriveEvent = (options: Omit<EventOptions, "repository">) =>
|
|
||||||
makeDeriveEvent({...options, repository: this.repository})
|
|
||||||
|
|
||||||
getEventsByIdByUrl = (options: Omit<EventsByIdByUrlOptions, "tracker" | "repository">) =>
|
|
||||||
getEventsByIdByUrl({...options, tracker: this.tracker, repository: this.repository})
|
|
||||||
|
|
||||||
deriveEventsByIdByUrl = (options: Omit<EventsByIdByUrlOptions, "tracker" | "repository">) =>
|
|
||||||
deriveEventsByIdByUrl({...options, tracker: this.tracker, repository: this.repository})
|
|
||||||
|
|
||||||
getEventsByIdForUrl = (options: Omit<EventsByIdForUrlOptions, "tracker" | "repository">) =>
|
|
||||||
getEventsByIdForUrl({...options, tracker: this.tracker, repository: this.repository})
|
|
||||||
|
|
||||||
deriveEventsByIdForUrl = (options: Omit<EventsByIdForUrlOptions, "tracker" | "repository">) =>
|
|
||||||
deriveEventsByIdForUrl({...options, tracker: this.tracker, repository: this.repository})
|
|
||||||
|
|
||||||
deriveItemsByKey = <T>(options: Omit<ItemsByKeyOptions<T>, "repository">) =>
|
|
||||||
deriveItemsByKey<T>({...options, repository: this.repository})
|
|
||||||
|
|
||||||
deriveIsDeleted = (event: TrustedEvent) => deriveIsDeleted(this.repository, event)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type {Readable, Unsubscriber} from "svelte/store"
|
|||||||
import type {Maybe} from "@welshman/lib"
|
import type {Maybe} from "@welshman/lib"
|
||||||
import {getter, makeDeriveItem, makeLoadItem, makeForceLoadItem} from "@welshman/store"
|
import {getter, makeDeriveItem, makeLoadItem, makeForceLoadItem} from "@welshman/store"
|
||||||
import type {MakeLoadItemOptions} from "@welshman/store"
|
import type {MakeLoadItemOptions} from "@welshman/store"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for a reactive, keyed collection of "local" (non-event) data —
|
* Base class for a reactive, keyed collection of "local" (non-event) data —
|
||||||
@@ -11,7 +11,7 @@ import type {ClientContext} from "./client.js"
|
|||||||
* repository. The collection owns its own map and is its own Svelte store: its
|
* repository. The collection owns its own map and is its own Svelte store: its
|
||||||
* `subscribe` emits the underlying `Map`.
|
* `subscribe` emits the underlying `Map`.
|
||||||
*
|
*
|
||||||
* Subclasses reach the client through the `ClientContext` seam, never the
|
* Subclasses reach the client through the `IClient` seam, never the
|
||||||
* concrete `Client`, so they never create a dependency cycle.
|
* concrete `Client`, so they never create a dependency cycle.
|
||||||
*/
|
*/
|
||||||
export class ClientData<T> {
|
export class ClientData<T> {
|
||||||
@@ -20,7 +20,7 @@ export class ClientData<T> {
|
|||||||
protected itemSubscribers: ((key: string, value: Maybe<T>) => void)[] = []
|
protected itemSubscribers: ((key: string, value: Maybe<T>) => void)[] = []
|
||||||
public derive: (key?: string, ...args: any[]) => Readable<Maybe<T>>
|
public derive: (key?: string, ...args: any[]) => Readable<Maybe<T>>
|
||||||
|
|
||||||
constructor(protected readonly ctx: ClientContext) {
|
constructor(protected readonly ctx: IClient) {
|
||||||
this.derive = makeDeriveItem(this.index)
|
this.derive = makeDeriveItem(this.index)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ export abstract class LoadableData<T> extends ClientData<T> {
|
|||||||
|
|
||||||
abstract fetch(key: string, ...args: any[]): Promise<unknown>
|
abstract fetch(key: string, ...args: any[]): Promise<unknown>
|
||||||
|
|
||||||
constructor(ctx: ClientContext, options: MakeLoadItemOptions = {}) {
|
constructor(ctx: IClient, options: MakeLoadItemOptions = {}) {
|
||||||
super(ctx)
|
super(ctx)
|
||||||
|
|
||||||
// Subclasses implement `fetch` as an arrow field, whose initializer runs
|
// Subclasses implement `fetch` as an arrow field, whose initializer runs
|
||||||
|
|||||||
@@ -33,32 +33,18 @@ import {
|
|||||||
prep,
|
prep,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {ManagementRequest, EventTemplate, RoomMeta, Profile} from "@welshman/util"
|
import type {ManagementRequest, EventTemplate, RoomMeta, Profile} from "@welshman/util"
|
||||||
import {addMaximalFallbacks} from "./router.js"
|
import {addMaximalFallbacks, Router} from "./router.js"
|
||||||
import type {Router} from "./router.js"
|
|
||||||
import {MergedThunk, publishThunk} from "./thunk.js"
|
import {MergedThunk, publishThunk} from "./thunk.js"
|
||||||
import type {ThunkOptions} from "./thunk.js"
|
import type {ThunkOptions} from "./thunk.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
import type {User} from "./user.js"
|
import type {User} from "./user.js"
|
||||||
import type {RelayLists} from "./relayLists.js"
|
import {RelayLists} from "./relayLists.js"
|
||||||
import type {MessagingRelayLists} from "./messagingRelayLists.js"
|
import {MessagingRelayLists} from "./messagingRelayLists.js"
|
||||||
import type {BlockedRelayLists} from "./blockedRelayLists.js"
|
import {BlockedRelayLists} from "./blockedRelayLists.js"
|
||||||
import type {SearchRelayLists} from "./searchRelayLists.js"
|
import {SearchRelayLists} from "./searchRelayLists.js"
|
||||||
import type {FollowLists} from "./follows.js"
|
import {FollowLists} from "./follows.js"
|
||||||
import type {MuteLists} from "./mutes.js"
|
import {MuteLists} from "./mutes.js"
|
||||||
import type {PinLists} from "./pins.js"
|
import {PinLists} from "./pins.js"
|
||||||
|
|
||||||
export type CommandsDeps = {
|
|
||||||
client: ClientContext
|
|
||||||
user: User
|
|
||||||
router: Router
|
|
||||||
relayLists: RelayLists
|
|
||||||
messagingRelayLists: MessagingRelayLists
|
|
||||||
blockedRelayLists: BlockedRelayLists
|
|
||||||
searchRelayLists: SearchRelayLists
|
|
||||||
followLists: FollowLists
|
|
||||||
muteLists: MuteLists
|
|
||||||
pinLists: PinLists
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SendWrappedOptions = Omit<
|
export type SendWrappedOptions = Omit<
|
||||||
ThunkOptions,
|
ThunkOptions,
|
||||||
@@ -70,37 +56,54 @@ export type SendWrappedOptions = Omit<
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The high-level "do an action" API: each method builds an event for the
|
* The high-level "do an action" API: each method builds an event for the
|
||||||
* client's user and publishes it via a thunk. Replaces the old module of global
|
* client's user and publishes it via a thunk. Siblings (the user's lists, the
|
||||||
* functions; everything that used a global (current pubkey, signer, router, the
|
* router) are resolved lazily through `ctx.use`; the acting user is `ctx.user`.
|
||||||
* user's lists) is now injected.
|
|
||||||
*/
|
*/
|
||||||
export class Commands {
|
export class Commands {
|
||||||
readonly client: ClientContext
|
constructor(readonly ctx: IClient) {}
|
||||||
readonly user: User
|
|
||||||
readonly router: Router
|
|
||||||
readonly relayLists: RelayLists
|
|
||||||
readonly messagingRelayLists: MessagingRelayLists
|
|
||||||
readonly blockedRelayLists: BlockedRelayLists
|
|
||||||
readonly searchRelayLists: SearchRelayLists
|
|
||||||
readonly followLists: FollowLists
|
|
||||||
readonly muteLists: MuteLists
|
|
||||||
readonly pinLists: PinLists
|
|
||||||
|
|
||||||
constructor(deps: CommandsDeps) {
|
private get user(): User {
|
||||||
this.client = deps.client
|
if (!this.ctx.user) {
|
||||||
this.user = deps.user
|
throw new Error("Commands require a signed-in user")
|
||||||
this.router = deps.router
|
}
|
||||||
this.relayLists = deps.relayLists
|
|
||||||
this.messagingRelayLists = deps.messagingRelayLists
|
return this.ctx.user
|
||||||
this.blockedRelayLists = deps.blockedRelayLists
|
}
|
||||||
this.searchRelayLists = deps.searchRelayLists
|
|
||||||
this.followLists = deps.followLists
|
private get router() {
|
||||||
this.muteLists = deps.muteLists
|
return this.ctx.use(Router)
|
||||||
this.pinLists = deps.pinLists
|
}
|
||||||
|
|
||||||
|
private get relayLists() {
|
||||||
|
return this.ctx.use(RelayLists)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get messagingRelayLists() {
|
||||||
|
return this.ctx.use(MessagingRelayLists)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get blockedRelayLists() {
|
||||||
|
return this.ctx.use(BlockedRelayLists)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get searchRelayLists() {
|
||||||
|
return this.ctx.use(SearchRelayLists)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get followLists() {
|
||||||
|
return this.ctx.use(FollowLists)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get muteLists() {
|
||||||
|
return this.ctx.use(MuteLists)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get pinLists() {
|
||||||
|
return this.ctx.use(PinLists)
|
||||||
}
|
}
|
||||||
|
|
||||||
private publish = (options: Omit<ThunkOptions, "client" | "user">) =>
|
private publish = (options: Omit<ThunkOptions, "client" | "user">) =>
|
||||||
publishThunk({...options, client: this.client, user: this.user})
|
publishThunk({...options, client: this.ctx, user: this.user})
|
||||||
|
|
||||||
private fromUser = () => this.router.FromUser().policy(addMaximalFallbacks).getUrls()
|
private fromUser = () => this.router.FromUser().policy(addMaximalFallbacks).getUrls()
|
||||||
|
|
||||||
@@ -149,7 +152,9 @@ export class Commands {
|
|||||||
|
|
||||||
setReadRelays = async (urls: string[]) => {
|
setReadRelays = async (urls: string[]) => {
|
||||||
const list = (await this.relayLists.forceLoad(this.user.pubkey, [])) || makeList({kind: RELAYS})
|
const list = (await this.relayLists.forceLoad(this.user.pubkey, [])) || makeList({kind: RELAYS})
|
||||||
const writeRelays = reject(nthEq(2, RelayMode.Read), getRelayTags(getListTags(list))).map(nth(1))
|
const writeRelays = reject(nthEq(2, RelayMode.Read), getRelayTags(getListTags(list))).map(
|
||||||
|
nth(1),
|
||||||
|
)
|
||||||
const writeTags = writeRelays.map(url => ["r", url, RelayMode.Write])
|
const writeTags = writeRelays.map(url => ["r", url, RelayMode.Write])
|
||||||
const readTags = urls.map(url => ["r", url, RelayMode.Read])
|
const readTags = urls.map(url => ["r", url, RelayMode.Read])
|
||||||
const tags = [...writeTags, ...readTags]
|
const tags = [...writeTags, ...readTags]
|
||||||
@@ -160,7 +165,9 @@ export class Commands {
|
|||||||
|
|
||||||
setWriteRelays = async (urls: string[]) => {
|
setWriteRelays = async (urls: string[]) => {
|
||||||
const list = (await this.relayLists.forceLoad(this.user.pubkey, [])) || makeList({kind: RELAYS})
|
const list = (await this.relayLists.forceLoad(this.user.pubkey, [])) || makeList({kind: RELAYS})
|
||||||
const readRelays = reject(nthEq(2, RelayMode.Write), getRelayTags(getListTags(list))).map(nth(1))
|
const readRelays = reject(nthEq(2, RelayMode.Write), getRelayTags(getListTags(list))).map(
|
||||||
|
nth(1),
|
||||||
|
)
|
||||||
const readTags = readRelays.map(url => ["r", url, RelayMode.Read])
|
const readTags = readRelays.map(url => ["r", url, RelayMode.Read])
|
||||||
const writeTags = urls.map(url => ["r", url, RelayMode.Write])
|
const writeTags = urls.map(url => ["r", url, RelayMode.Write])
|
||||||
const tags = [...readTags, ...writeTags]
|
const tags = [...readTags, ...writeTags]
|
||||||
@@ -259,14 +266,16 @@ export class Commands {
|
|||||||
// NIP 02
|
// NIP 02
|
||||||
|
|
||||||
unfollow = async (value: string) => {
|
unfollow = async (value: string) => {
|
||||||
const list = (await this.followLists.forceLoad(this.user.pubkey, [])) || makeList({kind: FOLLOWS})
|
const list =
|
||||||
|
(await this.followLists.forceLoad(this.user.pubkey, [])) || makeList({kind: FOLLOWS})
|
||||||
const event = await removeFromList(list, value).reconcile(this.encryptToSelf)
|
const event = await removeFromList(list, value).reconcile(this.encryptToSelf)
|
||||||
|
|
||||||
return this.publish({event, relays: this.fromUser()})
|
return this.publish({event, relays: this.fromUser()})
|
||||||
}
|
}
|
||||||
|
|
||||||
follow = async (tag: string[]) => {
|
follow = async (tag: string[]) => {
|
||||||
const list = (await this.followLists.forceLoad(this.user.pubkey, [])) || makeList({kind: FOLLOWS})
|
const list =
|
||||||
|
(await this.followLists.forceLoad(this.user.pubkey, [])) || makeList({kind: FOLLOWS})
|
||||||
const event = await addToListPublicly(list, tag).reconcile(this.encryptToSelf)
|
const event = await addToListPublicly(list, tag).reconcile(this.encryptToSelf)
|
||||||
|
|
||||||
return this.publish({event, relays: this.fromUser()})
|
return this.publish({event, relays: this.fromUser()})
|
||||||
|
|||||||
@@ -1,141 +1,14 @@
|
|||||||
import {Client} from "./client.js"
|
import {Client} from "./client.js"
|
||||||
import type {ClientOptions} from "./client.js"
|
import type {ClientOptions} from "./client.js"
|
||||||
import {Router} from "./router.js"
|
import {defaultClientPolicies} from "./policies.js"
|
||||||
import type {RouterOptions} from "./router.js"
|
|
||||||
import {RelayStats} from "./relayStats.js"
|
|
||||||
import {RelayLists} from "./relayLists.js"
|
|
||||||
import {BlockedRelayLists} from "./blockedRelayLists.js"
|
|
||||||
import {Relays} from "./relays.js"
|
|
||||||
import {Plaintext} from "./plaintext.js"
|
|
||||||
import {Profiles} from "./profiles.js"
|
|
||||||
import {FollowLists} from "./follows.js"
|
|
||||||
import {MuteLists} from "./mutes.js"
|
|
||||||
import {PinLists} from "./pins.js"
|
|
||||||
import {BlossomServerLists} from "./blossom.js"
|
|
||||||
import {MessagingRelayLists} from "./messagingRelayLists.js"
|
|
||||||
import {SearchRelayLists} from "./searchRelayLists.js"
|
|
||||||
import {Handles} from "./handles.js"
|
|
||||||
import {Zappers} from "./zappers.js"
|
|
||||||
import {Topics} from "./topics.js"
|
|
||||||
import {Tags} from "./tags.js"
|
|
||||||
import {Wot} from "./wot.js"
|
|
||||||
import {Feeds} from "./feeds.js"
|
|
||||||
import {Searches} from "./search.js"
|
|
||||||
import {Sync} from "./sync.js"
|
|
||||||
import {GiftWraps} from "./giftWraps.js"
|
|
||||||
import {Commands} from "./commands.js"
|
|
||||||
|
|
||||||
export type AppOptions = ClientOptions & {
|
|
||||||
dufflepudUrl?: string
|
|
||||||
// Whether to unwrap incoming NIP-59 gift wraps (DMs) for this client's user.
|
|
||||||
shouldUnwrap?: boolean
|
|
||||||
// The router's data dependencies are wired up below, so callers only supply
|
|
||||||
// the configuration knobs.
|
|
||||||
router?: Omit<RouterOptions, "getRelaysForPubkey" | "getRelayQuality">
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composes a default application instance: a `Client` plus the core data
|
* Creates a batteries-included client: a `Client` wired with the default client
|
||||||
* modules, wired together. This is where genuine domain cycles (Router <->
|
* policies (event ingestion, relay-stats collection, gift-wrap unwrapping).
|
||||||
* RelayLists, RelayStats <-> BlockedRelayLists) are broken — modules are given
|
* Reach data modules via `client.use(Profiles)`, `client.use(Commands)`, etc.
|
||||||
* lazily-resolved closures that reach their siblings at call time, never at
|
|
||||||
* construction time.
|
|
||||||
*
|
*
|
||||||
* Callers who want a different module set can ignore this helper and compose
|
* For a bare client (no default side effects) construct `new Client(...)`
|
||||||
* their own bag directly, or spread additional modules onto the result.
|
* directly, or pass your own `policies`.
|
||||||
*/
|
*/
|
||||||
export const createApp = (options: AppOptions = {}) => {
|
export const createApp = (options: ClientOptions = {}) =>
|
||||||
const client = new Client(options)
|
new Client({...options, policies: options.policies ?? defaultClientPolicies})
|
||||||
const relays = new Relays(client)
|
|
||||||
const plaintext = new Plaintext(client)
|
|
||||||
|
|
||||||
// Declared up-front so the lazily-invoked closures below can reach them. None
|
|
||||||
// are called during construction, only at routing/scoring time.
|
|
||||||
let relayLists: RelayLists
|
|
||||||
let blockedRelayLists: BlockedRelayLists
|
|
||||||
|
|
||||||
const relayStats = new RelayStats(client, {
|
|
||||||
isRelayBlocked: url => {
|
|
||||||
const pubkey = client.user?.pubkey
|
|
||||||
|
|
||||||
return pubkey ? blockedRelayLists.getBlockedRelays(pubkey).includes(url) : false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const router = new Router(client, {
|
|
||||||
...options.router,
|
|
||||||
getRelaysForPubkey: (pubkey, mode) => relayLists.getRelaysForPubkey(pubkey, mode),
|
|
||||||
getRelayQuality: url => relayStats.getQuality(url),
|
|
||||||
})
|
|
||||||
|
|
||||||
relayLists = new RelayLists(client, router)
|
|
||||||
blockedRelayLists = new BlockedRelayLists(client, relayLists)
|
|
||||||
|
|
||||||
const profiles = new Profiles(client, relayLists)
|
|
||||||
const followLists = new FollowLists(client, relayLists)
|
|
||||||
const muteLists = new MuteLists(client, relayLists, plaintext)
|
|
||||||
const pinLists = new PinLists(client, relayLists)
|
|
||||||
const blossomServerLists = new BlossomServerLists(client, relayLists)
|
|
||||||
const messagingRelayLists = new MessagingRelayLists(client, relayLists)
|
|
||||||
const searchRelayLists = new SearchRelayLists(client, relayLists)
|
|
||||||
const handles = new Handles(client, profiles, {dufflepudUrl: options.dufflepudUrl})
|
|
||||||
const zappers = new Zappers(client, profiles, {dufflepudUrl: options.dufflepudUrl})
|
|
||||||
const topics = new Topics(client)
|
|
||||||
const tags = new Tags(client, router, profiles)
|
|
||||||
const wot = new Wot(client, followLists, muteLists)
|
|
||||||
const feeds = new Feeds(client, wot)
|
|
||||||
const searches = new Searches(client, router, profiles, topics, relays, handles, wot)
|
|
||||||
const sync = new Sync(client, relays)
|
|
||||||
const giftWraps = new GiftWraps(client, {shouldUnwrap: options.shouldUnwrap})
|
|
||||||
|
|
||||||
// Commands act on behalf of a signed-in user, so they're only available when
|
|
||||||
// the client has one.
|
|
||||||
const commands = client.user
|
|
||||||
? new Commands({
|
|
||||||
client,
|
|
||||||
user: client.user,
|
|
||||||
router,
|
|
||||||
relayLists,
|
|
||||||
messagingRelayLists,
|
|
||||||
blockedRelayLists,
|
|
||||||
searchRelayLists,
|
|
||||||
followLists,
|
|
||||||
muteLists,
|
|
||||||
pinLists,
|
|
||||||
})
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
return {
|
|
||||||
client,
|
|
||||||
router,
|
|
||||||
relays,
|
|
||||||
plaintext,
|
|
||||||
relayStats,
|
|
||||||
relayLists,
|
|
||||||
blockedRelayLists,
|
|
||||||
profiles,
|
|
||||||
followLists,
|
|
||||||
muteLists,
|
|
||||||
pinLists,
|
|
||||||
blossomServerLists,
|
|
||||||
messagingRelayLists,
|
|
||||||
searchRelayLists,
|
|
||||||
handles,
|
|
||||||
zappers,
|
|
||||||
topics,
|
|
||||||
tags,
|
|
||||||
wot,
|
|
||||||
feeds,
|
|
||||||
searches,
|
|
||||||
sync,
|
|
||||||
giftWraps,
|
|
||||||
commands,
|
|
||||||
cleanup: () => {
|
|
||||||
relayStats.cleanup()
|
|
||||||
giftWraps.cleanup()
|
|
||||||
client.cleanup()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type App = ReturnType<typeof createApp>
|
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
import {Scope, FeedController} from "@welshman/feeds"
|
import {Scope, FeedController} from "@welshman/feeds"
|
||||||
import type {FeedControllerOptions, Feed} from "@welshman/feeds"
|
import type {FeedControllerOptions, Feed} from "@welshman/feeds"
|
||||||
import type {AdapterContext} from "@welshman/net"
|
import type {AdapterContext} from "@welshman/net"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
import type {Wot} from "./wot.js"
|
import {Wot} from "./wot.js"
|
||||||
|
|
||||||
export type MakeFeedControllerOptions = Partial<Omit<FeedControllerOptions, "feed">> & {feed: Feed}
|
export type MakeFeedControllerOptions = Partial<Omit<FeedControllerOptions, "feed">> & {feed: Feed}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds `FeedController`s wired to this client. Scope/WOT pubkey resolution is
|
* Builds `FeedController`s wired to this client. Scope/WOT pubkey resolution is
|
||||||
* delegated to the injected `Wot`, and feeds fetch through THIS client's net
|
* delegated to `Wot`, and feeds fetch through THIS client's net context (pool +
|
||||||
* context (pool + repository) rather than the global one.
|
* repository) rather than the global one.
|
||||||
*/
|
*/
|
||||||
export class Feeds {
|
export class Feeds {
|
||||||
constructor(
|
constructor(readonly ctx: IClient) {}
|
||||||
readonly ctx: ClientContext,
|
|
||||||
readonly wot: Wot,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
getPubkeysForScope = (scope: Scope): string[] => {
|
getPubkeysForScope = (scope: Scope): string[] => {
|
||||||
const $pubkey = this.ctx.user?.pubkey
|
const $pubkey = this.ctx.user?.pubkey
|
||||||
@@ -28,11 +25,11 @@ export class Feeds {
|
|||||||
case Scope.Self:
|
case Scope.Self:
|
||||||
return [$pubkey]
|
return [$pubkey]
|
||||||
case Scope.Follows:
|
case Scope.Follows:
|
||||||
return this.wot.getFollows($pubkey)
|
return this.ctx.use(Wot).getFollows($pubkey)
|
||||||
case Scope.Network:
|
case Scope.Network:
|
||||||
return this.wot.getNetwork($pubkey)
|
return this.ctx.use(Wot).getNetwork($pubkey)
|
||||||
case Scope.Followers:
|
case Scope.Followers:
|
||||||
return this.wot.getFollowers($pubkey)
|
return this.ctx.use(Wot).getFollowers($pubkey)
|
||||||
default:
|
default:
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -40,11 +37,11 @@ export class Feeds {
|
|||||||
|
|
||||||
getPubkeysForWOTRange = (min: number, max: number): string[] => {
|
getPubkeysForWOTRange = (min: number, max: number): string[] => {
|
||||||
const pubkeys = []
|
const pubkeys = []
|
||||||
const $maxWot = this.wot.getMaxWot() ?? 0
|
const $maxWot = this.ctx.use(Wot).getMaxWot() ?? 0
|
||||||
const thresholdMin = $maxWot * min
|
const thresholdMin = $maxWot * min
|
||||||
const thresholdMax = $maxWot * max
|
const thresholdMax = $maxWot * max
|
||||||
|
|
||||||
for (const [tpk, score] of this.wot.getWotGraph().entries()) {
|
for (const [tpk, score] of this.ctx.use(Wot).getWotGraph().entries()) {
|
||||||
if (score >= thresholdMin && score <= thresholdMax) {
|
if (score >= thresholdMin && score <= thresholdMax) {
|
||||||
pubkeys.push(tpk)
|
pubkeys.push(tpk)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import {FOLLOWS, asDecryptedEvent, readList} from "@welshman/util"
|
import {FOLLOWS, asDecryptedEvent, readList} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import {RelayLists} from "./relayLists.js"
|
||||||
import type {RelayLists} from "./relayLists.js"
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kind-3 follow lists, keyed by pubkey. Loaded via the outbox model (the
|
* Kind-3 follow lists, keyed by pubkey. Loaded via the outbox model (the
|
||||||
* author's write relays), so it depends on the relay-list collection.
|
* author's write relays), so it depends on the relay-list collection.
|
||||||
*/
|
*/
|
||||||
export class FollowLists extends RepositoryCollection<ReturnType<typeof readList>> {
|
export class FollowLists extends RepositoryCollection<ReturnType<typeof readList>> {
|
||||||
constructor(
|
constructor(ctx: IClient) {
|
||||||
ctx: ClientContext,
|
|
||||||
readonly relayLists: RelayLists,
|
|
||||||
) {
|
|
||||||
super(ctx, {
|
super(ctx, {
|
||||||
filters: [{kinds: [FOLLOWS]}],
|
filters: [{kinds: [FOLLOWS]}],
|
||||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||||
@@ -21,6 +18,6 @@ export class FollowLists extends RepositoryCollection<ReturnType<typeof readList
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetch(pubkey: string, relayHints: string[] = []) {
|
fetch(pubkey: string, relayHints: string[] = []) {
|
||||||
return this.relayLists.makeOutboxLoader(FOLLOWS)(pubkey, relayHints)
|
return this.ctx.use(RelayLists).makeOutboxLoader(FOLLOWS)(pubkey, relayHints)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,22 @@
|
|||||||
import {get, writable} from "svelte/store"
|
import {get, writable} from "svelte/store"
|
||||||
import type {Unsubscriber} from "svelte/store"
|
import {TaskQueue} from "@welshman/lib"
|
||||||
import {on, TaskQueue} from "@welshman/lib"
|
import {getPubkeyTagValues} from "@welshman/util"
|
||||||
import {WRAP, getPubkeyTagValues} from "@welshman/util"
|
|
||||||
import type {TrustedEvent, SignedEvent} from "@welshman/util"
|
import type {TrustedEvent, SignedEvent} from "@welshman/util"
|
||||||
import {Nip59} from "@welshman/signer"
|
import {Nip59} from "@welshman/signer"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
export type GiftWrapsOptions = {
|
|
||||||
shouldUnwrap?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-client gift-wrap (NIP-59) ingestion. Watches the client's repository for
|
* Per-client gift-wrap (NIP-59) state: the unwrap queue plus failure/dedup
|
||||||
* kind-1059 wraps and unwraps the ones addressed to THIS client's user, storing
|
* tracking. Scoped to `ctx.user`, so a client only ever unwraps its own user's
|
||||||
* the resulting rumors via the wrap manager.
|
* messages into its own repository — which is what keeps DM history from being
|
||||||
*
|
* merged across identities. The repository subscription that feeds it lives in
|
||||||
* In the old global model a single queue tried every logged-in account's signer
|
* `clientPolicyGiftWraps`.
|
||||||
* against every wrap, depositing all rumors into one shared repository — which
|
|
||||||
* is exactly how DM history got merged across accounts. Here a client only ever
|
|
||||||
* unwraps its own user's messages into its own repository.
|
|
||||||
*/
|
*/
|
||||||
export class GiftWraps {
|
export class GiftWraps {
|
||||||
shouldUnwrap = writable(false)
|
|
||||||
failedUnwraps = new Set<string>()
|
failedUnwraps = new Set<string>()
|
||||||
queue: TaskQueue<TrustedEvent>
|
queue: TaskQueue<TrustedEvent>
|
||||||
cleanup: Unsubscriber
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly ctx: ClientContext,
|
|
||||||
options: GiftWrapsOptions = {},
|
|
||||||
) {
|
|
||||||
this.shouldUnwrap.set(options.shouldUnwrap ?? false)
|
|
||||||
|
|
||||||
|
constructor(readonly ctx: IClient) {
|
||||||
this.queue = new TaskQueue<TrustedEvent>({
|
this.queue = new TaskQueue<TrustedEvent>({
|
||||||
batchSize: 5,
|
batchSize: 5,
|
||||||
batchDelay: 30,
|
batchDelay: 30,
|
||||||
@@ -53,23 +38,9 @@ export class GiftWraps {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Process wraps already in the repository, then any that arrive later
|
|
||||||
for (const wrap of this.ctx.repository.query([{kinds: [WRAP]}])) {
|
|
||||||
this.enqueue(wrap)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cleanup = on(this.ctx.repository, "update", ({added}: {added: TrustedEvent[]}) => {
|
|
||||||
for (const event of added) {
|
|
||||||
if (event.kind === WRAP) {
|
|
||||||
this.enqueue(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enqueue = (wrap: TrustedEvent) => {
|
enqueue = (wrap: TrustedEvent) => {
|
||||||
if (!get(this.shouldUnwrap)) return
|
|
||||||
if (this.failedUnwraps.has(wrap.id)) return
|
if (this.failedUnwraps.has(wrap.id)) return
|
||||||
if (this.ctx.wrapManager.getRumor(wrap.id)) return
|
if (this.ctx.wrapManager.getRumor(wrap.id)) return
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import {tryCatch, fetchJson, batcher, postJson, last} from "@welshman/lib"
|
|||||||
import type {Maybe} from "@welshman/lib"
|
import type {Maybe} from "@welshman/lib"
|
||||||
import {deriveDeduplicated} from "@welshman/store"
|
import {deriveDeduplicated} from "@welshman/store"
|
||||||
import {LoadableData} from "./clientData.js"
|
import {LoadableData} from "./clientData.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
import type {Profiles} from "./profiles.js"
|
import {Profiles} from "./profiles.js"
|
||||||
|
|
||||||
export type Handle = {
|
export type Handle = {
|
||||||
nip05: string
|
nip05: string
|
||||||
@@ -54,11 +54,7 @@ export const displayHandle = (handle: Handle) => displayNip05(handle.nip05)
|
|||||||
* handle.
|
* handle.
|
||||||
*/
|
*/
|
||||||
export class Handles extends LoadableData<Handle> {
|
export class Handles extends LoadableData<Handle> {
|
||||||
constructor(
|
constructor(ctx: IClient) {
|
||||||
ctx: ClientContext,
|
|
||||||
readonly profiles: Profiles,
|
|
||||||
readonly options: {dufflepudUrl?: string} = {},
|
|
||||||
) {
|
|
||||||
super(ctx)
|
super(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,9 +62,10 @@ export class Handles extends LoadableData<Handle> {
|
|||||||
const result = new Map<string, Handle>()
|
const result = new Map<string, Handle>()
|
||||||
|
|
||||||
// Use dufflepud if it's set up to protect user privacy, otherwise fetch directly
|
// Use dufflepud if it's set up to protect user privacy, otherwise fetch directly
|
||||||
if (this.options.dufflepudUrl) {
|
if (this.ctx.config.dufflepudUrl) {
|
||||||
const res: any = await tryCatch(
|
const res: any = await tryCatch(
|
||||||
async () => await postJson(`${this.options.dufflepudUrl}/handle/info`, {handles: nip05s}),
|
async () =>
|
||||||
|
await postJson(`${this.ctx.config.dufflepudUrl}/handle/info`, {handles: nip05s}),
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const {handle: nip05, info} of res?.data || []) {
|
for (const {handle: nip05, info} of res?.data || []) {
|
||||||
@@ -99,7 +96,7 @@ export class Handles extends LoadableData<Handle> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
loadForPubkey = async (pubkey: string, relays: string[] = []) => {
|
loadForPubkey = async (pubkey: string, relays: string[] = []) => {
|
||||||
const $profile = await this.profiles.load(pubkey, relays)
|
const $profile = await this.ctx.use(Profiles).load(pubkey, relays)
|
||||||
|
|
||||||
return $profile?.nip05 ? this.load($profile.nip05) : undefined
|
return $profile?.nip05 ? this.load($profile.nip05) : undefined
|
||||||
}
|
}
|
||||||
@@ -108,7 +105,7 @@ export class Handles extends LoadableData<Handle> {
|
|||||||
this.loadForPubkey(pubkey, relays)
|
this.loadForPubkey(pubkey, relays)
|
||||||
|
|
||||||
return deriveDeduplicated(
|
return deriveDeduplicated(
|
||||||
[this.index, this.profiles.derive(pubkey, relays)],
|
[this.index, this.ctx.use(Profiles).derive(pubkey, relays)],
|
||||||
([$handlesByNip05, $profile]) => {
|
([$handlesByNip05, $profile]) => {
|
||||||
if (!$profile?.nip05) return undefined
|
if (!$profile?.nip05) return undefined
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
export * from "./client.js"
|
export * from "./client.js"
|
||||||
|
export * from "./policies.js"
|
||||||
|
export * from "./networking.js"
|
||||||
|
export * from "./stores.js"
|
||||||
export * from "./clientData.js"
|
export * from "./clientData.js"
|
||||||
export * from "./repositoryCollection.js"
|
export * from "./repositoryCollection.js"
|
||||||
export * from "./user.js"
|
export * from "./user.js"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {MESSAGING_RELAYS, asDecryptedEvent, readList} from "@welshman/util"
|
import {MESSAGING_RELAYS, asDecryptedEvent, readList} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import {RelayLists} from "./relayLists.js"
|
||||||
import type {RelayLists} from "./relayLists.js"
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kind-10050 messaging relay lists (NIP-17), keyed by pubkey. Loaded via the
|
* Kind-10050 messaging relay lists (NIP-17), keyed by pubkey. Loaded via the
|
||||||
@@ -10,10 +10,7 @@ import type {RelayLists} from "./relayLists.js"
|
|||||||
* collection.
|
* collection.
|
||||||
*/
|
*/
|
||||||
export class MessagingRelayLists extends RepositoryCollection<ReturnType<typeof readList>> {
|
export class MessagingRelayLists extends RepositoryCollection<ReturnType<typeof readList>> {
|
||||||
constructor(
|
constructor(ctx: IClient) {
|
||||||
ctx: ClientContext,
|
|
||||||
readonly relayLists: RelayLists,
|
|
||||||
) {
|
|
||||||
super(ctx, {
|
super(ctx, {
|
||||||
filters: [{kinds: [MESSAGING_RELAYS]}],
|
filters: [{kinds: [MESSAGING_RELAYS]}],
|
||||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||||
@@ -22,6 +19,6 @@ export class MessagingRelayLists extends RepositoryCollection<ReturnType<typeof
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetch(pubkey: string, relayHints: string[] = []) {
|
fetch(pubkey: string, relayHints: string[] = []) {
|
||||||
return this.relayLists.makeOutboxLoader(MESSAGING_RELAYS)(pubkey, relayHints)
|
return this.ctx.use(RelayLists).makeOutboxLoader(MESSAGING_RELAYS)(pubkey, relayHints)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
import {MUTES, asDecryptedEvent, readList} from "@welshman/util"
|
import {MUTES, asDecryptedEvent, readList} from "@welshman/util"
|
||||||
import type {TrustedEvent, PublishedList} from "@welshman/util"
|
import type {TrustedEvent, PublishedList} from "@welshman/util"
|
||||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
import type {RelayLists} from "./relayLists.js"
|
import {RelayLists} from "./relayLists.js"
|
||||||
import type {Plaintext} from "./plaintext.js"
|
import {Plaintext} from "./plaintext.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kind-10000 mute lists, keyed by pubkey. Mute lists carry private entries in
|
* Kind-10000 mute lists, keyed by pubkey. Mute lists carry private entries in
|
||||||
* encrypted content, so decoding goes through the plaintext cache.
|
* encrypted content, so decoding goes through the plaintext cache.
|
||||||
*/
|
*/
|
||||||
export class MuteLists extends RepositoryCollection<PublishedList> {
|
export class MuteLists extends RepositoryCollection<PublishedList> {
|
||||||
constructor(
|
constructor(ctx: IClient) {
|
||||||
ctx: ClientContext,
|
|
||||||
readonly relayLists: RelayLists,
|
|
||||||
readonly plaintext: Plaintext,
|
|
||||||
) {
|
|
||||||
super(ctx, {
|
super(ctx, {
|
||||||
filters: [{kinds: [MUTES]}],
|
filters: [{kinds: [MUTES]}],
|
||||||
eventToItem: async (event: TrustedEvent) => {
|
eventToItem: async (event: TrustedEvent) => {
|
||||||
const content = await plaintext.ensure(event)
|
const content = await ctx.use(Plaintext).ensure(event)
|
||||||
|
|
||||||
// If this is our own mute list but it couldn't be decrypted yet because
|
// If this is our own mute list but it couldn't be decrypted yet because
|
||||||
// no signer is available, don't cache a result with empty private tags —
|
// no signer is available, don't cache a result with empty private tags —
|
||||||
@@ -37,6 +33,6 @@ export class MuteLists extends RepositoryCollection<PublishedList> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetch(pubkey: string, relayHints: string[] = []) {
|
fetch(pubkey: string, relayHints: string[] = []) {
|
||||||
return this.relayLists.makeOutboxLoader(MUTES)(pubkey, relayHints)
|
return this.ctx.use(RelayLists).makeOutboxLoader(MUTES)(pubkey, relayHints)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import {request, publish, diff, pull, push, makeLoader} from "@welshman/net"
|
||||||
|
import type {
|
||||||
|
Loader,
|
||||||
|
LoaderOptions,
|
||||||
|
RequestOptions,
|
||||||
|
PublishOptions,
|
||||||
|
DiffOptions,
|
||||||
|
PullOptions,
|
||||||
|
PushOptions,
|
||||||
|
} from "@welshman/net"
|
||||||
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Net utilities bound to the client's net context (its pool + repository). Reach
|
||||||
|
* it via `client.use(Networking)`; `load` is a shared, batched loader.
|
||||||
|
*/
|
||||||
|
export class Networking {
|
||||||
|
load: Loader
|
||||||
|
|
||||||
|
constructor(readonly ctx: IClient) {
|
||||||
|
this.load = this.makeLoader({delay: 200, timeout: 3000, threshold: 0.5})
|
||||||
|
}
|
||||||
|
|
||||||
|
request = (options: Omit<RequestOptions, "context">) =>
|
||||||
|
request({...options, context: this.ctx.netContext})
|
||||||
|
|
||||||
|
publish = (options: Omit<PublishOptions, "context">) =>
|
||||||
|
publish({...options, context: this.ctx.netContext})
|
||||||
|
|
||||||
|
diff = (options: Omit<DiffOptions, "context">) => diff({...options, context: this.ctx.netContext})
|
||||||
|
|
||||||
|
pull = (options: Omit<PullOptions, "context">) => pull({...options, context: this.ctx.netContext})
|
||||||
|
|
||||||
|
push = (options: Omit<PushOptions, "context">) => push({...options, context: this.ctx.netContext})
|
||||||
|
|
||||||
|
makeLoader = (options: Omit<LoaderOptions, "context">): Loader =>
|
||||||
|
makeLoader({...options, context: this.ctx.netContext})
|
||||||
|
}
|
||||||
@@ -1,18 +1,15 @@
|
|||||||
import {PINS, asDecryptedEvent, readList} from "@welshman/util"
|
import {PINS, asDecryptedEvent, readList} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import {RelayLists} from "./relayLists.js"
|
||||||
import type {RelayLists} from "./relayLists.js"
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NIP-51 pin lists (kind 10001), keyed by pubkey. Loaded via the outbox model
|
* NIP-51 pin lists (kind 10001), keyed by pubkey. Loaded via the outbox model
|
||||||
* (the author's write relays), so it depends on the relay-list collection.
|
* (the author's write relays), so it depends on the relay-list collection.
|
||||||
*/
|
*/
|
||||||
export class PinLists extends RepositoryCollection<ReturnType<typeof readList>> {
|
export class PinLists extends RepositoryCollection<ReturnType<typeof readList>> {
|
||||||
constructor(
|
constructor(ctx: IClient) {
|
||||||
ctx: ClientContext,
|
|
||||||
readonly relayLists: RelayLists,
|
|
||||||
) {
|
|
||||||
super(ctx, {
|
super(ctx, {
|
||||||
filters: [{kinds: [PINS]}],
|
filters: [{kinds: [PINS]}],
|
||||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||||
@@ -21,6 +18,6 @@ export class PinLists extends RepositoryCollection<ReturnType<typeof readList>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetch(pubkey: string, relayHints: string[] = []) {
|
fetch(pubkey: string, relayHints: string[] = []) {
|
||||||
return this.relayLists.makeOutboxLoader(PINS)(pubkey, relayHints)
|
return this.ctx.use(RelayLists).makeOutboxLoader(PINS)(pubkey, relayHints)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import type {Unsubscriber} from "svelte/store"
|
||||||
|
import {on} from "@welshman/lib"
|
||||||
|
import {WRAP, isDVMKind, isEphemeralKind} from "@welshman/util"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {SocketEvent, netContext, isRelayEvent} from "@welshman/net"
|
||||||
|
import type {RelayMessage} from "@welshman/net"
|
||||||
|
import type {IClient} from "./client.js"
|
||||||
|
import {RelayStats} from "./relayStats.js"
|
||||||
|
import {GiftWraps} from "./giftWraps.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A client policy is a side effect applied once per client at construction,
|
||||||
|
* returning a cleanup function — directly analogous to a socket policy. Policies
|
||||||
|
* own everything that subscribes or links components together (event ingestion,
|
||||||
|
* stats collection, gift-wrap unwrapping), so the data classes themselves stay
|
||||||
|
* pure and free of subscriptions, and teardown is centralized in `cleanup()`.
|
||||||
|
*/
|
||||||
|
export type ClientPolicy = (client: IClient) => Unsubscriber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ingests every event received on any socket into the client's repository. The
|
||||||
|
* net layer doesn't do this for us, and it's how all the repository-backed
|
||||||
|
* collections (and gift-wrap unwrapping) get populated.
|
||||||
|
*/
|
||||||
|
export const clientPolicyIngest: ClientPolicy = client =>
|
||||||
|
client.pool.subscribe(socket => {
|
||||||
|
const onReceive = (message: RelayMessage) => {
|
||||||
|
if (!isRelayEvent(message)) return
|
||||||
|
|
||||||
|
const event = message[2]
|
||||||
|
|
||||||
|
if (isDVMKind(event.kind) || isEphemeralKind(event.kind)) return
|
||||||
|
if (!netContext.isEventValid(event, socket.url)) return
|
||||||
|
|
||||||
|
client.tracker.track(event.id, socket.url)
|
||||||
|
client.repository.publish(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on(SocketEvent.Receive, onReceive)
|
||||||
|
|
||||||
|
return () => socket.off(SocketEvent.Receive, onReceive)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wires socket activity on the client's pool into the RelayStats store.
|
||||||
|
*/
|
||||||
|
export const clientPolicyRelayStats: ClientPolicy = client => {
|
||||||
|
const stats = client.use(RelayStats)
|
||||||
|
|
||||||
|
return client.pool.subscribe(socket => {
|
||||||
|
socket.on(SocketEvent.Send, stats.onSocketSend)
|
||||||
|
socket.on(SocketEvent.Receive, stats.onSocketReceive)
|
||||||
|
socket.on(SocketEvent.Status, stats.onSocketStatus)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off(SocketEvent.Send, stats.onSocketSend)
|
||||||
|
socket.off(SocketEvent.Receive, stats.onSocketReceive)
|
||||||
|
socket.off(SocketEvent.Status, stats.onSocketStatus)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watches the client's repository for gift wraps (existing and incoming) and
|
||||||
|
* feeds them to the unwrap queue.
|
||||||
|
*/
|
||||||
|
export const clientPolicyGiftWraps: ClientPolicy = client => {
|
||||||
|
const giftWraps = client.use(GiftWraps)
|
||||||
|
|
||||||
|
for (const wrap of client.repository.query([{kinds: [WRAP]}])) {
|
||||||
|
giftWraps.enqueue(wrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
return on(client.repository, "update", ({added}: {added: TrustedEvent[]}) => {
|
||||||
|
for (const event of added) {
|
||||||
|
if (event.kind === WRAP) {
|
||||||
|
giftWraps.enqueue(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultClientPolicies: ClientPolicy[] = [
|
||||||
|
clientPolicyIngest,
|
||||||
|
clientPolicyRelayStats,
|
||||||
|
clientPolicyGiftWraps,
|
||||||
|
]
|
||||||
@@ -1,18 +1,15 @@
|
|||||||
import {derived, readable} from "svelte/store"
|
import {derived, readable} from "svelte/store"
|
||||||
import {readProfile, displayProfile, displayPubkey, PROFILE} from "@welshman/util"
|
import {readProfile, displayProfile, displayPubkey, PROFILE} from "@welshman/util"
|
||||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import {RelayLists} from "./relayLists.js"
|
||||||
import type {RelayLists} from "./relayLists.js"
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kind-0 profiles, keyed by pubkey. Loaded via the outbox model (the author's
|
* Kind-0 profiles, keyed by pubkey. Loaded via the outbox model (the author's
|
||||||
* write relays), so it depends on the relay-list collection.
|
* write relays), resolved through the relay-list collection at fetch time.
|
||||||
*/
|
*/
|
||||||
export class Profiles extends RepositoryCollection<ReturnType<typeof readProfile>> {
|
export class Profiles extends RepositoryCollection<ReturnType<typeof readProfile>> {
|
||||||
constructor(
|
constructor(ctx: IClient) {
|
||||||
ctx: ClientContext,
|
|
||||||
readonly relayLists: RelayLists,
|
|
||||||
) {
|
|
||||||
super(ctx, {
|
super(ctx, {
|
||||||
filters: [{kinds: [PROFILE]}],
|
filters: [{kinds: [PROFILE]}],
|
||||||
eventToItem: readProfile,
|
eventToItem: readProfile,
|
||||||
@@ -21,7 +18,7 @@ export class Profiles extends RepositoryCollection<ReturnType<typeof readProfile
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetch(pubkey: string, relayHints: string[] = []) {
|
fetch(pubkey: string, relayHints: string[] = []) {
|
||||||
return this.relayLists.makeOutboxLoader(PROFILE)(pubkey, relayHints)
|
return this.ctx.use(RelayLists).makeOutboxLoader(PROFILE)(pubkey, relayHints)
|
||||||
}
|
}
|
||||||
|
|
||||||
display = (pubkey: string | undefined) =>
|
display = (pubkey: string | undefined) =>
|
||||||
|
|||||||
@@ -10,24 +10,18 @@ import {
|
|||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {Filter, TrustedEvent, PublishedList} from "@welshman/util"
|
import type {Filter, TrustedEvent, PublishedList} from "@welshman/util"
|
||||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||||
import {addMinimalFallbacks} from "./router.js"
|
import {Router, addMinimalFallbacks} from "./router.js"
|
||||||
import type {Router} from "./router.js"
|
import {Networking} from "./networking.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NIP-65 relay lists, keyed by pubkey. This is the routing substrate every
|
* NIP-65 relay lists, keyed by pubkey. This is the routing substrate every other
|
||||||
* other outbox-model load depends on, so it also exposes `loadUsingOutbox` /
|
* outbox-model load depends on, so it also exposes `loadUsingOutbox` /
|
||||||
* `makeOutboxLoader` for other collections to build their fetchers on.
|
* `makeOutboxLoader` for other collections to build their fetchers on. It and the
|
||||||
*
|
* Router reference each other lazily via `ctx.use`, so the cycle never bites.
|
||||||
* It depends on a `Router`, and the `Router` depends (via injected functions) on
|
|
||||||
* this collection — a genuine domain cycle that `createApp` breaks by wiring the
|
|
||||||
* router's `getRelaysForPubkey` to a lazily-resolved closure.
|
|
||||||
*/
|
*/
|
||||||
export class RelayLists extends RepositoryCollection<PublishedList> {
|
export class RelayLists extends RepositoryCollection<PublishedList> {
|
||||||
constructor(
|
constructor(ctx: IClient) {
|
||||||
ctx: ClientContext,
|
|
||||||
readonly router: Router,
|
|
||||||
) {
|
|
||||||
super(ctx, {
|
super(ctx, {
|
||||||
filters: [{kinds: [RELAYS]}],
|
filters: [{kinds: [RELAYS]}],
|
||||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||||
@@ -37,11 +31,12 @@ export class RelayLists extends RepositoryCollection<PublishedList> {
|
|||||||
|
|
||||||
fetch(pubkey: string, relayHints: string[] = []) {
|
fetch(pubkey: string, relayHints: string[] = []) {
|
||||||
const filters = [{kinds: [RELAYS], authors: [pubkey], limit: 1}]
|
const filters = [{kinds: [RELAYS], authors: [pubkey], limit: 1}]
|
||||||
|
const router = this.ctx.use(Router)
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
this.ctx.load({filters, relays: this.router.FromRelays(relayHints).getUrls()}),
|
this.ctx.use(Networking).load({filters, relays: router.FromRelays(relayHints).getUrls()}),
|
||||||
this.ctx.load({filters, relays: this.router.FromPubkey(pubkey).getUrls()}),
|
this.ctx.use(Networking).load({filters, relays: router.FromPubkey(pubkey).getUrls()}),
|
||||||
this.ctx.load({filters, relays: this.router.Index().getUrls()}),
|
this.ctx.use(Networking).load({filters, relays: router.Index().getUrls()}),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +48,8 @@ export class RelayLists extends RepositoryCollection<PublishedList> {
|
|||||||
loadUsingOutbox = async (kind: number, pubkey: string, filter: Filter = {}) => {
|
loadUsingOutbox = async (kind: number, pubkey: string, filter: Filter = {}) => {
|
||||||
const filters: Filter[] = [{...filter, kinds: [kind], authors: [pubkey]}]
|
const filters: Filter[] = [{...filter, kinds: [kind], authors: [pubkey]}]
|
||||||
const writeRelays = getRelaysFromList(await this.load(pubkey), RelayMode.Write)
|
const writeRelays = getRelaysFromList(await this.load(pubkey), RelayMode.Write)
|
||||||
const allRelays = this.router
|
const allRelays = this.ctx
|
||||||
|
.use(Router)
|
||||||
.FromRelays(writeRelays)
|
.FromRelays(writeRelays)
|
||||||
.policy(addMinimalFallbacks)
|
.policy(addMinimalFallbacks)
|
||||||
.limit(8)
|
.limit(8)
|
||||||
@@ -64,7 +60,7 @@ export class RelayLists extends RepositoryCollection<PublishedList> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const relays of chunk(2, allRelays)) {
|
for (const relays of chunk(2, allRelays)) {
|
||||||
const events = await this.ctx.load({filters, relays})
|
const events = await this.ctx.use(Networking).load({filters, relays})
|
||||||
|
|
||||||
if (events.length > 0) {
|
if (events.length > 0) {
|
||||||
return first(sortEventsDesc(events))
|
return first(sortEventsDesc(events))
|
||||||
@@ -76,10 +72,10 @@ export class RelayLists extends RepositoryCollection<PublishedList> {
|
|||||||
(kind: number, filter: Filter = {}) =>
|
(kind: number, filter: Filter = {}) =>
|
||||||
async (pubkey: string, relayHints: string[] = []) => {
|
async (pubkey: string, relayHints: string[] = []) => {
|
||||||
const filters: Filter[] = [{...filter, kinds: [kind], authors: [pubkey]}]
|
const filters: Filter[] = [{...filter, kinds: [kind], authors: [pubkey]}]
|
||||||
const relays = this.router.FromRelays(relayHints).getUrls()
|
const relays = this.ctx.use(Router).FromRelays(relayHints).getUrls()
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.ctx.load({filters, relays}),
|
this.ctx.use(Networking).load({filters, relays}),
|
||||||
this.loadUsingOutbox(kind, pubkey, filter),
|
this.loadUsingOutbox(kind, pubkey, filter),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type {Unsubscriber} from "svelte/store"
|
|
||||||
import {groupBy, batch, now, uniq, ago, DAY, HOUR, MINUTE} from "@welshman/lib"
|
import {groupBy, batch, now, uniq, ago, DAY, HOUR, MINUTE} from "@welshman/lib"
|
||||||
import {isOnionUrl, isLocalUrl, isIPAddress, isRelayUrl} from "@welshman/util"
|
import {isOnionUrl, isLocalUrl, isIPAddress, isRelayUrl} from "@welshman/util"
|
||||||
import {SocketStatus, SocketEvent} from "@welshman/net"
|
import {SocketStatus} from "@welshman/net"
|
||||||
import type {Socket, ClientMessage, RelayMessage} from "@welshman/net"
|
import type {ClientMessage, RelayMessage} from "@welshman/net"
|
||||||
import {ClientData} from "./clientData.js"
|
import {ClientData} from "./clientData.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import {BlockedRelayLists} from "./blockedRelayLists.js"
|
||||||
|
|
||||||
export type RelayStatsUpdate = [string, (stats: RelayStatsItem) => void]
|
export type RelayStatsUpdate = [string, (stats: RelayStatsItem) => void]
|
||||||
|
|
||||||
@@ -52,45 +51,22 @@ export const makeRelayStatsItem = (url: string): RelayStatsItem => ({
|
|||||||
notice_count: 0,
|
notice_count: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
export type RelayStatsOptions = {
|
|
||||||
// Allows a host app to zero out blocked relays once a blocked-relay-list
|
|
||||||
// module is composed in, without RelayStats depending on it.
|
|
||||||
isRelayBlocked?: (url: string) => boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks per-relay connection statistics by listening to socket activity on the
|
* Per-relay connection statistics, keyed by url, plus the `getQuality` heuristic
|
||||||
* client's pool, and exposes a `getQuality` heuristic the router uses to rank
|
* the router uses to rank relays. A pure store — the socket wiring that fills it
|
||||||
* relays. A "local" collection — its data isn't backed by the repository.
|
* lives in `clientPolicyRelayStats`.
|
||||||
*/
|
*/
|
||||||
export class RelayStats extends ClientData<RelayStatsItem> {
|
export class RelayStats extends ClientData<RelayStatsItem> {
|
||||||
cleanup: Unsubscriber
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
ctx: ClientContext,
|
|
||||||
readonly statsOptions: RelayStatsOptions = {},
|
|
||||||
) {
|
|
||||||
super(ctx)
|
|
||||||
|
|
||||||
this.cleanup = ctx.pool.subscribe(socket => {
|
|
||||||
socket.on(SocketEvent.Send, this.onSocketSend)
|
|
||||||
socket.on(SocketEvent.Receive, this.onSocketReceive)
|
|
||||||
socket.on(SocketEvent.Status, this.onSocketStatus)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socket.off(SocketEvent.Send, this.onSocketSend)
|
|
||||||
socket.off(SocketEvent.Receive, this.onSocketReceive)
|
|
||||||
socket.off(SocketEvent.Status, this.onSocketStatus)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getQuality = (url: string) => {
|
getQuality = (url: string) => {
|
||||||
// Skip non-relays entirely
|
// Skip non-relays entirely
|
||||||
if (!isRelayUrl(url)) return 0
|
if (!isRelayUrl(url)) return 0
|
||||||
|
|
||||||
// Skip blocked relays (when a host app provides the check)
|
// Skip relays the user has blocked
|
||||||
if (this.statsOptions.isRelayBlocked?.(url)) return 0
|
const pubkey = this.ctx.user?.pubkey
|
||||||
|
|
||||||
|
if (pubkey && this.ctx.use(BlockedRelayLists).getBlockedRelays(pubkey).includes(url)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
const stats = this.get(url)
|
const stats = this.get(url)
|
||||||
|
|
||||||
@@ -116,9 +92,7 @@ export class RelayStats extends ClientData<RelayStatsItem> {
|
|||||||
return 0.7
|
return 0.7
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utilities for syncing stats from connections to relays
|
private update = batch(150, (batched: RelayStatsUpdate[]) => {
|
||||||
|
|
||||||
private updateRelayStats = batch(150, (batched: RelayStatsUpdate[]) => {
|
|
||||||
for (const [url, updates] of groupBy(([url]) => url, batched)) {
|
for (const [url, updates] of groupBy(([url]) => url, batched)) {
|
||||||
if (!url || !isRelayUrl(url)) {
|
if (!url || !isRelayUrl(url)) {
|
||||||
console.warn(`Attempted to update stats for an invalid relay url: ${url}`)
|
console.warn(`Attempted to update stats for an invalid relay url: ${url}`)
|
||||||
@@ -136,9 +110,9 @@ export class RelayStats extends ClientData<RelayStatsItem> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
private onSocketSend = ([verb]: ClientMessage, url: string) => {
|
onSocketSend = ([verb]: ClientMessage, url: string) => {
|
||||||
if (verb === "REQ") {
|
if (verb === "REQ") {
|
||||||
this.updateRelayStats([
|
this.update([
|
||||||
url,
|
url,
|
||||||
stats => {
|
stats => {
|
||||||
stats.request_count++
|
stats.request_count++
|
||||||
@@ -146,7 +120,7 @@ export class RelayStats extends ClientData<RelayStatsItem> {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
} else if (verb === "EVENT") {
|
} else if (verb === "EVENT") {
|
||||||
this.updateRelayStats([
|
this.update([
|
||||||
url,
|
url,
|
||||||
stats => {
|
stats => {
|
||||||
stats.publish_count++
|
stats.publish_count++
|
||||||
@@ -156,11 +130,11 @@ export class RelayStats extends ClientData<RelayStatsItem> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSocketReceive = ([verb, ...extra]: RelayMessage, url: string) => {
|
onSocketReceive = ([verb, ...extra]: RelayMessage, url: string) => {
|
||||||
if (verb === "OK") {
|
if (verb === "OK") {
|
||||||
const [, ok] = extra
|
const [, ok] = extra
|
||||||
|
|
||||||
this.updateRelayStats([
|
this.update([
|
||||||
url,
|
url,
|
||||||
stats => {
|
stats => {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -171,14 +145,9 @@ export class RelayStats extends ClientData<RelayStatsItem> {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
} else if (verb === "AUTH") {
|
} else if (verb === "AUTH") {
|
||||||
this.updateRelayStats([
|
this.update([url, stats => (stats.last_auth = now())])
|
||||||
url,
|
|
||||||
stats => {
|
|
||||||
stats.last_auth = now()
|
|
||||||
},
|
|
||||||
])
|
|
||||||
} else if (verb === "EVENT") {
|
} else if (verb === "EVENT") {
|
||||||
this.updateRelayStats([
|
this.update([
|
||||||
url,
|
url,
|
||||||
stats => {
|
stats => {
|
||||||
stats.event_count++
|
stats.event_count++
|
||||||
@@ -186,25 +155,15 @@ export class RelayStats extends ClientData<RelayStatsItem> {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
} else if (verb === "EOSE") {
|
} else if (verb === "EOSE") {
|
||||||
this.updateRelayStats([
|
this.update([url, stats => stats.eose_count++])
|
||||||
url,
|
|
||||||
stats => {
|
|
||||||
stats.eose_count++
|
|
||||||
},
|
|
||||||
])
|
|
||||||
} else if (verb === "NOTICE") {
|
} else if (verb === "NOTICE") {
|
||||||
this.updateRelayStats([
|
this.update([url, stats => stats.notice_count++])
|
||||||
url,
|
|
||||||
stats => {
|
|
||||||
stats.notice_count++
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSocketStatus = (status: string, url: string) => {
|
onSocketStatus = (status: string, url: string) => {
|
||||||
if (status === SocketStatus.Open) {
|
if (status === SocketStatus.Open) {
|
||||||
this.updateRelayStats([
|
this.update([
|
||||||
url,
|
url,
|
||||||
stats => {
|
stats => {
|
||||||
stats.last_open = now()
|
stats.last_open = now()
|
||||||
@@ -214,7 +173,7 @@ export class RelayStats extends ClientData<RelayStatsItem> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === SocketStatus.Closed) {
|
if (status === SocketStatus.Closed) {
|
||||||
this.updateRelayStats([
|
this.update([
|
||||||
url,
|
url,
|
||||||
stats => {
|
stats => {
|
||||||
stats.last_close = now()
|
stats.last_close = now()
|
||||||
@@ -224,7 +183,7 @@ export class RelayStats extends ClientData<RelayStatsItem> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === SocketStatus.Error) {
|
if (status === SocketStatus.Error) {
|
||||||
this.updateRelayStats([
|
this.update([
|
||||||
url,
|
url,
|
||||||
stats => {
|
stats => {
|
||||||
stats.last_error = now()
|
stats.last_error = now()
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import type {Maybe} from "@welshman/lib"
|
|||||||
import type {Filter} from "@welshman/util"
|
import type {Filter} from "@welshman/util"
|
||||||
import {deriveItems, getter, makeLoadItem, makeForceLoadItem, makeDeriveItem} from "@welshman/store"
|
import {deriveItems, getter, makeLoadItem, makeForceLoadItem, makeDeriveItem} from "@welshman/store"
|
||||||
import type {EventToItem, ItemsByKey, MakeLoadItemOptions} from "@welshman/store"
|
import type {EventToItem, ItemsByKey, MakeLoadItemOptions} from "@welshman/store"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
|
import {Stores} from "./stores.js"
|
||||||
|
|
||||||
export type RepositoryCollectionOptions<T> = {
|
export type RepositoryCollectionOptions<T> = {
|
||||||
filters: Filter[]
|
filters: Filter[]
|
||||||
@@ -19,7 +20,7 @@ export type RepositoryCollectionOptions<T> = {
|
|||||||
* `fetch` (how to load an item by key from the network) and pass the
|
* `fetch` (how to load an item by key from the network) and pass the
|
||||||
* filters/decoder via `super`.
|
* filters/decoder via `super`.
|
||||||
*
|
*
|
||||||
* Like `ClientData`, subclasses depend only on the `ClientContext` seam.
|
* Like `ClientData`, subclasses depend only on the `IClient` seam.
|
||||||
*/
|
*/
|
||||||
export abstract class RepositoryCollection<T> {
|
export abstract class RepositoryCollection<T> {
|
||||||
byKey: Readable<ItemsByKey<T>>
|
byKey: Readable<ItemsByKey<T>>
|
||||||
@@ -40,12 +41,12 @@ export abstract class RepositoryCollection<T> {
|
|||||||
abstract fetch(key: string, ...args: any[]): Promise<unknown>
|
abstract fetch(key: string, ...args: any[]): Promise<unknown>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly ctx: ClientContext,
|
protected readonly ctx: IClient,
|
||||||
options: RepositoryCollectionOptions<T>,
|
options: RepositoryCollectionOptions<T>,
|
||||||
) {
|
) {
|
||||||
const fetch = (key: string, ...args: any[]) => this.fetch(key, ...args)
|
const fetch = (key: string, ...args: any[]) => this.fetch(key, ...args)
|
||||||
|
|
||||||
this.byKey = ctx.deriveItemsByKey<T>({
|
this.byKey = ctx.use(Stores).deriveItemsByKey<T>({
|
||||||
filters: options.filters,
|
filters: options.filters,
|
||||||
eventToItem: options.eventToItem,
|
eventToItem: options.eventToItem,
|
||||||
getKey: options.getKey,
|
getKey: options.getKey,
|
||||||
|
|||||||
@@ -11,55 +11,15 @@ import {
|
|||||||
RelayMode,
|
RelayMode,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, Filter} from "@welshman/util"
|
import type {TrustedEvent, Filter} from "@welshman/util"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
|
import {RelayLists} from "./relayLists.js"
|
||||||
|
import {RelayStats} from "./relayStats.js"
|
||||||
|
|
||||||
export type RelaysAndFilters = {
|
export type RelaysAndFilters = {
|
||||||
relays: string[]
|
relays: string[]
|
||||||
filters: Filter[]
|
filters: Filter[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RouterOptions = {
|
|
||||||
/**
|
|
||||||
* Retrieves default relays, for use as fallbacks when no other relays can be selected.
|
|
||||||
* @returns An array of relay URLs as strings.
|
|
||||||
*/
|
|
||||||
getDefaultRelays?: () => string[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves relays that index profiles and relay selections.
|
|
||||||
* @returns An array of relay URLs as strings.
|
|
||||||
*/
|
|
||||||
getIndexerRelays?: () => string[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves relays likely to support NIP-50 search.
|
|
||||||
* @returns An array of relay URLs as strings.
|
|
||||||
*/
|
|
||||||
getSearchRelays?: () => string[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the limit setting, which is the maximum number of relays that should be
|
|
||||||
* returned from getUrls and getSelections.
|
|
||||||
* @returns The limit setting as a number.
|
|
||||||
*/
|
|
||||||
getLimit?: () => number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a pubkey's relays for a given mode. Injected (rather than read off
|
|
||||||
* a relay-list module directly) so the router has no dependency on its sibling
|
|
||||||
* data modules. See `createApp`.
|
|
||||||
* @returns An array of relay URLs as strings.
|
|
||||||
*/
|
|
||||||
getRelaysForPubkey?: (pubkey: string, mode?: RelayMode) => string[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scores a relay url for ranking (higher is better). Injected so the router
|
|
||||||
* doesn't depend on the relay-stats module.
|
|
||||||
* @returns A quality score, typically between 0 and 1.
|
|
||||||
*/
|
|
||||||
getRelayQuality?: (url: string) => number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Selection = {
|
export type Selection = {
|
||||||
weight: number
|
weight: number
|
||||||
relays: string[]
|
relays: string[]
|
||||||
@@ -83,15 +43,12 @@ export const addMaximalFallbacks = (count: number, limit: number) => limit - cou
|
|||||||
// Router class
|
// Router class
|
||||||
|
|
||||||
export class Router {
|
export class Router {
|
||||||
constructor(
|
constructor(readonly ctx: IClient) {}
|
||||||
readonly ctx: ClientContext,
|
|
||||||
readonly options: RouterOptions,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
// Utilities derived from options
|
// Utilities derived from the relay-list collection and client config
|
||||||
|
|
||||||
getRelaysForPubkey = (pubkey: string, mode?: RelayMode) =>
|
getRelaysForPubkey = (pubkey: string, mode?: RelayMode) =>
|
||||||
this.options.getRelaysForPubkey?.(pubkey, mode) || []
|
this.ctx.use(RelayLists).getRelaysForPubkey(pubkey, mode)
|
||||||
|
|
||||||
getRelaysForPubkeys = (pubkeys: string[], mode?: RelayMode) =>
|
getRelaysForPubkeys = (pubkeys: string[], mode?: RelayMode) =>
|
||||||
pubkeys.map(pubkey => this.getRelaysForPubkey(pubkey, mode))
|
pubkeys.map(pubkey => this.getRelaysForPubkey(pubkey, mode))
|
||||||
@@ -113,11 +70,11 @@ export class Router {
|
|||||||
|
|
||||||
FromRelays = (relays: string[]) => this.scenario([makeSelection(relays)])
|
FromRelays = (relays: string[]) => this.scenario([makeSelection(relays)])
|
||||||
|
|
||||||
Search = () => this.FromRelays(this.options.getSearchRelays?.() || [])
|
Search = () => this.FromRelays(this.ctx.config.getSearchRelays?.() || [])
|
||||||
|
|
||||||
Index = () => this.FromRelays(this.options.getIndexerRelays?.() || [])
|
Index = () => this.FromRelays(this.ctx.config.getIndexerRelays?.() || [])
|
||||||
|
|
||||||
Default = () => this.FromRelays(this.options.getDefaultRelays?.() || [])
|
Default = () => this.FromRelays(this.ctx.config.getDefaultRelays?.() || [])
|
||||||
|
|
||||||
ForUser = () => this.FromRelays(this.getRelaysForUser(RelayMode.Read))
|
ForUser = () => this.FromRelays(this.getRelaysForUser(RelayMode.Read))
|
||||||
|
|
||||||
@@ -251,9 +208,9 @@ export class RouterScenario {
|
|||||||
weight = (scale: number) =>
|
weight = (scale: number) =>
|
||||||
this.update(selection => ({...selection, weight: selection.weight * scale}))
|
this.update(selection => ({...selection, weight: selection.weight * scale}))
|
||||||
|
|
||||||
getPolicy = () => this.options.policy || addNoFallbacks
|
getPolicy = () => this.options.policy ?? addNoFallbacks
|
||||||
|
|
||||||
getLimit = () => this.options.limit || this.router.options.getLimit?.() || 3
|
getLimit = () => this.options.limit ?? 3
|
||||||
|
|
||||||
getUrls = () => {
|
getUrls = () => {
|
||||||
const limit = this.getLimit()
|
const limit = this.getLimit()
|
||||||
@@ -274,7 +231,7 @@ export class RouterScenario {
|
|||||||
|
|
||||||
const scoreRelay = (relay: string) => {
|
const scoreRelay = (relay: string) => {
|
||||||
const weight = relayWeights.get(relay)!
|
const weight = relayWeights.get(relay)!
|
||||||
const quality = this.router.options.getRelayQuality?.(relay) ?? 1
|
const quality = this.router.ctx.use(RelayStats).getQuality(relay)
|
||||||
|
|
||||||
// Log the weight, since it's a straight count which ends up over-weighting hubs.
|
// Log the weight, since it's a straight count which ends up over-weighting hubs.
|
||||||
// Also add some random noise so that we'll occasionally pick lower quality/less
|
// Also add some random noise so that we'll occasionally pick lower quality/less
|
||||||
@@ -288,7 +245,7 @@ export class RouterScenario {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const fallbacksNeeded = fallbackPolicy(relays.length, limit)
|
const fallbacksNeeded = fallbackPolicy(relays.length, limit)
|
||||||
const allFallbackRelays: string[] = this.router.options.getDefaultRelays?.() || []
|
const allFallbackRelays: string[] = this.router.ctx.config.getDefaultRelays?.() || []
|
||||||
const fallbackRelays = shuffle(allFallbackRelays).slice(0, fallbacksNeeded)
|
const fallbackRelays = shuffle(allFallbackRelays).slice(0, fallbacksNeeded)
|
||||||
|
|
||||||
for (const fallbackRelay of fallbackRelays) {
|
for (const fallbackRelay of fallbackRelays) {
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ import {dec, inc, sortBy} from "@welshman/lib"
|
|||||||
import {PROFILE} from "@welshman/util"
|
import {PROFILE} from "@welshman/util"
|
||||||
import type {PublishedProfile, RelayProfile} from "@welshman/util"
|
import type {PublishedProfile, RelayProfile} from "@welshman/util"
|
||||||
import {throttled, deriveItems} from "@welshman/store"
|
import {throttled, deriveItems} from "@welshman/store"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
import type {Router} from "./router.js"
|
import {Networking} from "./networking.js"
|
||||||
import type {Profiles} from "./profiles.js"
|
import {Router} from "./router.js"
|
||||||
import type {Topics, Topic} from "./topics.js"
|
import {Profiles} from "./profiles.js"
|
||||||
import type {Relays} from "./relays.js"
|
import {Topics} from "./topics.js"
|
||||||
import type {Handles} from "./handles.js"
|
import type {Topic} from "./topics.js"
|
||||||
import type {Wot} from "./wot.js"
|
import {Relays} from "./relays.js"
|
||||||
|
import {Handles} from "./handles.js"
|
||||||
|
import {Wot} from "./wot.js"
|
||||||
|
|
||||||
export type SearchOptions<V, T> = {
|
export type SearchOptions<V, T> = {
|
||||||
getValue: (item: T) => V
|
getValue: (item: T) => V
|
||||||
@@ -65,22 +67,13 @@ export class Searches {
|
|||||||
topicSearch: Readable<Search<string, Topic>>
|
topicSearch: Readable<Search<string, Topic>>
|
||||||
relaySearch: Readable<Search<string, RelayProfile>>
|
relaySearch: Readable<Search<string, RelayProfile>>
|
||||||
|
|
||||||
constructor(
|
constructor(readonly ctx: IClient) {
|
||||||
readonly ctx: ClientContext,
|
|
||||||
readonly router: Router,
|
|
||||||
readonly profiles: Profiles,
|
|
||||||
readonly topics: Topics,
|
|
||||||
readonly relays: Relays,
|
|
||||||
readonly handles: Handles,
|
|
||||||
readonly wot: Wot,
|
|
||||||
) {
|
|
||||||
this.profileSearch = derived(
|
this.profileSearch = derived(
|
||||||
[throttled(800, this.profiles.all), throttled(800, this.handles)],
|
[throttled(800, this.ctx.use(Profiles).all), throttled(800, this.ctx.use(Handles))],
|
||||||
([$profiles, $handlesByNip05]) => {
|
([$profiles, $handlesByNip05]) => {
|
||||||
// Remove invalid nip05's from profiles
|
// Remove invalid nip05's from profiles
|
||||||
const options = $profiles.map(p => {
|
const options = $profiles.map(p => {
|
||||||
const isNip05Valid =
|
const isNip05Valid = !p.nip05 || $handlesByNip05.get(p.nip05)?.pubkey === p.event.pubkey
|
||||||
!p.nip05 || $handlesByNip05.get(p.nip05)?.pubkey === p.event.pubkey
|
|
||||||
|
|
||||||
return isNip05Valid ? p : {...p, nip05: ""}
|
return isNip05Valid ? p : {...p, nip05: ""}
|
||||||
})
|
})
|
||||||
@@ -89,9 +82,9 @@ export class Searches {
|
|||||||
onSearch: this.searchProfiles,
|
onSearch: this.searchProfiles,
|
||||||
getValue: (profile: PublishedProfile) => profile.event.pubkey,
|
getValue: (profile: PublishedProfile) => profile.event.pubkey,
|
||||||
sortFn: ({score = 1, item}) => {
|
sortFn: ({score = 1, item}) => {
|
||||||
const wotScore = this.wot.getWotGraph().get(item.event.pubkey) || 0
|
const wotScore = this.ctx.use(Wot).getWotGraph().get(item.event.pubkey) || 0
|
||||||
|
|
||||||
return dec(score) * inc(wotScore / (this.wot.getMaxWot() || 1))
|
return dec(score) * inc(wotScore / (this.ctx.use(Wot).getMaxWot() || 1))
|
||||||
},
|
},
|
||||||
fuseOptions: {
|
fuseOptions: {
|
||||||
keys: [
|
keys: [
|
||||||
@@ -107,14 +100,14 @@ export class Searches {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
this.topicSearch = derived(this.topics.all, $topics =>
|
this.topicSearch = derived(this.ctx.use(Topics).all, $topics =>
|
||||||
createSearch($topics, {
|
createSearch($topics, {
|
||||||
getValue: (topic: Topic) => topic.name,
|
getValue: (topic: Topic) => topic.name,
|
||||||
fuseOptions: {keys: ["name"]},
|
fuseOptions: {keys: ["name"]},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
this.relaySearch = derived(deriveItems(this.relays), $relays =>
|
this.relaySearch = derived(deriveItems(this.ctx.use(Relays)), $relays =>
|
||||||
createSearch($relays, {
|
createSearch($relays, {
|
||||||
getValue: (relay: RelayProfile) => relay.url,
|
getValue: (relay: RelayProfile) => relay.url,
|
||||||
fuseOptions: {
|
fuseOptions: {
|
||||||
@@ -126,9 +119,9 @@ export class Searches {
|
|||||||
|
|
||||||
searchProfiles = debounce(500, (search: string) => {
|
searchProfiles = debounce(500, (search: string) => {
|
||||||
if (search.length > 2) {
|
if (search.length > 2) {
|
||||||
this.ctx.load({
|
this.ctx.use(Networking).load({
|
||||||
filters: [{kinds: [PROFILE], search}],
|
filters: [{kinds: [PROFILE], search}],
|
||||||
relays: this.router.Search().getUrls(),
|
relays: this.ctx.use(Router).Search().getUrls(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {SEARCH_RELAYS, asDecryptedEvent, readList} from "@welshman/util"
|
import {SEARCH_RELAYS, asDecryptedEvent, readList} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import {RelayLists} from "./relayLists.js"
|
||||||
import type {RelayLists} from "./relayLists.js"
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NIP-51 search relay lists (kind 10007), keyed by pubkey. Loaded via the
|
* NIP-51 search relay lists (kind 10007), keyed by pubkey. Loaded via the
|
||||||
@@ -10,10 +10,7 @@ import type {RelayLists} from "./relayLists.js"
|
|||||||
* collection.
|
* collection.
|
||||||
*/
|
*/
|
||||||
export class SearchRelayLists extends RepositoryCollection<ReturnType<typeof readList>> {
|
export class SearchRelayLists extends RepositoryCollection<ReturnType<typeof readList>> {
|
||||||
constructor(
|
constructor(ctx: IClient) {
|
||||||
ctx: ClientContext,
|
|
||||||
readonly relayLists: RelayLists,
|
|
||||||
) {
|
|
||||||
super(ctx, {
|
super(ctx, {
|
||||||
filters: [{kinds: [SEARCH_RELAYS]}],
|
filters: [{kinds: [SEARCH_RELAYS]}],
|
||||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||||
@@ -22,6 +19,6 @@ export class SearchRelayLists extends RepositoryCollection<ReturnType<typeof rea
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetch(pubkey: string, relayHints: string[] = []) {
|
fetch(pubkey: string, relayHints: string[] = []) {
|
||||||
return this.relayLists.makeOutboxLoader(SEARCH_RELAYS)(pubkey, relayHints)
|
return this.ctx.use(RelayLists).makeOutboxLoader(SEARCH_RELAYS)(pubkey, relayHints)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,7 +196,8 @@ export const getSigner = (session?: Session): ISigner | undefined => {
|
|||||||
if (isNip07Session(session)) return wrapSigner(new Nip07Signer())
|
if (isNip07Session(session)) return wrapSigner(new Nip07Signer())
|
||||||
if (isNip01Session(session)) return wrapSigner(new Nip01Signer(session.secret))
|
if (isNip01Session(session)) return wrapSigner(new Nip01Signer(session.secret))
|
||||||
if (isNip55Session(session)) return wrapSigner(new Nip55Signer(session.signer, session.pubkey))
|
if (isNip55Session(session)) return wrapSigner(new Nip55Signer(session.signer, session.pubkey))
|
||||||
if (isPomadeSession(session)) return wrapSigner(new PomadeSigner(new PomadeClient(session.clientOptions)))
|
if (isPomadeSession(session))
|
||||||
|
return wrapSigner(new PomadeSigner(new PomadeClient(session.clientOptions)))
|
||||||
if (isNip46Session(session)) {
|
if (isNip46Session(session)) {
|
||||||
const {
|
const {
|
||||||
secret: clientSecret,
|
secret: clientSecret,
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
getEventsById,
|
||||||
|
deriveEventsById,
|
||||||
|
deriveEvents,
|
||||||
|
makeDeriveEvent,
|
||||||
|
getEventsByIdByUrl,
|
||||||
|
deriveEventsByIdByUrl,
|
||||||
|
getEventsByIdForUrl,
|
||||||
|
deriveEventsByIdForUrl,
|
||||||
|
deriveItemsByKey,
|
||||||
|
deriveIsDeleted,
|
||||||
|
} from "@welshman/store"
|
||||||
|
import type {
|
||||||
|
EventsByIdOptions,
|
||||||
|
EventOptions,
|
||||||
|
EventsByIdByUrlOptions,
|
||||||
|
EventsByIdForUrlOptions,
|
||||||
|
ItemsByKeyOptions,
|
||||||
|
} from "@welshman/store"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store/derivation utilities bound to the client's repository and tracker. Reach
|
||||||
|
* it via `client.use(Stores)`.
|
||||||
|
*/
|
||||||
|
export class Stores {
|
||||||
|
constructor(readonly ctx: IClient) {}
|
||||||
|
|
||||||
|
getEventsById = (options: Omit<EventsByIdOptions, "repository">) =>
|
||||||
|
getEventsById({...options, repository: this.ctx.repository})
|
||||||
|
|
||||||
|
deriveEventsById = (options: Omit<EventsByIdOptions, "repository">) =>
|
||||||
|
deriveEventsById({...options, repository: this.ctx.repository})
|
||||||
|
|
||||||
|
deriveEvents = (options: Omit<EventsByIdOptions, "repository">) =>
|
||||||
|
deriveEvents({...options, repository: this.ctx.repository})
|
||||||
|
|
||||||
|
makeDeriveEvent = (options: Omit<EventOptions, "repository">) =>
|
||||||
|
makeDeriveEvent({...options, repository: this.ctx.repository})
|
||||||
|
|
||||||
|
getEventsByIdByUrl = (options: Omit<EventsByIdByUrlOptions, "tracker" | "repository">) =>
|
||||||
|
getEventsByIdByUrl({...options, tracker: this.ctx.tracker, repository: this.ctx.repository})
|
||||||
|
|
||||||
|
deriveEventsByIdByUrl = (options: Omit<EventsByIdByUrlOptions, "tracker" | "repository">) =>
|
||||||
|
deriveEventsByIdByUrl({...options, tracker: this.ctx.tracker, repository: this.ctx.repository})
|
||||||
|
|
||||||
|
getEventsByIdForUrl = (options: Omit<EventsByIdForUrlOptions, "tracker" | "repository">) =>
|
||||||
|
getEventsByIdForUrl({...options, tracker: this.ctx.tracker, repository: this.ctx.repository})
|
||||||
|
|
||||||
|
deriveEventsByIdForUrl = (options: Omit<EventsByIdForUrlOptions, "tracker" | "repository">) =>
|
||||||
|
deriveEventsByIdForUrl({...options, tracker: this.ctx.tracker, repository: this.ctx.repository})
|
||||||
|
|
||||||
|
deriveItemsByKey = <T>(options: Omit<ItemsByKeyOptions<T>, "repository">) =>
|
||||||
|
deriveItemsByKey<T>({...options, repository: this.ctx.repository})
|
||||||
|
|
||||||
|
deriveIsDeleted = (event: TrustedEvent) => deriveIsDeleted(this.ctx.repository, event)
|
||||||
|
}
|
||||||
+12
-14
@@ -1,7 +1,8 @@
|
|||||||
import {isSignedEvent} from "@welshman/util"
|
import {isSignedEvent} from "@welshman/util"
|
||||||
import type {Filter, SignedEvent} from "@welshman/util"
|
import type {Filter, SignedEvent} from "@welshman/util"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
import type {Relays} from "./relays.js"
|
import {Networking} from "./networking.js"
|
||||||
|
import {Relays} from "./relays.js"
|
||||||
|
|
||||||
export type AppSyncOpts = {
|
export type AppSyncOpts = {
|
||||||
relays: string[]
|
relays: string[]
|
||||||
@@ -12,19 +13,16 @@ export type AppSyncOpts = {
|
|||||||
* Negentropy-aware sync. Pulls/pushes events between the local repository and a
|
* Negentropy-aware sync. Pulls/pushes events between the local repository and a
|
||||||
* set of relays, using NIP-77 reconciliation where the relay supports it and
|
* set of relays, using NIP-77 reconciliation where the relay supports it and
|
||||||
* falling back to plain request/publish otherwise. Reads NIP-11 relay profiles
|
* falling back to plain request/publish otherwise. Reads NIP-11 relay profiles
|
||||||
* from the injected `Relays` collection to detect negentropy support.
|
* from the `Relays` collection to detect negentropy support.
|
||||||
*/
|
*/
|
||||||
export class Sync {
|
export class Sync {
|
||||||
constructor(
|
constructor(readonly ctx: IClient) {}
|
||||||
readonly ctx: ClientContext,
|
|
||||||
readonly relays: Relays,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
query = (filters: Filter[]) =>
|
query = (filters: Filter[]) =>
|
||||||
this.ctx.repository.query(filters, {shouldSort: filters.every(f => f.limit === undefined)})
|
this.ctx.repository.query(filters, {shouldSort: filters.every(f => f.limit === undefined)})
|
||||||
|
|
||||||
hasNegentropy = (url: string) => {
|
hasNegentropy = (url: string) => {
|
||||||
const relay = this.relays.get(url)
|
const relay = this.ctx.use(Relays).get(url)
|
||||||
|
|
||||||
if (relay?.negentropy) return true
|
if (relay?.negentropy) return true
|
||||||
if (relay?.supported_nips?.includes?.("77")) return true
|
if (relay?.supported_nips?.includes?.("77")) return true
|
||||||
@@ -34,27 +32,27 @@ export class Sync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pull = async ({relays, filters}: AppSyncOpts) => {
|
pull = async ({relays, filters}: AppSyncOpts) => {
|
||||||
|
const net = this.ctx.use(Networking)
|
||||||
const events = this.query(filters).filter(isSignedEvent)
|
const events = this.query(filters).filter(isSignedEvent)
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
relays.map(async relay => {
|
relays.map(async relay => {
|
||||||
await (this.hasNegentropy(relay)
|
await (this.hasNegentropy(relay)
|
||||||
? this.ctx.pull({filters, events, relays: [relay]})
|
? net.pull({filters, events, relays: [relay]})
|
||||||
: this.ctx.request({filters, relays: [relay], autoClose: true}))
|
: net.request({filters, relays: [relay], autoClose: true}))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
push = async ({relays, filters}: AppSyncOpts) => {
|
push = async ({relays, filters}: AppSyncOpts) => {
|
||||||
|
const net = this.ctx.use(Networking)
|
||||||
const events = this.query(filters).filter(isSignedEvent)
|
const events = this.query(filters).filter(isSignedEvent)
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
relays.map(async relay => {
|
relays.map(async relay => {
|
||||||
await (this.hasNegentropy(relay)
|
await (this.hasNegentropy(relay)
|
||||||
? this.ctx.push({filters, events, relays: [relay]})
|
? net.push({filters, events, relays: [relay]})
|
||||||
: Promise.all(
|
: Promise.all(events.map((event: SignedEvent) => net.publish({event, relays: [relay]}))))
|
||||||
events.map((event: SignedEvent) => this.ctx.publish({event, relays: [relay]})),
|
|
||||||
))
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-18
@@ -8,9 +8,9 @@ import {
|
|||||||
isShareableRelayUrl,
|
isShareableRelayUrl,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import type {Router} from "./router.js"
|
import {Router} from "./router.js"
|
||||||
import type {Profiles} from "./profiles.js"
|
import {Profiles} from "./profiles.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builders for nostr tags (p/e/a/q/zap/reply/comment/reaction). Needs the router
|
* Builders for nostr tags (p/e/a/q/zap/reply/comment/reaction). Needs the router
|
||||||
@@ -18,29 +18,25 @@ import type {ClientContext} from "./client.js"
|
|||||||
* user to avoid self-tagging.
|
* user to avoid self-tagging.
|
||||||
*/
|
*/
|
||||||
export class Tags {
|
export class Tags {
|
||||||
constructor(
|
constructor(readonly ctx: IClient) {}
|
||||||
readonly ctx: ClientContext,
|
|
||||||
readonly router: Router,
|
|
||||||
readonly profiles: Profiles,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
tagZapSplit = (pubkey: string, split = 1) => [
|
tagZapSplit = (pubkey: string, split = 1) => [
|
||||||
"zap",
|
"zap",
|
||||||
pubkey,
|
pubkey,
|
||||||
this.router.FromPubkey(pubkey).getUrl() || "",
|
this.ctx.use(Router).FromPubkey(pubkey).getUrl() || "",
|
||||||
String(split),
|
String(split),
|
||||||
]
|
]
|
||||||
|
|
||||||
tagPubkey = (pubkey: string) => [
|
tagPubkey = (pubkey: string) => [
|
||||||
"p",
|
"p",
|
||||||
pubkey,
|
pubkey,
|
||||||
this.router.FromPubkey(pubkey).getUrl() || "",
|
this.ctx.use(Router).FromPubkey(pubkey).getUrl() || "",
|
||||||
this.profiles.display(pubkey),
|
this.ctx.use(Profiles).display(pubkey),
|
||||||
]
|
]
|
||||||
|
|
||||||
tagEvent = (event: TrustedEvent, url = "", mark = "") => {
|
tagEvent = (event: TrustedEvent, url = "", mark = "") => {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
url = this.router.Event(event).getUrl() || ""
|
url = this.ctx.use(Router).Event(event).getUrl() || ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = [["e", event.id, url, mark, event.pubkey]]
|
const tags = [["e", event.id, url, mark, event.pubkey]]
|
||||||
@@ -58,7 +54,7 @@ export class Tags {
|
|||||||
).map(pubkey => this.tagPubkey(pubkey))
|
).map(pubkey => this.tagPubkey(pubkey))
|
||||||
|
|
||||||
tagEventForQuote = (event: TrustedEvent, relay?: string) => {
|
tagEventForQuote = (event: TrustedEvent, relay?: string) => {
|
||||||
const hint = relay || this.router.Event(event).getUrl() || ""
|
const hint = relay || this.ctx.use(Router).Event(event).getUrl() || ""
|
||||||
|
|
||||||
return ["q", event.id, hint, event.pubkey]
|
return ["q", event.id, hint, event.pubkey]
|
||||||
}
|
}
|
||||||
@@ -68,13 +64,13 @@ export class Tags {
|
|||||||
const {roots, replies} = getReplyTags(event.tags)
|
const {roots, replies} = getReplyTags(event.tags)
|
||||||
const parents = roots.length > 0 ? roots : replies
|
const parents = roots.length > 0 ? roots : replies
|
||||||
const mark = parents.length > 0 ? "reply" : "root"
|
const mark = parents.length > 0 ? "reply" : "root"
|
||||||
const hint = relay || this.router.Event(event).getUrl() || ""
|
const hint = relay || this.ctx.use(Router).Event(event).getUrl() || ""
|
||||||
|
|
||||||
// If the parent included roots use them, otherwise use replies as a fallback
|
// If the parent included roots use them, otherwise use replies as a fallback
|
||||||
for (const [k, id, originalHint = "", _, pubkey = ""] of parents) {
|
for (const [k, id, originalHint = "", _, pubkey = ""] of parents) {
|
||||||
const hint = isShareableRelayUrl(originalHint)
|
const hint = isShareableRelayUrl(originalHint)
|
||||||
? originalHint
|
? originalHint
|
||||||
: this.router.EventRoots(event).getUrl()
|
: this.ctx.use(Router).EventRoots(event).getUrl()
|
||||||
|
|
||||||
tags.push([k, id, hint || "", "root", pubkey])
|
tags.push([k, id, hint || "", "root", pubkey])
|
||||||
}
|
}
|
||||||
@@ -91,8 +87,8 @@ export class Tags {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tagEventForComment = (event: TrustedEvent, relay?: string) => {
|
tagEventForComment = (event: TrustedEvent, relay?: string) => {
|
||||||
const pubkeyHint = this.router.FromPubkey(event.pubkey).getUrl() || ""
|
const pubkeyHint = this.ctx.use(Router).FromPubkey(event.pubkey).getUrl() || ""
|
||||||
const eventHint = relay || this.router.Event(event).getUrl() || ""
|
const eventHint = relay || this.ctx.use(Router).Event(event).getUrl() || ""
|
||||||
const address = getAddress(event)
|
const address = getAddress(event)
|
||||||
const seenRoots = new Set<string>()
|
const seenRoots = new Set<string>()
|
||||||
const tags: string[][] = []
|
const tags: string[][] = []
|
||||||
@@ -126,7 +122,7 @@ export class Tags {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tagEventForReaction = (event: TrustedEvent, relay?: string) => {
|
tagEventForReaction = (event: TrustedEvent, relay?: string) => {
|
||||||
const hint = relay || this.router.Event(event).getUrl() || ""
|
const hint = relay || this.ctx.use(Router).Event(event).getUrl() || ""
|
||||||
const tags: string[][] = []
|
const tags: string[][] = []
|
||||||
|
|
||||||
// Mention the event's author
|
// Mention the event's author
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ import {
|
|||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {PublishStatus, PublishResult, PublishOptions, PublishResultsByRelay} from "@welshman/net"
|
import {PublishStatus, PublishResult, PublishOptions, PublishResultsByRelay} from "@welshman/net"
|
||||||
import {Nip01Signer, Nip59} from "@welshman/signer"
|
import {Nip01Signer, Nip59} from "@welshman/signer"
|
||||||
import type {ClientContext} from './client.js'
|
import type {IClient} from "./client.js"
|
||||||
import type {User} from './user.js'
|
import {Networking} from "./networking.js"
|
||||||
|
import type {User} from "./user.js"
|
||||||
|
|
||||||
export type ThunkOptions = Override<
|
export type ThunkOptions = Override<
|
||||||
PublishOptions,
|
PublishOptions,
|
||||||
{
|
{
|
||||||
user: User
|
user: User
|
||||||
client: ClientContext
|
client: IClient
|
||||||
event: EventTemplate
|
event: EventTemplate
|
||||||
recipient?: string
|
recipient?: string
|
||||||
delay?: number
|
delay?: number
|
||||||
@@ -111,7 +112,7 @@ export class Thunk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send it off
|
// Send it off
|
||||||
await this.options.client.publish({
|
await this.options.client.use(Networking).publish({
|
||||||
...this.options,
|
...this.options,
|
||||||
event,
|
event,
|
||||||
onSuccess: (result: PublishResult) => {
|
onSuccess: (result: PublishResult) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type {Readable} from "svelte/store"
|
|||||||
import {on} from "@welshman/lib"
|
import {on} from "@welshman/lib"
|
||||||
import {getTopicTagValues} from "@welshman/util"
|
import {getTopicTagValues} from "@welshman/util"
|
||||||
import {deriveItems} from "@welshman/store"
|
import {deriveItems} from "@welshman/store"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
|
|
||||||
export type Topic = {
|
export type Topic = {
|
||||||
name: string
|
name: string
|
||||||
@@ -18,7 +18,7 @@ export class Topics {
|
|||||||
byName: Readable<Map<string, Topic>>
|
byName: Readable<Map<string, Topic>>
|
||||||
all: Readable<Topic[]>
|
all: Readable<Topic[]>
|
||||||
|
|
||||||
constructor(readonly ctx: ClientContext) {
|
constructor(readonly ctx: IClient) {
|
||||||
const topicsByName = new Map<string, Topic>()
|
const topicsByName = new Map<string, Topic>()
|
||||||
|
|
||||||
const addTopic = (name: string) => {
|
const addTopic = (name: string) => {
|
||||||
|
|||||||
+18
-15
@@ -3,9 +3,9 @@ import type {Readable, Writable} from "svelte/store"
|
|||||||
import {max, throttle, addToMapKey, inc, dec} from "@welshman/lib"
|
import {max, throttle, addToMapKey, inc, dec} from "@welshman/lib"
|
||||||
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||||
import {throttled, getter} from "@welshman/store"
|
import {throttled, getter} from "@welshman/store"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
import type {FollowLists} from "./follows.js"
|
import {FollowLists} from "./follows.js"
|
||||||
import type {MuteLists} from "./mutes.js"
|
import {MuteLists} from "./mutes.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Web-of-trust scoring derived from follow and mute lists. The trust graph is
|
* Web-of-trust scoring derived from follow and mute lists. The trust graph is
|
||||||
@@ -23,12 +23,11 @@ export class Wot {
|
|||||||
private getWotGraphStore: () => Map<string, number>
|
private getWotGraphStore: () => Map<string, number>
|
||||||
private getMaxWotStore: () => number | undefined
|
private getMaxWotStore: () => number | undefined
|
||||||
|
|
||||||
constructor(
|
constructor(readonly ctx: IClient) {
|
||||||
readonly ctx: ClientContext,
|
const followLists = this.ctx.use(FollowLists)
|
||||||
readonly followLists: FollowLists,
|
const muteLists = this.ctx.use(MuteLists)
|
||||||
readonly muteLists: MuteLists,
|
|
||||||
) {
|
this.followersByPubkey = derived(throttled(1000, followLists.all), lists => {
|
||||||
this.followersByPubkey = derived(throttled(1000, this.followLists.all), lists => {
|
|
||||||
const $followersByPubkey = new Map<string, Set<string>>()
|
const $followersByPubkey = new Map<string, Set<string>>()
|
||||||
|
|
||||||
for (const list of lists) {
|
for (const list of lists) {
|
||||||
@@ -40,7 +39,7 @@ export class Wot {
|
|||||||
return $followersByPubkey
|
return $followersByPubkey
|
||||||
})
|
})
|
||||||
|
|
||||||
this.mutersByPubkey = derived(throttled(1000, this.muteLists.all), lists => {
|
this.mutersByPubkey = derived(throttled(1000, muteLists.all), lists => {
|
||||||
const $mutersByPubkey = new Map<string, Set<string>>()
|
const $mutersByPubkey = new Map<string, Set<string>>()
|
||||||
|
|
||||||
for (const list of lists) {
|
for (const list of lists) {
|
||||||
@@ -61,13 +60,15 @@ export class Wot {
|
|||||||
this.getWotGraphStore = getter(this.wotGraph)
|
this.getWotGraphStore = getter(this.wotGraph)
|
||||||
this.getMaxWotStore = getter(this.maxWot)
|
this.getMaxWotStore = getter(this.maxWot)
|
||||||
|
|
||||||
this.followLists.subscribe(this.buildGraph)
|
followLists.subscribe(this.buildGraph)
|
||||||
this.muteLists.subscribe(this.buildGraph)
|
muteLists.subscribe(this.buildGraph)
|
||||||
}
|
}
|
||||||
|
|
||||||
getFollows = (pubkey: string) => getPubkeyTagValues(getListTags(this.followLists.get(pubkey)))
|
getFollows = (pubkey: string) =>
|
||||||
|
getPubkeyTagValues(getListTags(this.ctx.use(FollowLists).get(pubkey)))
|
||||||
|
|
||||||
getMutes = (pubkey: string) => getPubkeyTagValues(getListTags(this.muteLists.get(pubkey)))
|
getMutes = (pubkey: string) =>
|
||||||
|
getPubkeyTagValues(getListTags(this.ctx.use(MuteLists).get(pubkey)))
|
||||||
|
|
||||||
getNetwork = (pubkey: string) => {
|
getNetwork = (pubkey: string) => {
|
||||||
const pubkeys = new Set(this.getFollows(pubkey))
|
const pubkeys = new Set(this.getFollows(pubkey))
|
||||||
@@ -105,7 +106,9 @@ export class Wot {
|
|||||||
buildGraph = throttle(1000, () => {
|
buildGraph = throttle(1000, () => {
|
||||||
const $pubkey = this.ctx.user?.pubkey
|
const $pubkey = this.ctx.user?.pubkey
|
||||||
const $graph = new Map<string, number>()
|
const $graph = new Map<string, number>()
|
||||||
const $follows = $pubkey ? this.getFollows($pubkey) : Array.from(this.followLists.keys())
|
const $follows = $pubkey
|
||||||
|
? this.getFollows($pubkey)
|
||||||
|
: Array.from(this.ctx.use(FollowLists).keys())
|
||||||
|
|
||||||
for (const follow of $follows) {
|
for (const follow of $follows) {
|
||||||
for (const pubkey of this.getFollows(follow)) {
|
for (const pubkey of this.getFollows(follow)) {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {getTagValues, zapFromEvent} from "@welshman/util"
|
|||||||
import type {Zapper, Zap, TrustedEvent} from "@welshman/util"
|
import type {Zapper, Zap, TrustedEvent} from "@welshman/util"
|
||||||
import {deriveDeduplicated} from "@welshman/store"
|
import {deriveDeduplicated} from "@welshman/store"
|
||||||
import {LoadableData} from "./clientData.js"
|
import {LoadableData} from "./clientData.js"
|
||||||
import type {ClientContext} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
import type {Profiles} from "./profiles.js"
|
import {Profiles} from "./profiles.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lightning zapper info, keyed by lnurl. A "local" loadable collection: items
|
* Lightning zapper info, keyed by lnurl. A "local" loadable collection: items
|
||||||
@@ -22,11 +22,7 @@ import type {Profiles} from "./profiles.js"
|
|||||||
* profiles collection to resolve a pubkey's lnurl.
|
* profiles collection to resolve a pubkey's lnurl.
|
||||||
*/
|
*/
|
||||||
export class Zappers extends LoadableData<Zapper> {
|
export class Zappers extends LoadableData<Zapper> {
|
||||||
constructor(
|
constructor(ctx: IClient) {
|
||||||
ctx: ClientContext,
|
|
||||||
readonly profiles: Profiles,
|
|
||||||
readonly options: {dufflepudUrl?: string} = {},
|
|
||||||
) {
|
|
||||||
super(ctx)
|
super(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,11 +41,11 @@ export class Zappers extends LoadableData<Zapper> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use dufflepud if it's set up to protect user privacy, otherwise fetch directly
|
// Use dufflepud if it's set up to protect user privacy, otherwise fetch directly
|
||||||
if (this.options.dufflepudUrl) {
|
if (this.ctx.config.dufflepudUrl) {
|
||||||
const hexUrls = valid.map(bech32ToHex)
|
const hexUrls = valid.map(bech32ToHex)
|
||||||
const res: any = await tryCatch(
|
const res: any = await tryCatch(
|
||||||
async () =>
|
async () =>
|
||||||
await postJson(`${this.options.dufflepudUrl}/zapper/info`, {lnurls: hexUrls}),
|
await postJson(`${this.ctx.config.dufflepudUrl}/zapper/info`, {lnurls: hexUrls}),
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const {lnurl, info} of res?.data || []) {
|
for (const {lnurl, info} of res?.data || []) {
|
||||||
@@ -71,7 +67,7 @@ export class Zappers extends LoadableData<Zapper> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
loadForPubkey = async (pubkey: string, relays: string[] = []) => {
|
loadForPubkey = async (pubkey: string, relays: string[] = []) => {
|
||||||
const $profile = await this.profiles.load(pubkey, relays)
|
const $profile = await this.ctx.use(Profiles).load(pubkey, relays)
|
||||||
|
|
||||||
return $profile?.lnurl ? this.load($profile.lnurl) : undefined
|
return $profile?.lnurl ? this.load($profile.lnurl) : undefined
|
||||||
}
|
}
|
||||||
@@ -80,7 +76,7 @@ export class Zappers extends LoadableData<Zapper> {
|
|||||||
this.loadForPubkey(pubkey, relays)
|
this.loadForPubkey(pubkey, relays)
|
||||||
|
|
||||||
return deriveDeduplicated(
|
return deriveDeduplicated(
|
||||||
[this.index, this.profiles.derive(pubkey, relays)],
|
[this.index, this.ctx.use(Profiles).derive(pubkey, relays)],
|
||||||
([$zappersByLnurl, $profile]) =>
|
([$zappersByLnurl, $profile]) =>
|
||||||
$profile?.lnurl ? $zappersByLnurl.get($profile.lnurl) : undefined,
|
$profile?.lnurl ? $zappersByLnurl.get($profile.lnurl) : undefined,
|
||||||
)
|
)
|
||||||
@@ -90,7 +86,7 @@ export class Zappers extends LoadableData<Zapper> {
|
|||||||
const pubkeys = getTagValues("zap", event.tags)
|
const pubkeys = getTagValues("zap", event.tags)
|
||||||
|
|
||||||
if (pubkeys.length > 0) {
|
if (pubkeys.length > 0) {
|
||||||
const profiles = await Promise.all(pubkeys.map(pubkey => this.profiles.load(pubkey)))
|
const profiles = await Promise.all(pubkeys.map(pubkey => this.ctx.use(Profiles).load(pubkey)))
|
||||||
const lnurls = removeUndefined(profiles.map(profile => profile?.lnurl))
|
const lnurls = removeUndefined(profiles.map(profile => profile?.lnurl))
|
||||||
|
|
||||||
if (lnurls.length > 0) {
|
if (lnurls.length > 0) {
|
||||||
@@ -98,7 +94,7 @@ export class Zappers extends LoadableData<Zapper> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = await this.profiles.load(event.pubkey)
|
const profile = await this.ctx.use(Profiles).load(event.pubkey)
|
||||||
|
|
||||||
return removeUndefined([profile?.lnurl])
|
return removeUndefined([profile?.lnurl])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user