rework context

This commit is contained in:
Jon Staab
2024-09-05 13:38:48 -07:00
parent 233a4e3576
commit 44f29c9da4
19 changed files with 157 additions and 138 deletions
+1
View File
@@ -20,6 +20,7 @@
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": ["error", {"args": "none", "destructuredArrayIgnorePattern": "^_"}], "@typescript-eslint/no-unused-vars": ["error", {"args": "none", "destructuredArrayIgnorePattern": "^_"}],
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"no-useless-escape": "off", "no-useless-escape": "off",
"prefer-const": ["error", {"destructuring": "all"}], "prefer-const": ["error", {"destructuring": "all"}],
+45
View File
@@ -0,0 +1,45 @@
import {partition} from "@welshman/lib"
import {defaultOptimizeSubscriptions, getDefaultNetContext as originalGetDefaultNetContext} from "@welshman/net"
import type {Subscription, RelaysAndFilters} from "@welshman/net"
import {unionFilters, isSignedEvent, hasValidSignature} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {tracker, repository} from './core'
import {makeRouter, getFilterSelections} from './router'
import {onAuth, getSession} from './session'
import type {Router} from './router'
export type AppContext = {
router: Router
requestDelay: number
requestTimeout: number
dufflepudUrl?: string
}
export const getDefaultNetContext = () => ({
...originalGetDefaultNetContext(),
onAuth: onAuth,
onEvent: (url: string, event: TrustedEvent) => tracker.track(event.id, url),
isDeleted: (url: string, event: TrustedEvent) => repository.isDeleted(event),
hasValidSignature: (event: TrustedEvent) =>
getSession(event.pubkey) || (isSignedEvent(event) && hasValidSignature(event)),
optimizeSubscriptions: (subs: Subscription[]) => {
const [withRelays, withoutRelays] = partition(sub => sub.request.relays.length > 0, subs)
const filters = unionFilters(withoutRelays.flatMap(sub => sub.request.filters))
const selections: RelaysAndFilters[] = defaultOptimizeSubscriptions(withRelays)
if (filters.length > 0) {
for (const selection of getFilterSelections(filters)) {
selections.push(selection)
}
}
return selections
},
})
export const getDefaultAppContext = () => ({
router: makeRouter(),
requestDelay: 50,
requestTimeout: 3000,
})
+4 -16
View File
@@ -1,21 +1,9 @@
import {isNil} from "@welshman/lib" import {ctx, isNil} from "@welshman/lib"
import {Repository, Relay, LOCAL_RELAY_URL, getFilterResultCardinality} from "@welshman/util" import {Repository, Relay, LOCAL_RELAY_URL, getFilterResultCardinality} from "@welshman/util"
import type {TrustedEvent, Filter} from "@welshman/util" import type {TrustedEvent, Filter} from "@welshman/util"
import {Tracker, subscribe as baseSubscribe} from "@welshman/net" import {Tracker, subscribe as baseSubscribe} from "@welshman/net"
import type {SubscribeRequestWithHandlers} from "@welshman/net" import type {SubscribeRequestWithHandlers} from "@welshman/net"
import {createEventStore} from "@welshman/store" import {createEventStore} from "@welshman/store"
import type {Router} from './router'
export const AppContext: {
router: Router,
requestDelay: number
requestTimeout: number
dufflepudUrl?: string
} = {
router: undefined as unknown as Router,
requestDelay: 50,
requestTimeout: 3000,
}
export const repository = new Repository<TrustedEvent>() export const repository = new Repository<TrustedEvent>()
@@ -52,8 +40,8 @@ export const subscribe = (request: PartialSubscribeRequest) => {
} }
// Make sure to query our local relay too // Make sure to query our local relay too
const delay = AppContext.requestDelay const delay = ctx.app.requestDelay
const timeout = AppContext.requestTimeout const timeout = ctx.app.requestTimeout
const sub = baseSubscribe({delay, authTimeout: timeout, relays: [], ...request}) const sub = baseSubscribe({delay, authTimeout: timeout, relays: [], ...request})
sub.emitter.on("event", (url: string, e: TrustedEvent) => { sub.emitter.on("event", (url: string, e: TrustedEvent) => {
@@ -72,7 +60,7 @@ export const subscribe = (request: PartialSubscribeRequest) => {
export const load = (request: PartialSubscribeRequest) => export const load = (request: PartialSubscribeRequest) =>
new Promise<TrustedEvent[]>(resolve => { new Promise<TrustedEvent[]>(resolve => {
const sub = subscribe({closeOnEose: true, timeout: AppContext.requestTimeout, ...request}) const sub = subscribe({closeOnEose: true, timeout: ctx.app.requestTimeout, ...request})
const events: TrustedEvent[] = [] const events: TrustedEvent[] = []
sub.emitter.on("event", (url: string, e: TrustedEvent) => events.push(e)) sub.emitter.on("event", (url: string, e: TrustedEvent) => events.push(e))
+3 -4
View File
@@ -1,8 +1,7 @@
import {writable, derived} from 'svelte/store' import {writable, derived} from 'svelte/store'
import {withGetter} from '@welshman/store' import {withGetter} from '@welshman/store'
import {type SubscribeRequest} from "@welshman/net" import {type SubscribeRequest} from "@welshman/net"
import {uniq, uniqBy, batcher, postJson, last} from '@welshman/lib' import {ctx, uniq, uniqBy, batcher, postJson, last} from '@welshman/lib'
import {AppContext} from './core'
import {collection} from './collection' import {collection} from './collection'
import {deriveProfile} from './profiles' import {deriveProfile} from './profiles'
@@ -16,10 +15,10 @@ export type Handle = {
export const handles = withGetter(writable<Handle[]>([])) export const handles = withGetter(writable<Handle[]>([]))
export const fetchHandles = (handles: string[]) => { export const fetchHandles = (handles: string[]) => {
const base = AppContext.dufflepudUrl! const base = ctx.app.dufflepudUrl!
if (!base) { if (!base) {
throw new Error("AppContext.dufflepudUrl is required to fetch nip05 info") throw new Error("ctx.app.dufflepudUrl is required to fetch nip05 info")
} }
const res: any = postJson(`${base}/handle/info`, {handles}) const res: any = postJson(`${base}/handle/info`, {handles})
+1 -31
View File
@@ -1,3 +1,4 @@
export * from './context'
export * from './core' export * from './core'
export * from './collection' export * from './collection'
export * from './freshness' export * from './freshness'
@@ -15,34 +16,3 @@ export * from './thunk'
export * from './topics' export * from './topics'
export * from './util' export * from './util'
export * from './zappers' export * from './zappers'
import {partition} from "@welshman/lib"
import {type Subscription, NetworkContext, defaultOptimizeSubscriptions} from "@welshman/net"
import {type TrustedEvent, unionFilters, isSignedEvent, hasValidSignature} from "@welshman/util"
import {tracker, repository, AppContext} from './core'
import {makeRouter, getFilterSelections} from './router'
import {onAuth, getSession} from './session'
export function* optimizeSubscriptions(subs: Subscription[]) {
const [withRelays, withoutRelays] = partition(sub => sub.request.relays.length > 0, subs)
const filters = unionFilters(withoutRelays.flatMap(sub => sub.request.filters))
yield* defaultOptimizeSubscriptions(withRelays)
if (filters.length > 0) {
yield* getFilterSelections(filters)
}
}
Object.assign(NetworkContext, {
onAuth,
onEvent: (url: string, event: TrustedEvent) => tracker.track(event.id, url),
isDeleted: (url: string, event: TrustedEvent) => repository.isDeleted(event),
hasValidSignature: (event: TrustedEvent) =>
getSession(event.pubkey) || (isSignedEvent(event) && hasValidSignature(event)),
optimizeSubscriptions,
})
Object.assign(AppContext, {
router: makeRouter(),
})
+4 -5
View File
@@ -1,9 +1,8 @@
import {writable, derived} from 'svelte/store' import {writable, derived} from 'svelte/store'
import {withGetter} from '@welshman/store' import {withGetter} from '@welshman/store'
import {groupBy, indexBy, batch, now, uniq, uniqBy, batcher, postJson} from '@welshman/lib' import {ctx, groupBy, indexBy, batch, now, uniq, uniqBy, batcher, postJson} from '@welshman/lib'
import {type RelayProfile} from "@welshman/util" import type {RelayProfile} from "@welshman/util"
import {AuthStatus, asMessage, type Connection, type SocketMessage} from '@welshman/net' import {AuthStatus, asMessage, type Connection, type SocketMessage} from '@welshman/net'
import {AppContext} from './core'
import {createSearch} from './util' import {createSearch} from './util'
import {collection} from './collection' import {collection} from './collection'
@@ -45,10 +44,10 @@ export const relaysByPubkey = derived(relays, $relays =>
) )
export const fetchRelayProfiles = (urls: string[]) => { export const fetchRelayProfiles = (urls: string[]) => {
const base = AppContext.dufflepudUrl! const base = ctx.app.dufflepudUrl!
if (!base) { if (!base) {
throw new Error("AppContext.dufflepudUrl is required to fetch relay metadata") throw new Error("ctx.app.dufflepudUrl is required to fetch relay metadata")
} }
const res: any = postJson(`${base}/relay/info`, {urls}) const res: any = postJson(`${base}/relay/info`, {urls})
+17 -22
View File
@@ -1,14 +1,14 @@
import { import {
intersection, first, switcher, throttleWithValue, clamp, last, splitAt, identity, sortBy, uniq, shuffle, intersection, first, switcher, throttleWithValue, clamp, last, splitAt, identity, sortBy, uniq, shuffle,
pushToMapKey, now, assoc, pushToMapKey, now, assoc, ctx,
} from '@welshman/lib' } from '@welshman/lib'
import { import {
Tags, getFilterId, unionFilters, isShareableRelayUrl, isCommunityAddress, isGroupAddress, isContextAddress, Tags, getFilterId, unionFilters, isShareableRelayUrl, isCommunityAddress, isGroupAddress, isContextAddress,
PROFILE, RELAYS, INBOX_RELAYS, FOLLOWS, PROFILE, RELAYS, INBOX_RELAYS, FOLLOWS,
} from '@welshman/util' } from '@welshman/util'
import type {TrustedEvent, Filter} from '@welshman/util' import type {TrustedEvent, Filter} from '@welshman/util'
import {NetworkContext, ConnectionStatus} from '@welshman/net' import {ConnectionStatus} from '@welshman/net'
import {AppContext} from './core' import type {RelaysAndFilters} from '@welshman/net'
import {pubkey} from './session' import {pubkey} from './session'
import {relaySelectionsByPubkey, getReadRelayUrls, getWriteRelayUrls, getRelayUrls} from './relaySelections' import {relaySelectionsByPubkey, getReadRelayUrls, getWriteRelayUrls, getRelayUrls} from './relaySelections'
import {relays, relaysByUrl} from './relays' import {relays, relaysByUrl} from './relays'
@@ -396,7 +396,7 @@ export const getRelayQuality = (url: string) => {
const relay = relaysByUrl.get().get(url) const relay = relaysByUrl.get().get(url)
const connect_count = relay?.stats?.connect_count || 0 const connect_count = relay?.stats?.connect_count || 0
const recent_errors = relay?.stats?.recent_errors || [] const recent_errors = relay?.stats?.recent_errors || []
const connection = NetworkContext.pool.get(url, {autoConnect: false}) const connection = ctx.net.pool.get(url, {autoConnect: false})
// If we haven't connected, consult our relay record and see if there has // If we haven't connected, consult our relay record and see if there has
// been a recent fault. If there has been, penalize the relay. If there have been several, // been a recent fault. If there has been, penalize the relay. If there have been several,
@@ -467,11 +467,6 @@ export const makeRouter = (options: Partial<RouterOptions> = {}) =>
// Infer relay selections from filters // Infer relay selections from filters
export type RelayFilters = {
relays: string[]
filters: Filter[]
}
export type FilterSelection = { export type FilterSelection = {
id: string, id: string,
filter: Filter, filter: Filter,
@@ -492,8 +487,8 @@ export const getFilterSelectionsForSearch = (state: FilterSelectionRuleState) =>
if (!state.filter.search) return false if (!state.filter.search) return false
const id = getFilterId(state.filter) const id = getFilterId(state.filter)
const relays = AppContext.router.options.getSearchRelays?.() || [] const relays = ctx.app.router.options.getSearchRelays?.() || []
const scenario = AppContext.router.product([id], relays) const scenario = ctx.app.router.product([id], relays)
state.selections.push(makeFilterSelection(id, state.filter, scenario)) state.selections.push(makeFilterSelection(id, state.filter, scenario))
@@ -505,12 +500,12 @@ export const getFilterSelectionsForContext = (state: FilterSelectionRuleState) =
if (contexts.length === 0) return false if (contexts.length === 0) return false
const scenario = AppContext.router.WithinMultipleContexts(contexts) const scenario = ctx.app.router.WithinMultipleContexts(contexts)
for (const {relay, values} of scenario.getSelections()) { for (const {relay, values} of scenario.getSelections()) {
const contextFilter = {...state.filter, "#a": Array.from(values)} const contextFilter = {...state.filter, "#a": Array.from(values)}
const id = getFilterId(contextFilter) const id = getFilterId(contextFilter)
const scenario = AppContext.router.product([id], [relay]) const scenario = ctx.app.router.product([id], [relay])
state.selections.push(makeFilterSelection(id, contextFilter, scenario)) state.selections.push(makeFilterSelection(id, contextFilter, scenario))
} }
@@ -524,8 +519,8 @@ export const getFilterSelectionsForIndexedKinds = (state: FilterSelectionRuleSta
if (kinds.length === 0) return false if (kinds.length === 0) return false
const id = getFilterId({...state.filter, kinds}) const id = getFilterId({...state.filter, kinds})
const relays = AppContext.router.options.getIndexerRelays?.() || [] const relays = ctx.app.router.options.getIndexerRelays?.() || []
const scenario = AppContext.router.product([id], relays) const scenario = ctx.app.router.product([id], relays)
state.selections.push(makeFilterSelection(id, state.filter, scenario)) state.selections.push(makeFilterSelection(id, state.filter, scenario))
@@ -536,7 +531,7 @@ export const getFilterSelectionsForAuthors = (state: FilterSelectionRuleState) =
if (!state.filter.authors) return false if (!state.filter.authors) return false
const id = getFilterId(state.filter) const id = getFilterId(state.filter)
const scenario = AppContext.router.FromPubkeys(state.filter.authors!).update(assoc('value', id)) const scenario = ctx.app.router.FromPubkeys(state.filter.authors!).update(assoc('value', id))
state.selections.push(makeFilterSelection(id, state.filter, scenario)) state.selections.push(makeFilterSelection(id, state.filter, scenario))
@@ -545,8 +540,8 @@ export const getFilterSelectionsForAuthors = (state: FilterSelectionRuleState) =
export const getFilterSelectionsForUser = (state: FilterSelectionRuleState) => { export const getFilterSelectionsForUser = (state: FilterSelectionRuleState) => {
const id = getFilterId(state.filter) const id = getFilterId(state.filter)
const relays = AppContext.router.ReadRelays().getUrls() const relays = ctx.app.router.ReadRelays().getUrls()
const scenario = AppContext.router.product([id], relays) const scenario = ctx.app.router.product([id], relays)
state.selections.push(makeFilterSelection(id, state.filter, scenario)) state.selections.push(makeFilterSelection(id, state.filter, scenario))
@@ -561,7 +556,7 @@ export const defaultFilterSelectionRules = [
getFilterSelectionsForUser, getFilterSelectionsForUser,
] ]
export const getFilterSelections = (filters: Filter[], rules: FilterSelectionRule[] = defaultFilterSelectionRules): RelayFilters[] => { export const getFilterSelections = (filters: Filter[], rules: FilterSelectionRule[] = defaultFilterSelectionRules): RelaysAndFilters[] => {
const scenarios: RouterScenario[] = [] const scenarios: RouterScenario[] = []
const filtersById = new Map<string, Filter>() const filtersById = new Map<string, Filter>()
@@ -584,7 +579,7 @@ export const getFilterSelections = (filters: Filter[], rules: FilterSelectionRul
// Use low redundancy because filters will be very low cardinality // Use low redundancy because filters will be very low cardinality
const selections = AppContext.router const selections = ctx.app.router
.merge(scenarios) .merge(scenarios)
.redundancy(1) .redundancy(1)
.getSelections() .getSelections()
@@ -594,8 +589,8 @@ export const getFilterSelections = (filters: Filter[], rules: FilterSelectionRul
})) }))
// Pubkey-based selections can get really big. Use the most popular relays for the long tail // Pubkey-based selections can get really big. Use the most popular relays for the long tail
const limit = AppContext.router.options.getLimit?.() || 8 const limit = ctx.app.router.options.getLimit?.() || 8
const redundancy = AppContext.router.options.getRedundancy?.() || 3 const redundancy = ctx.app.router.options.getRedundancy?.() || 3
const [keep, discard] = splitAt(limit, selections) const [keep, discard] = splitAt(limit, selections)
for (const target of keep.slice(0, redundancy)) { for (const target of keep.slice(0, redundancy)) {
+2 -3
View File
@@ -1,9 +1,8 @@
import {derived} from "svelte/store" import {derived} from "svelte/store"
import {memoize, omit, equals, assoc} from "@welshman/lib" import {ctx, memoize, omit, equals, assoc} from "@welshman/lib"
import {createEvent} from "@welshman/util" import {createEvent} from "@welshman/util"
import {withGetter, synced} from "@welshman/store" import {withGetter, synced} from "@welshman/store"
import {type Nip46Handler} from "@welshman/signer" import {type Nip46Handler} from "@welshman/signer"
import {NetworkContext} from "@welshman/net"
import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer} from "@welshman/signer" import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer} from "@welshman/signer"
export type Session = { export type Session = {
@@ -76,7 +75,7 @@ export const onAuth = async (url: string, challenge: string) => {
}), }),
) )
NetworkContext.pool.get(url).send(["AUTH", event]) ctx.net.pool.get(url).send(["AUTH", event])
return event return event
} }
+9
View File
@@ -0,0 +1,9 @@
import type {NetContext} from '@welshman/net'
import type {AppContext} from './context'
declare module "@welshman/lib" {
interface Context {
net: NetContext
app: AppContext
}
}
+3 -4
View File
@@ -2,18 +2,17 @@ import {writable, derived} from 'svelte/store'
import {withGetter} from '@welshman/store' import {withGetter} from '@welshman/store'
import {type Zapper} from '@welshman/util' import {type Zapper} from '@welshman/util'
import {type SubscribeRequest} from "@welshman/net" import {type SubscribeRequest} from "@welshman/net"
import {uniq, identity, bech32ToHex, tryCatch, uniqBy, batcher, postJson} from '@welshman/lib' import {ctx, uniq, identity, bech32ToHex, tryCatch, uniqBy, batcher, postJson} from '@welshman/lib'
import {AppContext} from './core'
import {collection} from './collection' import {collection} from './collection'
import {deriveProfile} from './profiles' import {deriveProfile} from './profiles'
export const zappers = withGetter(writable<Zapper[]>([])) export const zappers = withGetter(writable<Zapper[]>([]))
export const fetchZappers = (lnurls: string[]) => { export const fetchZappers = (lnurls: string[]) => {
const base = AppContext.dufflepudUrl! const base = ctx.app.dufflepudUrl!
if (!base) { if (!base) {
throw new Error("AppContext.dufflepudUrl is required to fetch zapper info") throw new Error("ctx.app.dufflepudUrl is required to fetch zapper info")
} }
const zappersByLnurl = new Map<string, Zapper>() const zappersByLnurl = new Map<string, Zapper>()
+5
View File
@@ -0,0 +1,5 @@
import type {Context} from '@welshman/lib'
export const ctx: Context = {}
export const setContext = (newCtx: Context) => Object.assign(ctx, newCtx)
+2
View File
@@ -13,6 +13,8 @@ export type Maybe<T> = T | undefined
export const now = () => Math.round(Date.now() / 1000) export const now = () => Math.round(Date.now() / 1000)
export const noop = (...args: unknown[]) => undefined
export const first = <T>(xs: T[], ...args: unknown[]) => xs[0] export const first = <T>(xs: T[], ...args: unknown[]) => xs[0]
export const last = <T>(xs: T[], ...args: unknown[]) => xs[xs.length - 1] export const last = <T>(xs: T[], ...args: unknown[]) => xs[xs.length - 1]
+1
View File
@@ -1,3 +1,4 @@
export * from './Context'
export * from './Deferred' export * from './Deferred'
export * from './Emitter' export * from './Emitter'
export * from './Fluent' export * from './Fluent'
+3
View File
@@ -0,0 +1,3 @@
declare module "@welshman/lib" {
export interface Context {}
}
+31 -36
View File
@@ -1,45 +1,40 @@
import {uniq} from '@welshman/lib' import {ctx, uniq, noop, always} from '@welshman/lib'
import {matchFilters, unionFilters, isSignedEvent, hasValidSignature} from '@welshman/util' import {matchFilters, unionFilters, isSignedEvent, hasValidSignature} from '@welshman/util'
import type {Filter, TrustedEvent} from '@welshman/util' import type {Filter, TrustedEvent} from '@welshman/util'
import {Pool} from "./Pool" import {Pool} from "./Pool"
import {Executor} from "./Executor" import {Executor} from "./Executor"
import {Relays} from "./target/Relays" import {Relays} from "./target/Relays"
import type {Subscription} from "./Subscribe" import type {Subscription, RelaysAndFilters} from "./Subscribe"
export const defaultPool = new Pool() export type NetContext = {
pool: Pool
export const defaultGetExecutor = (relays: string[]) => getExecutor: (relays: string[]) => Executor
new Executor(new Relays(relays.map((relay: string) => NetworkContext.pool.get(relay)))) onEvent: (url: string, event: TrustedEvent) => void
onAuth: (url: string, challenge: string) => void
const defaultOnEvent = (url: string, event: TrustedEvent) => null onOk: (url: string, id: string, ok: boolean, message: string) => void
isDeleted: (url: string, event: TrustedEvent) => boolean
const defaultOnAuth = (url: string, challenge: string) => null hasValidSignature: (url: string, event: TrustedEvent) => boolean
matchFilters: (url: string, filters: Filter[], event: TrustedEvent) => boolean
const defaultOnOk = (url: string, id: string, ok: boolean, message: string) => null optimizeSubscriptions: (subs: Subscription[]) => RelaysAndFilters[]
const defaultIsDeleted = (url: string, event: TrustedEvent) => false
const defaultHasValidSignature = (url: string, event: TrustedEvent) => isSignedEvent(event) && hasValidSignature(event)
const defaultMatchFilters = (url: string, filters: Filter[], event: TrustedEvent) => matchFilters(filters, event)
export function* defaultOptimizeSubscriptions(subs: Subscription[]) {
for (const relay of uniq(subs.flatMap(sub => sub.request.relays || []))) {
const relaySubs = subs.filter(sub => sub.request.relays.includes(relay))
const filters = unionFilters(relaySubs.flatMap(sub => sub.request.filters))
yield {relays: [relay], filters}
}
} }
export const NetworkContext = { export const defaultOptimizeSubscriptions = (subs: Subscription[]) =>
pool: defaultPool, uniq(subs.flatMap(sub => sub.request.relays || []))
getExecutor: defaultGetExecutor, .map(relay => {
onEvent: defaultOnEvent, const relaySubs = subs.filter(sub => sub.request.relays.includes(relay))
onAuth: defaultOnAuth, const filters = unionFilters(relaySubs.flatMap(sub => sub.request.filters))
onOk: defaultOnOk,
isDeleted: defaultIsDeleted, return {relays: [relay], filters}
hasValidSignature: defaultHasValidSignature, })
matchFilters: defaultMatchFilters,
export const getDefaultNetContext = () => ({
onOk: noop,
onAuth: noop,
onEvent: noop,
pool: new Pool(),
isDeleted: always(false),
getExecutor: (relays: string[]) => new Executor(new Relays(relays.map((relay: string) => ctx.net.pool.get(relay)))),
hasValidSignature: (url: string, event: TrustedEvent) => isSignedEvent(event) && hasValidSignature(event),
matchFilters: (url: string, filters: Filter[], event: TrustedEvent) => matchFilters(filters, event),
optimizeSubscriptions: defaultOptimizeSubscriptions, optimizeSubscriptions: defaultOptimizeSubscriptions,
} })
+5 -5
View File
@@ -1,8 +1,8 @@
import {ctx} from '@welshman/lib'
import type {Emitter} from '@welshman/lib' import type {Emitter} from '@welshman/lib'
import type {SignedEvent, Filter} from '@welshman/util' import type {SignedEvent, Filter} from '@welshman/util'
import type {Message} from './Socket' import type {Message} from './Socket'
import type {Connection} from './Connection' import type {Connection} from './Connection'
import {NetworkContext} from './Context'
export type Target = Emitter & { export type Target = Emitter & {
connections: Connection[] connections: Connection[]
@@ -22,8 +22,8 @@ const createSubId = (prefix: string) => [prefix, Math.random().toString().slice(
export class Executor { export class Executor {
constructor(readonly target: Target) { constructor(readonly target: Target) {
target.on('AUTH', NetworkContext.onAuth) target.on('AUTH', ctx.net.onAuth)
target.on('OK', NetworkContext.onOk) target.on('OK', ctx.net.onOk)
} }
subscribe(filters: Filter[], {onEvent, onEose}: SubscribeOpts = {}) { subscribe(filters: Filter[], {onEvent, onEose}: SubscribeOpts = {}) {
@@ -33,7 +33,7 @@ export class Executor {
const eventListener = (url: string, subid: string, e: SignedEvent) => { const eventListener = (url: string, subid: string, e: SignedEvent) => {
if (subid === id) { if (subid === id) {
NetworkContext.onEvent(url, e) ctx.net.onEvent(url, e)
onEvent?.(url, e) onEvent?.(url, e)
} }
} }
@@ -64,7 +64,7 @@ export class Executor {
publish(event: SignedEvent, {verb = 'EVENT', onOk, onError}: PublishOpts = {}) { publish(event: SignedEvent, {verb = 'EVENT', onOk, onError}: PublishOpts = {}) {
const okListener = (url: string, id: string, ...payload: any[]) => { const okListener = (url: string, id: string, ...payload: any[]) => {
if (id === event.id) { if (id === event.id) {
NetworkContext.onEvent(url, event) ctx.net.onEvent(url, event)
onOk?.(url, id, ...payload) onOk?.(url, id, ...payload)
} }
} }
+2 -3
View File
@@ -1,8 +1,7 @@
import {Emitter, now, randomId, defer} from '@welshman/lib' import {ctx, Emitter, now, randomId, defer} from '@welshman/lib'
import type {Deferred} from '@welshman/lib' import type {Deferred} from '@welshman/lib'
import {asSignedEvent} from '@welshman/util' import {asSignedEvent} from '@welshman/util'
import type {SignedEvent} from '@welshman/util' import type {SignedEvent} from '@welshman/util'
import {NetworkContext} from './Context'
export enum PublishStatus { export enum PublishStatus {
Pending = "pending", Pending = "pending",
@@ -44,7 +43,7 @@ export const makePublish = (request: PublishRequest) => {
export const publish = (request: PublishRequest) => { export const publish = (request: PublishRequest) => {
const pub = makePublish(request) const pub = makePublish(request)
const event = asSignedEvent(request.event) const event = asSignedEvent(request.event)
const executor = NetworkContext.getExecutor(request.relays) const executor = ctx.net.getExecutor(request.relays)
const abort = (reason: PublishStatus) => { const abort = (reason: PublishStatus) => {
for (const [url, status] of pub.status.entries()) { for (const [url, status] of pub.status.entries()) {
+11 -9
View File
@@ -1,16 +1,15 @@
import {Emitter, max, chunk, randomId, once, groupBy, uniq} from '@welshman/lib' import {ctx, Emitter, max, chunk, randomId, once, groupBy, uniq} from '@welshman/lib'
import {matchFilters, unionFilters, TrustedEvent} from '@welshman/util' import {matchFilters, unionFilters, TrustedEvent} from '@welshman/util'
import type {Filter} from '@welshman/util' import type {Filter} from '@welshman/util'
import {Tracker} from "./Tracker" import {Tracker} from "./Tracker"
import {Connection} from './Connection' import {Connection} from './Connection'
import {NetworkContext} from './Context'
// `subscribe` is a super function that handles batching subscriptions by merging // `subscribe` is a super function that handles batching subscriptions by merging
// them based on parameters (filters and subscribe opts), then splits them by relay. // them based on parameters (filters and subscribe opts), then splits them by relay.
// This results in fewer REQs being opened per connection, fewer duplicate events // This results in fewer REQs being opened per connection, fewer duplicate events
// being downloaded, and therefore less signature validation. // being downloaded, and therefore less signature validation.
// //
// Behavior can be further configured using NetworkContext. This can be useful for // Behavior can be further configured using ctx.net. This can be useful for
// adding support for querying a local cache like a relay, tracking deleted events, // adding support for querying a local cache like a relay, tracking deleted events,
// and bypassing validation for trusted relays. // and bypassing validation for trusted relays.
// //
@@ -30,9 +29,12 @@ export enum SubscriptionEvent {
InvalidSignature = "invalid-signature", InvalidSignature = "invalid-signature",
} }
export type SubscribeRequest = { export type RelaysAndFilters = {
relays: string[] relays: string[]
filters: Filter[] filters: Filter[]
}
export type SubscribeRequest = RelaysAndFilters & {
delay?: number delay?: number
signal?: AbortSignal signal?: AbortSignal
timeout?: number timeout?: number
@@ -136,7 +138,7 @@ export const optimizeSubscriptions = (subs: Subscription[]) => {
const eosedSubs = new Set<string>() const eosedSubs = new Set<string>()
const mergedSubs = [] const mergedSubs = []
for (const {relays, filters} of NetworkContext.optimizeSubscriptions(group)) { for (const {relays, filters} of ctx.net.optimizeSubscriptions(group)) {
const mergedSub = makeSubscription({filters, const mergedSub = makeSubscription({filters,
relays, relays,
timeout, timeout,
@@ -212,7 +214,7 @@ export const optimizeSubscriptions = (subs: Subscription[]) => {
export const executeSubscription = (sub: Subscription) => { export const executeSubscription = (sub: Subscription) => {
const {request, emitter, tracker, controller} = sub const {request, emitter, tracker, controller} = sub
const {filters, closeOnEose, relays, signal, timeout, authTimeout = 0} = request const {filters, closeOnEose, relays, signal, timeout, authTimeout = 0} = request
const executor = NetworkContext.getExecutor(relays) const executor = ctx.net.getExecutor(relays)
const subs: {unsubscribe: () => void}[] = [] const subs: {unsubscribe: () => void}[] = []
const completedRelays = new Set() const completedRelays = new Set()
const events: TrustedEvent[] = [] const events: TrustedEvent[] = []
@@ -251,11 +253,11 @@ export const executeSubscription = (sub: Subscription) => {
const onEvent = (url: string, event: TrustedEvent) => { const onEvent = (url: string, event: TrustedEvent) => {
if (tracker.track(event.id, url)) { if (tracker.track(event.id, url)) {
emitter.emit(SubscriptionEvent.Duplicate, url, event) emitter.emit(SubscriptionEvent.Duplicate, url, event)
} else if (NetworkContext.isDeleted(url, event)) { } else if (ctx.net.isDeleted(url, event)) {
emitter.emit(SubscriptionEvent.DeletedEvent, url, event) emitter.emit(SubscriptionEvent.DeletedEvent, url, event)
} else if (!NetworkContext.matchFilters(url, filters, event)) { } else if (!ctx.net.matchFilters(url, filters, event)) {
emitter.emit(SubscriptionEvent.FailedFilter, url, event) emitter.emit(SubscriptionEvent.FailedFilter, url, event)
} else if (!NetworkContext.hasValidSignature(url, event)) { } else if (!ctx.net.hasValidSignature(url, event)) {
emitter.emit(SubscriptionEvent.InvalidSignature, url, event) emitter.emit(SubscriptionEvent.InvalidSignature, url, event)
} else { } else {
emitter.emit(SubscriptionEvent.Event, url, event) emitter.emit(SubscriptionEvent.Event, url, event)
+8
View File
@@ -0,0 +1,8 @@
import type {NetContext} from './Context'
declare module "@welshman/lib" {
interface Context {
net: NetContext
}
}