rework context
This commit is contained in:
@@ -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"}],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
|
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,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(),
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -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
@@ -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)) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+9
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>()
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type {Context} from '@welshman/lib'
|
||||||
|
|
||||||
|
export const ctx: Context = {}
|
||||||
|
|
||||||
|
export const setContext = (newCtx: Context) => Object.assign(ctx, newCtx)
|
||||||
@@ -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,3 +1,4 @@
|
|||||||
|
export * from './Context'
|
||||||
export * from './Deferred'
|
export * from './Deferred'
|
||||||
export * from './Emitter'
|
export * from './Emitter'
|
||||||
export * from './Fluent'
|
export * from './Fluent'
|
||||||
|
|||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
declare module "@welshman/lib" {
|
||||||
|
export interface Context {}
|
||||||
|
}
|
||||||
+31
-36
@@ -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,
|
||||||
}
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import type {NetContext} from './Context'
|
||||||
|
|
||||||
|
|
||||||
|
declare module "@welshman/lib" {
|
||||||
|
interface Context {
|
||||||
|
net: NetContext
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user