diff --git a/packages/lib/Tools.ts b/packages/lib/Tools.ts index de40bc4..4107316 100644 --- a/packages/lib/Tools.ts +++ b/packages/lib/Tools.ts @@ -60,20 +60,6 @@ export const groupBy = (f: (x: T) => string, xs: T[]) => { return r } -export const pushToKey = (m: Record | Map, k: string, v: T) => { - if (m instanceof Map) { - const a = m.get(k) || [] - - a.push(v) - m.set(k, a) - } else { - m[k] = m[k] || [] - m[k].push(v) - } - - return m -} - export const sample = (n: number, xs: T[]) => { const result: T[] = [] diff --git a/packages/network/Subscribe.ts b/packages/network/Subscribe.ts index ffd253b..9d6fedf 100644 --- a/packages/network/Subscribe.ts +++ b/packages/network/Subscribe.ts @@ -237,7 +237,7 @@ export const executeSubscription = (sub: Subscription) => { export const executeSubscriptions = (subs: Subscription[]) => mergeSubscriptions(subs).forEach(executeSubscription) -export const executeSubscriptionBatched = batch(500, executeSubscriptions) +export const executeSubscriptionBatched = batch(300, executeSubscriptions) export const subscribe = (request: SubscribeRequest) => { const subscription: Subscription = makeSubscription(request) diff --git a/packages/util/Filters.ts b/packages/util/Filters.ts index d720bcb..4803c72 100644 --- a/packages/util/Filters.ts +++ b/packages/util/Filters.ts @@ -78,7 +78,7 @@ export const combineFilters = (filters: Filter[]) => { return result } -export const getIdFilters = (idsOrAddresses: string[]) => { +export const getIdFilters = (idsOrAddresses: Iterable) => { const ids = [] const aFilters = [] diff --git a/packages/util/Router.ts b/packages/util/Router.ts index d142a3b..2aa63c2 100644 --- a/packages/util/Router.ts +++ b/packages/util/Router.ts @@ -1,8 +1,8 @@ -import type {EventTemplate, UnsignedEvent} from 'nostr-tools' +import type {EventTemplate} from 'nostr-tools' import {first, identity, sortBy, uniq, shuffle} from '@coracle.social/lib' +import {Tags, Tag} from '@coracle.social/util' import type {Rumor} from './Events' import {getAddress, isReplaceable} from './Events' -import {Tag, Tags} from './Tags' import {isShareableRelayUrl} from './Relays' import {GROUP_DEFINITION, COMMUNITY_DEFINITION} from './Kinds' import {addressFromEvent, decodeAddress, isCommunityAddress, isGroupAddress} from './Address' @@ -23,16 +23,16 @@ export type RouterOptions = { getRedundancy: () => number } -export type TagsByRelay = Map +export type ValuesByRelay = Map> -export type RelayTags = { +export type RelayValues = { relay: string - tags: Tags + values: Set } -export type TagRelays = { - tag: Tag - relays: string[] +export type ValueRelays = { + value: string + relays: Set } export type FallbackPolicy = (count: number, limit: number) => number @@ -45,11 +45,11 @@ export class Router { getTagSelections = (tags: Tags) => tags .filter(t => isShareableRelayUrl(t.nth(2))) - .mapTo(t => this.selection(t.take(2), [t.nth(2)])) + .mapTo(t => this.selection(t.nth(1), [t.nth(2)])) .valueOf() getPubkeySelection = (pubkey: string, mode?: RelayMode) => - this.pubkeySelection(pubkey, this.options.getPubkeyRelays(pubkey, mode)) + this.selection(pubkey, this.options.getPubkeyRelays(pubkey, mode)) getPubkeySelections = (pubkeys: string[], mode?: RelayMode) => pubkeys.map(pubkey => this.getPubkeySelection(pubkey, mode)) @@ -59,32 +59,35 @@ export class Router { getContextSelections = (tags: Tags) => { return [ - ...tags.communities().mapTo(t => this.selection(t, this.options.getCommunityRelays(t.value()))).valueOf(), - ...tags.groups().mapTo(t => this.selection(t, this.options.getGroupRelays(t.value()))).valueOf(), + ...tags.communities().mapTo(t => this.selection(t.value(), this.options.getCommunityRelays(t.value()))).valueOf(), + ...tags.groups().mapTo(t => this.selection(t.value(), this.options.getGroupRelays(t.value()))).valueOf(), ] } - // Utilities for creating ItemSelections + // Utilities for creating ValueRelays - selection = (tag: Tag, relays: string[]) => ({tag, relays}) + selection = (value: string, relays: Iterable) => ({value, relays: new Set(relays)}) - pubkeySelection = (pubkey: string, relays: string[]) => - this.selection(Tag.fromPubkey(pubkey), relays) + selections = (values: string[], relays: string[]) => + values.map(value => this.selection(value, relays)) + + forceValue = (value: string, selections: ValueRelays[]) => + selections.map(({relays}) => this.selection(value, relays)) // Utilities for processing hints - relaySelectionsFromMap = (tagsByRelay: TagsByRelay) => - Array.from(tagsByRelay).map(([relay, tags]: [string, Tags]) => ({relay, tags})) + relaySelectionsFromMap = (valuesByRelay: ValuesByRelay) => + Array.from(valuesByRelay).map(([relay, values]: [string, Set]) => ({relay, values})) - scoreRelaySelection = ({tags, relay}: RelayTags) => - tags.count() * this.options.getRelayQuality(relay) + scoreRelaySelection = ({values, relay}: RelayValues) => + values.size * this.options.getRelayQuality(relay) - sortRelaySelections = (relaySelections: RelayTags[]) => { + sortRelaySelections = (relaySelections: RelayValues[]) => { const scores = new Map() - const getScore = (relayTags: RelayTags) => scores.get(relayTags.relay) || 0 + const getScore = (relayValues: RelayValues) => scores.get(relayValues.relay) || 0 - for (const relayTags of relaySelections) { - scores.set(relayTags.relay, this.scoreRelaySelection(relayTags)) + for (const relayValues of relaySelections) { + scores.set(relayValues.relay, this.scoreRelaySelection(relayValues)) } return sortBy(getScore, relaySelections.filter(getScore)) @@ -92,22 +95,15 @@ export class Router { // Utilities for creating scenarios - scenario = (selections: TagRelays[]) => new RouterScenario(this, selections) + scenario = (selections: ValueRelays[]) => new RouterScenario(this, selections) merge = (scenarios: RouterScenario[]) => this.scenario(scenarios.flatMap((scenario: RouterScenario) => scenario.selections)) - tagScenario = (tags: Tags, relays: string[]) => - this.scenario(tags.mapTo(tag => this.selection(tag, relays)).valueOf()) + product = (values: string[], relays: string[]) => + this.scenario(this.selections(values, relays)) - idScenario = (ids: string[], relays: string[]) => - this.tagScenario(Tags.wrap(ids.map(id => ["e", id])), relays) - - pubkeyScenario = (pubkeys: string[], relays: string[]) => - this.tagScenario(Tags.wrap(pubkeys.map(pubkey => ["p", pubkey])), relays) - - addressScenario = (addresses: string[], relays: string[]) => - this.tagScenario(Tags.wrap(addresses.map(address => ["a", address])), relays) + fromRelays = (relays: string[]) => this.scenario([this.selection("", relays)]) // Routing scenarios @@ -129,35 +125,42 @@ export class Router { this.getPubkeySelection(pubkey, RelayMode.Read), ]).policy(this.addMinimalFallbacks) - Event = (event: UnsignedEvent) => - this.scenario([ + Event = (event: Rumor) => + this.scenario(this.forceValue(event.id, [ this.getPubkeySelection(event.pubkey, RelayMode.Write), ...this.getContextSelections(Tags.fromEvent(event).context()), - ]) + ])) - EventChildren = (event: UnsignedEvent) => - this.scenario([ + EventChildren = (event: Rumor) => + this.scenario(this.forceValue(event.id, [ this.getPubkeySelection(event.pubkey, RelayMode.Read), ...this.getContextSelections(Tags.fromEvent(event).context()), - ]) + ])) - EventAncestors = (event: UnsignedEvent) => { + EventAncestors = (event: Rumor, type: "mentions" | "replies" | "roots") => { const tags = Tags.fromEvent(event) - const ptags = tags.whereKey("p") - const atags = tags.context() - const {replies, roots} = tags.ancestors() - - return this.scenario([ - ...this.getTagSelections(replies), - ...this.getTagSelections(roots), - ...this.getTagSelections(ptags), - ...this.getContextSelections(atags), - ...this.getPubkeySelections(ptags.values().valueOf(), RelayMode.Write), - this.getPubkeySelection(event.pubkey, RelayMode.Read), + const ancestors = tags.ancestors()[type] + const pubkeys = tags.whereKey("p").values().valueOf() + const communities = tags.communities().values().valueOf() + const groups = tags.groups().values().valueOf() + const relays = uniq([ + ...this.options.getPubkeyRelays(event.pubkey, RelayMode.Read), + ...pubkeys.flatMap((k: string) => this.options.getPubkeyRelays(k, RelayMode.Write)), + ...communities.flatMap((a: string) => this.options.getCommunityRelays(a)), + ...groups.flatMap((a: string) => this.options.getGroupRelays(a)), + ...ancestors.relays().valueOf(), ]) + + return this.product(ancestors.values().valueOf(), relays) } - PublishEvent = (event: UnsignedEvent) => { + EventMentions = (event: Rumor) => this.EventAncestors(event, "mentions") + + EventParents = (event: Rumor) => this.EventAncestors(event, "replies") + + EventRoots = (event: Rumor) => this.EventAncestors(event, "roots") + + PublishEvent = (event: Rumor) => { const tags = Tags.fromEvent(event) const mentions = tags.values("p").valueOf() @@ -168,11 +171,11 @@ export class Router { .policy(this.addNoFallbacks) } - return this.scenario([ + return this.scenario(this.forceValue(event.id, [ this.getPubkeySelection(event.pubkey, RelayMode.Write), ...this.getContextSelections(tags.context()), ...this.getPubkeySelections(mentions, RelayMode.Read), - ]) + ])) } FromPubkeys = (pubkeys: string[]) => @@ -220,7 +223,7 @@ export class Router { tagEventId = (event: Rumor, ...extra: string[]) => Tag.from(["e", event.id, this.Event(event).getUrl(), ...extra]) - tagEventAddress = (event: UnsignedEvent, ...extra: string[]) => + tagEventAddress = (event: Rumor, ...extra: string[]) => Tag.from(["a", getAddress(event), this.Event(event).getUrl(), ...extra]) tagEvent = (event: Rumor, ...extra: string[]) => { @@ -233,7 +236,7 @@ export class Router { return new Tags(tags) } - address = (event: UnsignedEvent) => + address = (event: Rumor) => addressFromEvent(event, this.Event(event).redundancy(3).getUrls()) } @@ -246,13 +249,13 @@ export type RouterScenarioOptions = { } export class RouterScenario { - constructor(readonly router: Router, readonly selections: TagRelays[], readonly options: RouterScenarioOptions = {}) {} + constructor(readonly router: Router, readonly selections: ValueRelays[], readonly options: RouterScenarioOptions = {}) {} clone = (options: RouterScenarioOptions) => new RouterScenario(this.router, this.selections, {...this.options, ...options}) - select = (f: (selection: Tag) => boolean) => - new RouterScenario(this.router, this.selections.filter(({tag}) => f(tag)), this.options) + select = (f: (selection: string) => boolean) => + new RouterScenario(this.router, this.selections.filter(({value}) => f(value)), this.options) redundancy = (redundancy: number) => this.clone({redundancy}) @@ -267,42 +270,42 @@ export class RouterScenario { getLimit = () => this.options.limit getSelections = () => { - const tagsByRelay: TagsByRelay = new Map() - for (const {tag, relays} of this.selections) { + const valuesByRelay: ValuesByRelay = new Map() + for (const {value, relays} of this.selections) { for (const relay of relays) { - addTagToMap(tagsByRelay, relay, tag) + addToKey(valuesByRelay, relay, value) } } const redundancy = this.getRedundancy() const seen = new Map() - const result: TagsByRelay = new Map() - const relaySelections = this.router.relaySelectionsFromMap(tagsByRelay) + const result: ValuesByRelay = new Map() + const relaySelections = this.router.relaySelectionsFromMap(valuesByRelay) for (const {relay} of this.router.sortRelaySelections(relaySelections)) { - const tags = [] - for (const tag of tagsByRelay.get(relay)?.valueOf() || []) { - const timesSeen = seen.get(tag.value()) || 0 + const values = new Set() + for (const value of valuesByRelay.get(relay) || []) { + const timesSeen = seen.get(value) || 0 if (timesSeen < redundancy) { - seen.set(tag.value(), timesSeen + 1) - tags.push(tag) + seen.set(value, timesSeen + 1) + values.add(value) } } - if (tags.length > 0) { - result.set(relay, Tags.from(tags)) + if (values.size > 0) { + result.set(relay, values) } } const fallbacks = shuffle(this.router.options.getStaticRelays()) const fallbackPolicy = this.getPolicy() - for (const {tag} of this.selections) { - const timesSeen = seen.get(tag.value()) || 0 + for (const {value} of this.selections) { + const timesSeen = seen.get(value) || 0 const fallbacksNeeded = fallbackPolicy(timesSeen, redundancy) if (fallbacksNeeded > 0) { for (const relay of fallbacks.slice(0, fallbacksNeeded)) { - addTagToMap(result, relay, tag) + addToKey(result, relay, value) } } } @@ -314,10 +317,14 @@ export class RouterScenario { : this.router.relaySelectionsFromMap(result) } - getUrls = () => this.getSelections().map((selection: RelayTags) => selection.relay) + getUrls = () => this.getSelections().map((selection: RelayValues) => selection.relay) getUrl = () => first(this.getUrls()) } -const addTagToMap = (m: Map, k: string, v: Tag) => - m.set(k, (m.get(k) || Tags.from([])).append(v)) +const addToKey = (m: Map>, k: string, v: T) => { + const a = m.get(k) || new Set() + + a.add(v) + m.set(k, a) +} diff --git a/packages/util/Tags.ts b/packages/util/Tags.ts index 6608525..1effc1d 100644 --- a/packages/util/Tags.ts +++ b/packages/util/Tags.ts @@ -79,7 +79,7 @@ export class Tags extends (Fluent as OmitStatics, 'from' topics = () => this.whereKey("t").values().map((t: string) => t.replace(/^#/, "")) - ancestors = () => { + ancestors = (x?: boolean) => { const tags = this.filter(t => ["a", "e", "q"].includes(t.key()) && !t.isContext()) const parentTags = tags.filter(t => ["a", "e"].includes(t.key())) const mentionTags = tags.whereKey("q") @@ -95,10 +95,10 @@ export class Tags extends (Fluent as OmitStatics, 'from' replies.push(t.valueOf()) } else if (t.mark() === 'mention') { mentions.push(t.valueOf()) - } else if (i === 0) { - roots.push(t.valueOf()) } else if (i === parentTags.count() - 1) { replies.push(t.valueOf()) + } else if (i === 0) { + roots.push(t.valueOf()) } else { mentions.push(t.valueOf()) }