rework router
This commit is contained in:
+121
-215
@@ -15,47 +15,26 @@ export type RouterOptions = {
|
|||||||
getUserPubkey: () => string | null
|
getUserPubkey: () => string | null
|
||||||
getGroupRelays: (address: string) => string[]
|
getGroupRelays: (address: string) => string[]
|
||||||
getCommunityRelays: (address: string) => string[]
|
getCommunityRelays: (address: string) => string[]
|
||||||
getPubkeyInboxRelays: (pubkey: string) => string[]
|
getPubkeyRelays: (pubkey: string, mode?: RelayMode) => string[]
|
||||||
getPubkeyOutboxRelays: (pubkey: string) => string[]
|
getDefaultRelays: (mode?: RelayMode) => string[]
|
||||||
getFallbackInboxRelays: () => string[]
|
|
||||||
getFallbackOutboxRelays: () => string[]
|
|
||||||
getRelayQuality?: (url: string) => number
|
getRelayQuality?: (url: string) => number
|
||||||
getDefaultLimit: () => number
|
getDefaultLimit: () => number
|
||||||
}
|
}
|
||||||
|
|
||||||
// - Fetch from and publish to non-shareable relays, but don't use them for hints
|
export type RouteScores = Record<string, {score: number, count: number}>
|
||||||
// - Test that scoring/sorting makes sense, particularly asc/desc sort
|
|
||||||
|
|
||||||
export class Router {
|
export class Router {
|
||||||
constructor(readonly options: RouterOptions) {}
|
constructor(readonly options: RouterOptions) {}
|
||||||
|
|
||||||
// Utilities derived from options
|
// Utilities derived from options
|
||||||
|
|
||||||
getAllPubkeyRelays = (pubkey: string) =>
|
getUserRelays = (mode?: RelayMode) => {
|
||||||
[
|
|
||||||
...this.options.getPubkeyInboxRelays(pubkey),
|
|
||||||
...this.options.getPubkeyOutboxRelays(pubkey),
|
|
||||||
]
|
|
||||||
|
|
||||||
getUserInboxRelays = () => {
|
|
||||||
const pubkey = this.options.getUserPubkey()
|
const pubkey = this.options.getUserPubkey()
|
||||||
|
|
||||||
return pubkey ? this.options.getPubkeyInboxRelays(pubkey) : []
|
return pubkey ? this.options.getPubkeyRelays(pubkey, mode) : []
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserOutboxRelays = () => {
|
getContextRelayGroups = (event: EventTemplate) => {
|
||||||
const pubkey = this.options.getUserPubkey()
|
|
||||||
|
|
||||||
return pubkey ? this.options.getPubkeyOutboxRelays(pubkey) : []
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllUserRelays = () => {
|
|
||||||
const pubkey = this.options.getUserPubkey()
|
|
||||||
|
|
||||||
return pubkey ? this.getAllPubkeyRelays(pubkey) : []
|
|
||||||
}
|
|
||||||
|
|
||||||
getEventContextRelayGroups = (event: EventTemplate) => {
|
|
||||||
const addresses = Tags.fromEvent(event).context().values().valueOf()
|
const addresses = Tags.fromEvent(event).context().values().valueOf()
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -67,7 +46,7 @@ export class Router {
|
|||||||
// Utilities for processing hints
|
// Utilities for processing hints
|
||||||
|
|
||||||
getGroupScores = (groups: string[][]) => {
|
getGroupScores = (groups: string[][]) => {
|
||||||
const scores: RouteScenarioScores = {}
|
const scores: RouteScores = {}
|
||||||
|
|
||||||
groups.forEach((urls, i) => {
|
groups.forEach((urls, i) => {
|
||||||
for (const url of shuffle(uniq(urls))) {
|
for (const url of shuffle(uniq(urls))) {
|
||||||
@@ -92,193 +71,122 @@ export class Router {
|
|||||||
return scores
|
return scores
|
||||||
}
|
}
|
||||||
|
|
||||||
urlsFromScores = (limit: number, scores: RouteScenarioScores) =>
|
urlsFromScores = (scores: RouteScores) =>
|
||||||
Object.entries(scores).sort((a, b) => a[1].score > b[1].score ? -1 : 1).map(pair => pair[0] as string).slice(0, limit)
|
Object.entries(scores).sort((a, b) => a[1].score > b[1].score ? -1 : 1).map(p => p[0] as string)
|
||||||
|
|
||||||
groupsToUrls = (limit: number, groups: string[][]) => this.urlsFromScores(limit, this.getGroupScores(groups))
|
groupsToUrls = (groups: string[][]) => this.urlsFromScores(this.getGroupScores(groups))
|
||||||
|
|
||||||
scenario = (options: RouterScenarioOptions) => new RouterScenario(this, options)
|
scenario = (groups: string[][]) => new RouterScenario(this, groups)
|
||||||
|
|
||||||
merge = ({fallbackPolicy, scenarios}: {fallbackPolicy: FallbackPolicy, scenarios: RouterScenario[]}) =>
|
merge = (scenarios: RouterScenario[]) =>
|
||||||
this.scenario({fallbackPolicy, getGroups: () => scenarios.map(s => s.getRawUrls())})
|
this.scenario(scenarios.map(scenario => scenario.policy(addNoFallbacks).getUrls()))
|
||||||
|
|
||||||
// Routing scenarios
|
// Routing scenarios
|
||||||
|
|
||||||
Broadcast = () => this.scenario({
|
User = () => this.scenario([])
|
||||||
fallbackPolicy: useMinimalFallbacks(RelayMode.Outbox),
|
|
||||||
getGroups: () => [this.getAllUserRelays()],
|
|
||||||
})
|
|
||||||
|
|
||||||
Aggregate = () => this.scenario({
|
Inbox = () => this.scenario([]).mode(RelayMode.Inbox)
|
||||||
fallbackPolicy: useMinimalFallbacks(RelayMode.Inbox),
|
|
||||||
getGroups: () => [this.getAllUserRelays()],
|
|
||||||
})
|
|
||||||
|
|
||||||
NoteToSelf = () => this.scenario({
|
Outbox = () => this.scenario([]).mode(RelayMode.Outbox)
|
||||||
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox),
|
|
||||||
getGroups: () => [this.getUserInboxRelays()],
|
|
||||||
})
|
|
||||||
|
|
||||||
FetchAllMessages = () => this.scenario({
|
AllMessages = () => this.scenario([this.getUserRelays()])
|
||||||
fallbackPolicy: useMinimalFallbacks(RelayMode.Inbox),
|
|
||||||
getGroups: () => [this.getAllUserRelays()],
|
|
||||||
})
|
|
||||||
|
|
||||||
FetchMessages = (pubkeys: string[]) => this.scenario({
|
Messages = (pubkeys: string[]) =>
|
||||||
fallbackPolicy: useMinimalFallbacks(RelayMode.Inbox),
|
this.scenario([
|
||||||
getGroups: () => [
|
this.getUserRelays(),
|
||||||
this.getAllUserRelays(),
|
...pubkeys.map(pubkey => this.options.getPubkeyRelays(pubkey))
|
||||||
...pubkeys.map(this.getAllPubkeyRelays)
|
])
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
PublishMessage = (pubkeys: string[]) => this.scenario({
|
PublishMessage = (pubkey: string) =>
|
||||||
fallbackPolicy: useMinimalFallbacks(RelayMode.Outbox),
|
this.scenario([
|
||||||
getGroups: () => [
|
this.getUserRelays(RelayMode.Outbox),
|
||||||
this.getUserOutboxRelays(),
|
this.options.getPubkeyRelays(pubkey, RelayMode.Inbox)
|
||||||
...pubkeys.map(this.options.getPubkeyInboxRelays)
|
]).policy(addMinimalFallbacks)
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
FetchEvent = (event: UnsignedEvent) => this.scenario({
|
Event = (event: UnsignedEvent) =>
|
||||||
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox),
|
this.scenario([
|
||||||
getGroups: () => [
|
this.options.getPubkeyRelays(event.pubkey, RelayMode.Outbox),
|
||||||
this.options.getPubkeyOutboxRelays(event.pubkey),
|
...this.getContextRelayGroups(event),
|
||||||
...this.getEventContextRelayGroups(event),
|
])
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
FetchEventChildren = (event: UnsignedEvent) => this.scenario({
|
EventChildren = (event: UnsignedEvent) =>
|
||||||
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox),
|
this.scenario([
|
||||||
getGroups: () => [
|
this.options.getPubkeyRelays(event.pubkey, RelayMode.Inbox),
|
||||||
this.options.getPubkeyInboxRelays(event.pubkey),
|
...this.getContextRelayGroups(event),
|
||||||
...this.getEventContextRelayGroups(event),
|
])
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
FetchEventParent = (event: UnsignedEvent) => this.scenario({
|
EventParent = (event: UnsignedEvent) =>
|
||||||
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox),
|
this.scenario([
|
||||||
getGroups: () => [
|
|
||||||
Tags.fromEvent(event).replies().relays().valueOf(),
|
Tags.fromEvent(event).replies().relays().valueOf(),
|
||||||
this.options.getPubkeyInboxRelays(event.pubkey),
|
this.options.getPubkeyRelays(event.pubkey, RelayMode.Inbox),
|
||||||
...this.getEventContextRelayGroups(event),
|
...this.getContextRelayGroups(event),
|
||||||
],
|
])
|
||||||
})
|
|
||||||
|
|
||||||
FetchEventRoot = (event: UnsignedEvent) => this.scenario({
|
EventRoot = (event: UnsignedEvent) =>
|
||||||
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox),
|
this.scenario([
|
||||||
getGroups: () => [
|
|
||||||
Tags.fromEvent(event).roots().relays().valueOf(),
|
Tags.fromEvent(event).roots().relays().valueOf(),
|
||||||
this.options.getPubkeyInboxRelays(event.pubkey),
|
this.options.getPubkeyRelays(event.pubkey, RelayMode.Inbox),
|
||||||
...this.getEventContextRelayGroups(event),
|
...this.getContextRelayGroups(event),
|
||||||
],
|
])
|
||||||
})
|
|
||||||
|
|
||||||
PublishEvent = (event: UnsignedEvent) => this.scenario({
|
PublishEvent = (event: UnsignedEvent) => {
|
||||||
fallbackPolicy: useMinimalFallbacks(RelayMode.Outbox),
|
const tags = Tags.fromEvent(event)
|
||||||
getGroups: () => {
|
const mentions = tags.values("p").valueOf()
|
||||||
const tags = Tags.fromEvent(event)
|
const addresses = tags.context().values().valueOf()
|
||||||
const mentions = tags.values("p").valueOf()
|
const groupAddresses = addresses.filter(isGroupAddress)
|
||||||
const addresses = tags.context().values().valueOf()
|
const communityAddresses = addresses.filter(isCommunityAddress)
|
||||||
const groupAddresses = addresses.filter(isGroupAddress)
|
|
||||||
const communityAddresses = addresses.filter(isCommunityAddress)
|
|
||||||
|
|
||||||
// If we're publishing only to private groups, only publish to those groups' relays.
|
// If we're publishing only to private groups, only publish to those groups' relays.
|
||||||
// Otherwise, publish to all relays, because it's essentially public.
|
// Otherwise, publish to all relays, because it's essentially public.
|
||||||
if (groupAddresses.length > 0 && communityAddresses.length === 0) {
|
if (groupAddresses.length > 0 && communityAddresses.length === 0) {
|
||||||
return groupAddresses.map(this.options.getGroupRelays)
|
return this.scenario(groupAddresses.map(this.options.getGroupRelays))
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return this.scenario([
|
||||||
this.options.getPubkeyOutboxRelays(event.pubkey),
|
this.options.getPubkeyRelays(event.pubkey, RelayMode.Outbox),
|
||||||
...groupAddresses.map(this.options.getGroupRelays),
|
...groupAddresses.map(this.options.getGroupRelays),
|
||||||
...communityAddresses.map(this.options.getCommunityRelays),
|
...communityAddresses.map(this.options.getCommunityRelays),
|
||||||
...mentions.map((pk: string) => this.options.getPubkeyInboxRelays(pk)),
|
...mentions.map((pk: string) => this.options.getPubkeyRelays(pk, RelayMode.Inbox)),
|
||||||
]
|
])
|
||||||
},
|
}
|
||||||
})
|
|
||||||
|
|
||||||
FetchFromHints = (...groups: string[][]) => this.scenario({
|
FromPubkeys = (pubkeys: string[]) =>
|
||||||
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox),
|
this.scenario(pubkeys.map(pk => this.options.getPubkeyRelays(pk, RelayMode.Outbox)))
|
||||||
getGroups: () => [...groups, this.getAllUserRelays()],
|
|
||||||
})
|
|
||||||
|
|
||||||
FetchFromPubkey = (pubkey: string) => this.scenario({
|
ForPubkeys = (pubkeys: string[]) =>
|
||||||
fallbackPolicy: useMaximalFallbacks(RelayMode.Outbox),
|
this.scenario(pubkeys.map(pk => this.options.getPubkeyRelays(pk, RelayMode.Inbox)))
|
||||||
getGroups: () => [this.options.getPubkeyOutboxRelays(pubkey)],
|
|
||||||
})
|
|
||||||
|
|
||||||
FetchFromPubkeys = (pubkeys: string[]) => this.scenario({
|
WithinGroup = (address: string) =>
|
||||||
fallbackPolicy: useMaximalFallbacks(RelayMode.Outbox),
|
this.scenario([this.options.getGroupRelays(address)]).policy(addNoFallbacks)
|
||||||
getGroups: () => pubkeys.map(this.options.getPubkeyOutboxRelays),
|
|
||||||
})
|
|
||||||
|
|
||||||
FetchFromGroup = (address: string) => this.scenario({
|
WithinCommunity = (address: string) =>
|
||||||
fallbackPolicy: useNoFallbacks(),
|
this.scenario([this.options.getCommunityRelays(address)])
|
||||||
getGroups: () => [this.options.getGroupRelays(address)],
|
|
||||||
})
|
|
||||||
|
|
||||||
PublishToGroup = (address: string) => this.scenario({
|
WithinContext = (address: string) => {
|
||||||
fallbackPolicy: useNoFallbacks(),
|
|
||||||
getGroups: () => [this.options.getGroupRelays(address)],
|
|
||||||
})
|
|
||||||
|
|
||||||
FetchFromCommunity = (address: string) => this.scenario({
|
|
||||||
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox),
|
|
||||||
getGroups: () => [this.options.getCommunityRelays(address)],
|
|
||||||
})
|
|
||||||
|
|
||||||
PublishToCommunity = (address: string) => this.scenario({
|
|
||||||
fallbackPolicy: useMaximalFallbacks(RelayMode.Outbox),
|
|
||||||
getGroups: () => [this.options.getCommunityRelays(address)],
|
|
||||||
})
|
|
||||||
|
|
||||||
FetchFromContext = (address: string) => {
|
|
||||||
if (isGroupAddress(address)) {
|
if (isGroupAddress(address)) {
|
||||||
return this.FetchFromGroup(address)
|
return this.WithinGroup(address)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCommunityAddress(address)) {
|
if (isCommunityAddress(address)) {
|
||||||
return this.FetchFromCommunity(address)
|
return this.WithinCommunity(address)
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Unknown context ${address}`)
|
throw new Error(`Unknown context ${address}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
FetchFromContexts = (addresses: string[]) =>
|
WithinMultipleContexts = (addresses: string[]) =>
|
||||||
this.merge({
|
this.merge(addresses.map(this.WithinContext))
|
||||||
fallbackPolicy: useMinimalFallbacks(RelayMode.Outbox),
|
|
||||||
scenarios: addresses.map(this.FetchFromContext),
|
|
||||||
})
|
|
||||||
|
|
||||||
PublishToContext = (address: string) => {
|
|
||||||
if (isGroupAddress(address)) {
|
|
||||||
return this.PublishToGroup(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCommunityAddress(address)) {
|
|
||||||
return this.PublishToCommunity(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unknown context ${address}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
PublishToContexts = (addresses: string[]) =>
|
|
||||||
this.merge({
|
|
||||||
fallbackPolicy: useMinimalFallbacks(RelayMode.Outbox),
|
|
||||||
scenarios: addresses.map(this.PublishToContext),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Higher level utils that use hints
|
// Higher level utils that use hints
|
||||||
|
|
||||||
tagPubkey = (pubkey: string) =>
|
tagPubkey = (pubkey: string) =>
|
||||||
Tag.from(["p", pubkey, this.FetchFromPubkey(pubkey).getUrl()])
|
Tag.from(["p", pubkey, this.FromPubkeys([pubkey]).getUrl()])
|
||||||
|
|
||||||
tagEventId = (event: Rumor, ...extra: string[]) =>
|
tagEventId = (event: Rumor, ...extra: string[]) =>
|
||||||
Tag.from(["e", event.id, this.FetchEvent(event).getUrl(), ...extra])
|
Tag.from(["e", event.id, this.Event(event).getUrl(), ...extra])
|
||||||
|
|
||||||
tagEventAddress = (event: UnsignedEvent, ...extra: string[]) =>
|
tagEventAddress = (event: UnsignedEvent, ...extra: string[]) =>
|
||||||
Tag.from(["a", getAddress(event), this.FetchEvent(event).getUrl(), ...extra])
|
Tag.from(["a", getAddress(event), this.Event(event).getUrl(), ...extra])
|
||||||
|
|
||||||
tagEvent = (event: Rumor, ...extra: string[]) => {
|
tagEvent = (event: Rumor, ...extra: string[]) => {
|
||||||
const tags = [this.tagEventId(event, ...extra)]
|
const tags = [this.tagEventId(event, ...extra)]
|
||||||
@@ -295,70 +203,68 @@ export class Router {
|
|||||||
kind: event.kind,
|
kind: event.kind,
|
||||||
pubkey: event.pubkey,
|
pubkey: event.pubkey,
|
||||||
identifier: Tags.fromEvent(event).get("d")?.value() || "",
|
identifier: Tags.fromEvent(event).get("d")?.value() || "",
|
||||||
relays: this.FetchEvent(event).getUrls(3),
|
relays: this.Event(event).limit(3).getUrls(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Router Scenario
|
// Router Scenario
|
||||||
|
|
||||||
export type RouterScenarioOptions = {
|
export type RouterScenarioOptions = {
|
||||||
getGroups: () => string[][]
|
mode?: RelayMode
|
||||||
fallbackPolicy: FallbackPolicy
|
limit?: number
|
||||||
|
policy?: FallbackPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RouteScenarioScores = Record<string, {score: number, count: number}>
|
|
||||||
|
|
||||||
export class RouterScenario {
|
export class RouterScenario {
|
||||||
constructor(readonly router: Router, readonly options: RouterScenarioOptions) {}
|
constructor(readonly router: Router, readonly groups: string[][], readonly options: RouterScenarioOptions = {}) {}
|
||||||
|
|
||||||
|
clone = (options: RouterScenarioOptions) =>
|
||||||
|
new RouterScenario(this.router, this.groups, {...this.options, ...options})
|
||||||
|
|
||||||
|
limit = (limit: number) => this.clone({limit})
|
||||||
|
|
||||||
|
mode = (mode: RelayMode) => this.clone({mode})
|
||||||
|
|
||||||
|
policy = (policy: FallbackPolicy) => this.clone({policy})
|
||||||
|
|
||||||
|
getLimit = () => this.options.limit || this.router.options.getDefaultLimit()
|
||||||
|
|
||||||
|
getPolicy = () => this.options.policy || addMaximalFallbacks
|
||||||
|
|
||||||
getFallbackRelays = () => {
|
getFallbackRelays = () => {
|
||||||
switch (this.options.fallbackPolicy.mode) {
|
const userRelays = shuffle(this.router.getUserRelays(this.options.mode))
|
||||||
case RelayMode.Inbox:
|
const defaultRelays = shuffle(this.router.options.getDefaultRelays(this.options.mode))
|
||||||
return this.router.options.getFallbackInboxRelays()
|
|
||||||
case RelayMode.Outbox:
|
return uniq([...userRelays, ...defaultRelays])
|
||||||
return this.router.options.getFallbackOutboxRelays()
|
|
||||||
default:
|
|
||||||
throw new Error(`Invalid relay mode ${this.options.fallbackPolicy.mode}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addFallbacks = (limit: number, urls: string[]) => {
|
getUrls = () => {
|
||||||
if (urls.length < limit) {
|
const fallbackPolicy = this.getPolicy()
|
||||||
const fallbackRelays = this.getFallbackRelays()
|
const limit = fallbackPolicy(this.getLimit())
|
||||||
const fallbackLimit = this.options.fallbackPolicy.getLimit(limit, urls)
|
const urls = this.router.groupsToUrls(this.groups)
|
||||||
|
|
||||||
return [...urls, ...fallbackRelays.slice(0, fallbackLimit)]
|
for (const url of this.getFallbackRelays()) {
|
||||||
|
if (urls.length >= limit) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!urls.includes(url)) {
|
||||||
|
urls.push(url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return urls
|
return urls.slice(0, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
getRawUrls = (limit?: number, extra: string[] = []) => {
|
getUrl = () => first(this.limit(1).getUrls())
|
||||||
const maxRelays = limit || this.router.options.getDefaultLimit()
|
|
||||||
const urlGroups = this.options.getGroups().concat([extra])
|
|
||||||
|
|
||||||
return this.router.groupsToUrls(maxRelays, urlGroups)
|
|
||||||
}
|
|
||||||
|
|
||||||
getUrls = (limit?: number, extra: string[] = []) => {
|
|
||||||
const maxRelays = limit || this.router.options.getDefaultLimit()
|
|
||||||
const urlGroups = [extra].concat(this.options.getGroups())
|
|
||||||
const urls = this.router.groupsToUrls(maxRelays, urlGroups)
|
|
||||||
|
|
||||||
return this.addFallbacks(maxRelays, urls)
|
|
||||||
}
|
|
||||||
|
|
||||||
getUrl = () => first(this.getUrls(1))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback Policy
|
// Fallback Policy
|
||||||
|
|
||||||
export class FallbackPolicy {
|
export type FallbackPolicy = (limit: number) => number
|
||||||
constructor(readonly mode: string, readonly getLimit: (limit: number, urls: string[]) => number) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useNoFallbacks = () => new FallbackPolicy(RelayMode.Inbox, (limit: number, urls: string[]) => 0)
|
export const addNoFallbacks = (limit: number) => 0
|
||||||
|
|
||||||
export const useMinimalFallbacks = (mode: string) => new FallbackPolicy(mode, (limit: number, urls: string[]) => urls.length === 0 ? 1 : 0)
|
export const addMinimalFallbacks = (limit: number) => 1
|
||||||
|
|
||||||
export const useMaximalFallbacks = (mode: string) => new FallbackPolicy(mode, (limit: number, urls: string[]) => Math.max(0, limit - urls.length))
|
export const addMaximalFallbacks = (limit: number) => limit
|
||||||
|
|||||||
Reference in New Issue
Block a user