Move hints back to strings, make sure the strings are what the user asked for

This commit is contained in:
Jon Staab
2024-04-02 16:35:26 -07:00
parent d159b94eeb
commit 8319ba4862
5 changed files with 91 additions and 98 deletions
-14
View File
@@ -60,20 +60,6 @@ export const groupBy = <T>(f: (x: T) => string, xs: T[]) => {
return r return r
} }
export const pushToKey = <T>(m: Record<string, T[]> | Map<string, T[]>, 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 = <T>(n: number, xs: T[]) => { export const sample = <T>(n: number, xs: T[]) => {
const result: T[] = [] const result: T[] = []
+1 -1
View File
@@ -237,7 +237,7 @@ export const executeSubscription = (sub: Subscription) => {
export const executeSubscriptions = (subs: Subscription[]) => export const executeSubscriptions = (subs: Subscription[]) =>
mergeSubscriptions(subs).forEach(executeSubscription) mergeSubscriptions(subs).forEach(executeSubscription)
export const executeSubscriptionBatched = batch(500, executeSubscriptions) export const executeSubscriptionBatched = batch(300, executeSubscriptions)
export const subscribe = (request: SubscribeRequest) => { export const subscribe = (request: SubscribeRequest) => {
const subscription: Subscription = makeSubscription(request) const subscription: Subscription = makeSubscription(request)
+1 -1
View File
@@ -78,7 +78,7 @@ export const combineFilters = (filters: Filter[]) => {
return result return result
} }
export const getIdFilters = (idsOrAddresses: string[]) => { export const getIdFilters = (idsOrAddresses: Iterable<string>) => {
const ids = [] const ids = []
const aFilters = [] const aFilters = []
+86 -79
View File
@@ -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 {first, identity, sortBy, uniq, shuffle} from '@coracle.social/lib'
import {Tags, Tag} from '@coracle.social/util'
import type {Rumor} from './Events' import type {Rumor} from './Events'
import {getAddress, isReplaceable} from './Events' import {getAddress, isReplaceable} from './Events'
import {Tag, Tags} from './Tags'
import {isShareableRelayUrl} from './Relays' import {isShareableRelayUrl} from './Relays'
import {GROUP_DEFINITION, COMMUNITY_DEFINITION} from './Kinds' import {GROUP_DEFINITION, COMMUNITY_DEFINITION} from './Kinds'
import {addressFromEvent, decodeAddress, isCommunityAddress, isGroupAddress} from './Address' import {addressFromEvent, decodeAddress, isCommunityAddress, isGroupAddress} from './Address'
@@ -23,16 +23,16 @@ export type RouterOptions = {
getRedundancy: () => number getRedundancy: () => number
} }
export type TagsByRelay = Map<string, Tags> export type ValuesByRelay = Map<string, Set<string>>
export type RelayTags = { export type RelayValues = {
relay: string relay: string
tags: Tags values: Set<string>
} }
export type TagRelays = { export type ValueRelays = {
tag: Tag value: string
relays: string[] relays: Set<string>
} }
export type FallbackPolicy = (count: number, limit: number) => number export type FallbackPolicy = (count: number, limit: number) => number
@@ -45,11 +45,11 @@ export class Router {
getTagSelections = (tags: Tags) => getTagSelections = (tags: Tags) =>
tags tags
.filter(t => isShareableRelayUrl(t.nth(2))) .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() .valueOf()
getPubkeySelection = (pubkey: string, mode?: RelayMode) => 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) => getPubkeySelections = (pubkeys: string[], mode?: RelayMode) =>
pubkeys.map(pubkey => this.getPubkeySelection(pubkey, mode)) pubkeys.map(pubkey => this.getPubkeySelection(pubkey, mode))
@@ -59,32 +59,35 @@ export class Router {
getContextSelections = (tags: Tags) => { getContextSelections = (tags: Tags) => {
return [ return [
...tags.communities().mapTo(t => this.selection(t, this.options.getCommunityRelays(t.value()))).valueOf(), ...tags.communities().mapTo(t => this.selection(t.value(), this.options.getCommunityRelays(t.value()))).valueOf(),
...tags.groups().mapTo(t => this.selection(t, this.options.getGroupRelays(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<string>) => ({value, relays: new Set(relays)})
pubkeySelection = (pubkey: string, relays: string[]) => selections = (values: string[], relays: string[]) =>
this.selection(Tag.fromPubkey(pubkey), relays) values.map(value => this.selection(value, relays))
forceValue = (value: string, selections: ValueRelays[]) =>
selections.map(({relays}) => this.selection(value, relays))
// Utilities for processing hints // Utilities for processing hints
relaySelectionsFromMap = (tagsByRelay: TagsByRelay) => relaySelectionsFromMap = (valuesByRelay: ValuesByRelay) =>
Array.from(tagsByRelay).map(([relay, tags]: [string, Tags]) => ({relay, tags})) Array.from(valuesByRelay).map(([relay, values]: [string, Set<string>]) => ({relay, values}))
scoreRelaySelection = ({tags, relay}: RelayTags) => scoreRelaySelection = ({values, relay}: RelayValues) =>
tags.count() * this.options.getRelayQuality(relay) values.size * this.options.getRelayQuality(relay)
sortRelaySelections = (relaySelections: RelayTags[]) => { sortRelaySelections = (relaySelections: RelayValues[]) => {
const scores = new Map<string, number>() const scores = new Map<string, number>()
const getScore = (relayTags: RelayTags) => scores.get(relayTags.relay) || 0 const getScore = (relayValues: RelayValues) => scores.get(relayValues.relay) || 0
for (const relayTags of relaySelections) { for (const relayValues of relaySelections) {
scores.set(relayTags.relay, this.scoreRelaySelection(relayTags)) scores.set(relayValues.relay, this.scoreRelaySelection(relayValues))
} }
return sortBy(getScore, relaySelections.filter(getScore)) return sortBy(getScore, relaySelections.filter(getScore))
@@ -92,22 +95,15 @@ export class Router {
// Utilities for creating scenarios // Utilities for creating scenarios
scenario = (selections: TagRelays[]) => new RouterScenario(this, selections) scenario = (selections: ValueRelays[]) => new RouterScenario(this, selections)
merge = (scenarios: RouterScenario[]) => merge = (scenarios: RouterScenario[]) =>
this.scenario(scenarios.flatMap((scenario: RouterScenario) => scenario.selections)) this.scenario(scenarios.flatMap((scenario: RouterScenario) => scenario.selections))
tagScenario = (tags: Tags, relays: string[]) => product = (values: string[], relays: string[]) =>
this.scenario(tags.mapTo(tag => this.selection(tag, relays)).valueOf()) this.scenario(this.selections(values, relays))
idScenario = (ids: string[], relays: string[]) => fromRelays = (relays: string[]) => this.scenario([this.selection("", relays)])
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)
// Routing scenarios // Routing scenarios
@@ -129,35 +125,42 @@ export class Router {
this.getPubkeySelection(pubkey, RelayMode.Read), this.getPubkeySelection(pubkey, RelayMode.Read),
]).policy(this.addMinimalFallbacks) ]).policy(this.addMinimalFallbacks)
Event = (event: UnsignedEvent) => Event = (event: Rumor) =>
this.scenario([ this.scenario(this.forceValue(event.id, [
this.getPubkeySelection(event.pubkey, RelayMode.Write), this.getPubkeySelection(event.pubkey, RelayMode.Write),
...this.getContextSelections(Tags.fromEvent(event).context()), ...this.getContextSelections(Tags.fromEvent(event).context()),
]) ]))
EventChildren = (event: UnsignedEvent) => EventChildren = (event: Rumor) =>
this.scenario([ this.scenario(this.forceValue(event.id, [
this.getPubkeySelection(event.pubkey, RelayMode.Read), this.getPubkeySelection(event.pubkey, RelayMode.Read),
...this.getContextSelections(Tags.fromEvent(event).context()), ...this.getContextSelections(Tags.fromEvent(event).context()),
]) ]))
EventAncestors = (event: UnsignedEvent) => { EventAncestors = (event: Rumor, type: "mentions" | "replies" | "roots") => {
const tags = Tags.fromEvent(event) const tags = Tags.fromEvent(event)
const ptags = tags.whereKey("p") const ancestors = tags.ancestors()[type]
const atags = tags.context() const pubkeys = tags.whereKey("p").values().valueOf()
const {replies, roots} = tags.ancestors() const communities = tags.communities().values().valueOf()
const groups = tags.groups().values().valueOf()
return this.scenario([ const relays = uniq([
...this.getTagSelections(replies), ...this.options.getPubkeyRelays(event.pubkey, RelayMode.Read),
...this.getTagSelections(roots), ...pubkeys.flatMap((k: string) => this.options.getPubkeyRelays(k, RelayMode.Write)),
...this.getTagSelections(ptags), ...communities.flatMap((a: string) => this.options.getCommunityRelays(a)),
...this.getContextSelections(atags), ...groups.flatMap((a: string) => this.options.getGroupRelays(a)),
...this.getPubkeySelections(ptags.values().valueOf(), RelayMode.Write), ...ancestors.relays().valueOf(),
this.getPubkeySelection(event.pubkey, RelayMode.Read),
]) ])
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 tags = Tags.fromEvent(event)
const mentions = tags.values("p").valueOf() const mentions = tags.values("p").valueOf()
@@ -168,11 +171,11 @@ export class Router {
.policy(this.addNoFallbacks) .policy(this.addNoFallbacks)
} }
return this.scenario([ return this.scenario(this.forceValue(event.id, [
this.getPubkeySelection(event.pubkey, RelayMode.Write), this.getPubkeySelection(event.pubkey, RelayMode.Write),
...this.getContextSelections(tags.context()), ...this.getContextSelections(tags.context()),
...this.getPubkeySelections(mentions, RelayMode.Read), ...this.getPubkeySelections(mentions, RelayMode.Read),
]) ]))
} }
FromPubkeys = (pubkeys: string[]) => FromPubkeys = (pubkeys: string[]) =>
@@ -220,7 +223,7 @@ export class Router {
tagEventId = (event: Rumor, ...extra: string[]) => tagEventId = (event: Rumor, ...extra: string[]) =>
Tag.from(["e", event.id, this.Event(event).getUrl(), ...extra]) 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]) Tag.from(["a", getAddress(event), this.Event(event).getUrl(), ...extra])
tagEvent = (event: Rumor, ...extra: string[]) => { tagEvent = (event: Rumor, ...extra: string[]) => {
@@ -233,7 +236,7 @@ export class Router {
return new Tags(tags) return new Tags(tags)
} }
address = (event: UnsignedEvent) => address = (event: Rumor) =>
addressFromEvent(event, this.Event(event).redundancy(3).getUrls()) addressFromEvent(event, this.Event(event).redundancy(3).getUrls())
} }
@@ -246,13 +249,13 @@ export type RouterScenarioOptions = {
} }
export class RouterScenario { 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) => clone = (options: RouterScenarioOptions) =>
new RouterScenario(this.router, this.selections, {...this.options, ...options}) new RouterScenario(this.router, this.selections, {...this.options, ...options})
select = (f: (selection: Tag) => boolean) => select = (f: (selection: string) => boolean) =>
new RouterScenario(this.router, this.selections.filter(({tag}) => f(tag)), this.options) new RouterScenario(this.router, this.selections.filter(({value}) => f(value)), this.options)
redundancy = (redundancy: number) => this.clone({redundancy}) redundancy = (redundancy: number) => this.clone({redundancy})
@@ -267,42 +270,42 @@ export class RouterScenario {
getLimit = () => this.options.limit getLimit = () => this.options.limit
getSelections = () => { getSelections = () => {
const tagsByRelay: TagsByRelay = new Map() const valuesByRelay: ValuesByRelay = new Map()
for (const {tag, relays} of this.selections) { for (const {value, relays} of this.selections) {
for (const relay of relays) { for (const relay of relays) {
addTagToMap(tagsByRelay, relay, tag) addToKey(valuesByRelay, relay, value)
} }
} }
const redundancy = this.getRedundancy() const redundancy = this.getRedundancy()
const seen = new Map<string, number>() const seen = new Map<string, number>()
const result: TagsByRelay = new Map() const result: ValuesByRelay = new Map()
const relaySelections = this.router.relaySelectionsFromMap(tagsByRelay) const relaySelections = this.router.relaySelectionsFromMap(valuesByRelay)
for (const {relay} of this.router.sortRelaySelections(relaySelections)) { for (const {relay} of this.router.sortRelaySelections(relaySelections)) {
const tags = [] const values = new Set<string>()
for (const tag of tagsByRelay.get(relay)?.valueOf() || []) { for (const value of valuesByRelay.get(relay) || []) {
const timesSeen = seen.get(tag.value()) || 0 const timesSeen = seen.get(value) || 0
if (timesSeen < redundancy) { if (timesSeen < redundancy) {
seen.set(tag.value(), timesSeen + 1) seen.set(value, timesSeen + 1)
tags.push(tag) values.add(value)
} }
} }
if (tags.length > 0) { if (values.size > 0) {
result.set(relay, Tags.from(tags)) result.set(relay, values)
} }
} }
const fallbacks = shuffle(this.router.options.getStaticRelays()) const fallbacks = shuffle(this.router.options.getStaticRelays())
const fallbackPolicy = this.getPolicy() const fallbackPolicy = this.getPolicy()
for (const {tag} of this.selections) { for (const {value} of this.selections) {
const timesSeen = seen.get(tag.value()) || 0 const timesSeen = seen.get(value) || 0
const fallbacksNeeded = fallbackPolicy(timesSeen, redundancy) const fallbacksNeeded = fallbackPolicy(timesSeen, redundancy)
if (fallbacksNeeded > 0) { if (fallbacksNeeded > 0) {
for (const relay of fallbacks.slice(0, fallbacksNeeded)) { 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) : this.router.relaySelectionsFromMap(result)
} }
getUrls = () => this.getSelections().map((selection: RelayTags) => selection.relay) getUrls = () => this.getSelections().map((selection: RelayValues) => selection.relay)
getUrl = () => first(this.getUrls()) getUrl = () => first(this.getUrls())
} }
const addTagToMap = (m: Map<string, Tags>, k: string, v: Tag) => const addToKey = <T>(m: Map<string, Set<T>>, k: string, v: T) => {
m.set(k, (m.get(k) || Tags.from([])).append(v)) const a = m.get(k) || new Set<T>()
a.add(v)
m.set(k, a)
}
+3 -3
View File
@@ -79,7 +79,7 @@ export class Tags extends (Fluent<Tag> as OmitStatics<typeof Fluent<Tag>, 'from'
topics = () => this.whereKey("t").values().map((t: string) => t.replace(/^#/, "")) 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 tags = this.filter(t => ["a", "e", "q"].includes(t.key()) && !t.isContext())
const parentTags = tags.filter(t => ["a", "e"].includes(t.key())) const parentTags = tags.filter(t => ["a", "e"].includes(t.key()))
const mentionTags = tags.whereKey("q") const mentionTags = tags.whereKey("q")
@@ -95,10 +95,10 @@ export class Tags extends (Fluent<Tag> as OmitStatics<typeof Fluent<Tag>, 'from'
replies.push(t.valueOf()) replies.push(t.valueOf())
} else if (t.mark() === 'mention') { } else if (t.mark() === 'mention') {
mentions.push(t.valueOf()) mentions.push(t.valueOf())
} else if (i === 0) {
roots.push(t.valueOf())
} else if (i === parentTags.count() - 1) { } else if (i === parentTags.count() - 1) {
replies.push(t.valueOf()) replies.push(t.valueOf())
} else if (i === 0) {
roots.push(t.valueOf())
} else { } else {
mentions.push(t.valueOf()) mentions.push(t.valueOf())
} }