Remove tsc-multi, re-install gts, apply autoformatting and linting
This commit is contained in:
@@ -15,21 +15,16 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/src/index.d.ts",
|
||||
"import": "./build/src/index.mjs",
|
||||
"require": "./build/src/index.cjs"
|
||||
"import": "./build/src/index.js",
|
||||
"require": "./build/src/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"pub": "npm run lint && npm run build && npm publish",
|
||||
"build": "gts clean && tsc-multi",
|
||||
"build": "gts clean && tsc",
|
||||
"lint": "gts lint",
|
||||
"fix": "gts fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gts": "^5.0.1",
|
||||
"tsc-multi": "^1.1.0",
|
||||
"typescript": "~5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@welshman/dvm": "~0.0.11",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {readable, derived, type Readable} from 'svelte/store'
|
||||
import {indexBy, type Maybe, now} from '@welshman/lib'
|
||||
import {withGetter} from '@welshman/store'
|
||||
import {getFreshness, setFreshnessThrottled} from './freshness'
|
||||
import {readable, derived, type Readable} from "svelte/store"
|
||||
import {indexBy, type Maybe, now} from "@welshman/lib"
|
||||
import {withGetter} from "@welshman/store"
|
||||
import {getFreshness, setFreshnessThrottled} from "./freshness.js"
|
||||
|
||||
export const collection = <T, LoadArgs extends any[]>({
|
||||
name,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {get} from 'svelte/store'
|
||||
import {ctx} from '@welshman/lib'
|
||||
import {addToListPublicly, removeFromList, makeList, FOLLOWS, MUTES} from '@welshman/util'
|
||||
import {userFollows, userMutes} from './user'
|
||||
import {nip44EncryptToSelf} from './session'
|
||||
import {publishThunk} from './thunk'
|
||||
import {get} from "svelte/store"
|
||||
import {ctx} from "@welshman/lib"
|
||||
import {addToListPublicly, removeFromList, makeList, FOLLOWS, MUTES} from "@welshman/util"
|
||||
import {userFollows, userMutes} from "./user.js"
|
||||
import {nip44EncryptToSelf} from "./session.js"
|
||||
import {publishThunk} from "./thunk.js"
|
||||
|
||||
export const unfollow = async (value: string) => {
|
||||
const list = get(userFollows) || makeList({kind: FOLLOWS})
|
||||
@@ -32,4 +32,3 @@ export const mute = async (tag: string[]) => {
|
||||
|
||||
return publishThunk({event, relays: ctx.app.router.FromUser().getUrls()})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import {partition} from "@welshman/lib"
|
||||
import {defaultOptimizeSubscriptions, getDefaultNetContext as originalGetDefaultNetContext} from "@welshman/net"
|
||||
import {
|
||||
defaultOptimizeSubscriptions,
|
||||
getDefaultNetContext as originalGetDefaultNetContext,
|
||||
} from "@welshman/net"
|
||||
import type {Subscription, RelaysAndFilters, NetContext} from "@welshman/net"
|
||||
import {LOCAL_RELAY_URL, isEphemeralKind, isDVMKind, unionFilters} from "@welshman/util"
|
||||
import {LOCAL_RELAY_URL, isEphemeralKind, isDVMKind, unionFilters} from "@welshman/util"
|
||||
import type {TrustedEvent, StampedEvent} from "@welshman/util"
|
||||
import {tracker, repository} from './core'
|
||||
import {makeRouter, getFilterSelections} from './router'
|
||||
import {signer} from './session'
|
||||
import type {Router} from './router'
|
||||
import {tracker, repository} from "./core.js"
|
||||
import {makeRouter, getFilterSelections} from "./router.js"
|
||||
import {signer} from "./session.js"
|
||||
import type {Router} from "./router.js"
|
||||
|
||||
export type AppContext = {
|
||||
router: Router
|
||||
@@ -52,4 +55,3 @@ export const getDefaultAppContext = (overrides: Partial<AppContext> = {}) => ({
|
||||
requestTimeout: 3000,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
|
||||
+29
-23
@@ -1,4 +1,4 @@
|
||||
import {throttle} from '@welshman/lib'
|
||||
import {throttle} from "@welshman/lib"
|
||||
import {Repository, Relay} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Tracker} from "@welshman/net"
|
||||
@@ -13,33 +13,39 @@ export const tracker = new Tracker()
|
||||
// Adapt above objects to stores
|
||||
|
||||
export const makeRepositoryStore = ({throttle: t = 300}: {throttle?: number} = {}) =>
|
||||
custom(setter => {
|
||||
let onUpdate = () => setter(repository)
|
||||
custom(
|
||||
setter => {
|
||||
let onUpdate = () => setter(repository)
|
||||
|
||||
if (t) {
|
||||
onUpdate = throttle(t, onUpdate)
|
||||
}
|
||||
if (t) {
|
||||
onUpdate = throttle(t, onUpdate)
|
||||
}
|
||||
|
||||
onUpdate()
|
||||
repository.on('update', onUpdate)
|
||||
onUpdate()
|
||||
repository.on("update", onUpdate)
|
||||
|
||||
return () => repository.off('update', onUpdate)
|
||||
}, {
|
||||
set: (other: Repository) => repository.load(other.dump()),
|
||||
})
|
||||
return () => repository.off("update", onUpdate)
|
||||
},
|
||||
{
|
||||
set: (other: Repository) => repository.load(other.dump()),
|
||||
},
|
||||
)
|
||||
|
||||
export const makeTrackerStore = ({throttle: t = 300}: {throttle?: number} = {}) =>
|
||||
custom(setter => {
|
||||
let onUpdate = () => setter(tracker)
|
||||
custom(
|
||||
setter => {
|
||||
let onUpdate = () => setter(tracker)
|
||||
|
||||
if (t) {
|
||||
onUpdate = throttle(t, onUpdate)
|
||||
}
|
||||
if (t) {
|
||||
onUpdate = throttle(t, onUpdate)
|
||||
}
|
||||
|
||||
onUpdate()
|
||||
tracker.on('update', onUpdate)
|
||||
onUpdate()
|
||||
tracker.on("update", onUpdate)
|
||||
|
||||
return () => tracker.off('update', onUpdate)
|
||||
}, {
|
||||
set: (other: Tracker) => tracker.load(other.relaysById),
|
||||
})
|
||||
return () => tracker.off("update", onUpdate)
|
||||
},
|
||||
{
|
||||
set: (other: Tracker) => tracker.load(other.relaysById),
|
||||
},
|
||||
)
|
||||
|
||||
+30
-29
@@ -1,23 +1,20 @@
|
||||
import {ctx, nthEq, now} from '@welshman/lib'
|
||||
import {createEvent, getPubkeyTagValues} from '@welshman/util'
|
||||
import {Scope, FeedController} from '@welshman/feeds'
|
||||
import type {RequestOpts, FeedOptions, DVMOpts, Feed} from '@welshman/feeds'
|
||||
import {makeDvmRequest, DVMEvent} from '@welshman/dvm'
|
||||
import {makeSecret, Nip01Signer} from '@welshman/signer'
|
||||
import {pubkey, signer} from './session'
|
||||
import {getFilterSelections} from './router'
|
||||
import {loadRelaySelections} from './relaySelections'
|
||||
import {wotGraph, maxWot, getFollows, getNetwork, getFollowers} from './wot'
|
||||
import {load} from './subscribe'
|
||||
import {ctx, nthEq, now} from "@welshman/lib"
|
||||
import {createEvent, getPubkeyTagValues} from "@welshman/util"
|
||||
import {Scope, FeedController} from "@welshman/feeds"
|
||||
import type {RequestOpts, FeedOptions, DVMOpts, Feed} from "@welshman/feeds"
|
||||
import {makeDvmRequest, DVMEvent} from "@welshman/dvm"
|
||||
import {makeSecret, Nip01Signer} from "@welshman/signer"
|
||||
import {pubkey, signer} from "./session.js"
|
||||
import {getFilterSelections} from "./router.js"
|
||||
import {loadRelaySelections} from "./relaySelections.js"
|
||||
import {wotGraph, maxWot, getFollows, getNetwork, getFollowers} from "./wot.js"
|
||||
import {load} from "./subscribe.js"
|
||||
|
||||
export const request = async ({filters = [{}], relays = [], onEvent}: RequestOpts) => {
|
||||
if (relays.length > 0) {
|
||||
await load({onEvent, filters, relays})
|
||||
} else {
|
||||
await Promise.all(
|
||||
getFilterSelections(filters)
|
||||
.map(opts => load({onEvent, ...opts}))
|
||||
)
|
||||
await Promise.all(getFilterSelections(filters).map(opts => load({onEvent, ...opts})))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,24 +29,23 @@ export const requestDVM = async ({kind, onEvent, ...request}: DVMOpts) => {
|
||||
const tags = request.tags || []
|
||||
const $signer = signer.get() || new Nip01Signer(makeSecret())
|
||||
const pubkey = await $signer.getPubkey()
|
||||
const relays =
|
||||
request.relays
|
||||
? ctx.app.router.FromRelays(request.relays).getUrls()
|
||||
: ctx.app.router.FromPubkeys(getPubkeyTagValues(tags)).getUrls()
|
||||
const relays = request.relays
|
||||
? ctx.app.router.FromRelays(request.relays).getUrls()
|
||||
: ctx.app.router.FromPubkeys(getPubkeyTagValues(tags)).getUrls()
|
||||
|
||||
if (!tags.some(nthEq(0, 'expiration'))) {
|
||||
if (!tags.some(nthEq(0, "expiration"))) {
|
||||
tags.push(["expiration", String(now() + 60)])
|
||||
}
|
||||
|
||||
if (!tags.some(nthEq(0, 'relays'))) {
|
||||
if (!tags.some(nthEq(0, "relays"))) {
|
||||
tags.push(["relays", ...relays])
|
||||
}
|
||||
|
||||
if (!tags.some(nthEq(1, 'user'))) {
|
||||
if (!tags.some(nthEq(1, "user"))) {
|
||||
tags.push(["param", "user", pubkey])
|
||||
}
|
||||
|
||||
if (!tags.some(nthEq(1, 'max_results'))) {
|
||||
if (!tags.some(nthEq(1, "max_results"))) {
|
||||
tags.push(["param", "max_results", "200"])
|
||||
}
|
||||
|
||||
@@ -72,11 +68,16 @@ export const getPubkeysForScope = (scope: string) => {
|
||||
}
|
||||
|
||||
switch (scope) {
|
||||
case Scope.Self: return [$pubkey]
|
||||
case Scope.Follows: return getFollows($pubkey)
|
||||
case Scope.Network: return getNetwork($pubkey)
|
||||
case Scope.Followers: return getFollowers($pubkey)
|
||||
default: return []
|
||||
case Scope.Self:
|
||||
return [$pubkey]
|
||||
case Scope.Follows:
|
||||
return getFollows($pubkey)
|
||||
case Scope.Network:
|
||||
return getNetwork($pubkey)
|
||||
case Scope.Followers:
|
||||
return getFollowers($pubkey)
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +95,7 @@ export const getPubkeysForWOTRange = (min: number, max: number) => {
|
||||
return pubkeys
|
||||
}
|
||||
|
||||
type _FeedOptions = Partial<Omit<FeedOptions, 'feed'>> & {feed: Feed}
|
||||
type _FeedOptions = Partial<Omit<FeedOptions, "feed">> & {feed: Feed}
|
||||
|
||||
export const createFeedController = (options: _FeedOptions) =>
|
||||
new FeedController({
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import {FOLLOWS, asDecryptedEvent, readList} from '@welshman/util'
|
||||
import {type TrustedEvent, type PublishedList} from '@welshman/util'
|
||||
import {FOLLOWS, asDecryptedEvent, readList} from "@welshman/util"
|
||||
import {type TrustedEvent, type PublishedList} from "@welshman/util"
|
||||
import {type SubscribeRequestWithHandlers} from "@welshman/net"
|
||||
import {deriveEventsMapped} from '@welshman/store'
|
||||
import {repository} from './core'
|
||||
import {load} from './subscribe'
|
||||
import {collection} from './collection'
|
||||
import {loadRelaySelections} from './relaySelections'
|
||||
import {deriveEventsMapped} from "@welshman/store"
|
||||
import {repository} from "./core.js"
|
||||
import {load} from "./subscribe.js"
|
||||
import {collection} from "./collection.js"
|
||||
import {loadRelaySelections} from "./relaySelections.js"
|
||||
|
||||
export const follows = deriveEventsMapped<PublishedList>(repository, {
|
||||
filters: [{kinds: [FOLLOWS]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: (event: TrustedEvent) =>
|
||||
readList(asDecryptedEvent(event)),
|
||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||
})
|
||||
|
||||
export const {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {writable} from 'svelte/store'
|
||||
import {assoc, batch} from '@welshman/lib'
|
||||
import {withGetter} from '@welshman/store'
|
||||
import {writable} from "svelte/store"
|
||||
import {assoc, batch} from "@welshman/lib"
|
||||
import {withGetter} from "@welshman/store"
|
||||
|
||||
export type FreshnessUpdate = {
|
||||
ns: string
|
||||
@@ -25,5 +25,5 @@ export const setFreshnessThrottled = batch(100, (updates: FreshnessUpdate[]) =>
|
||||
}
|
||||
|
||||
return $freshness
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
+30
-21
@@ -1,8 +1,8 @@
|
||||
import {writable, derived} from 'svelte/store'
|
||||
import {writable, derived} from "svelte/store"
|
||||
import {type SubscribeRequestWithHandlers} from "@welshman/net"
|
||||
import {ctx, tryCatch, fetchJson, uniq, batcher, postJson, last} from '@welshman/lib'
|
||||
import {collection} from './collection'
|
||||
import {deriveProfile} from './profiles'
|
||||
import {ctx, tryCatch, fetchJson, uniq, batcher, postJson, last} from "@welshman/lib"
|
||||
import {collection} from "./collection.js"
|
||||
import {deriveProfile} from "./profiles.js"
|
||||
|
||||
export type Handle = {
|
||||
nip05: string
|
||||
@@ -18,10 +18,14 @@ export async function queryProfile(nip05: string) {
|
||||
|
||||
if (!match) return undefined
|
||||
|
||||
const [_, name = '_', domain] = match
|
||||
const [_, name = "_", domain] = match
|
||||
|
||||
try {
|
||||
const {names, relays = {}, nip46 = {}} = await fetchJson(`https://${domain}/.well-known/nostr.json?name=${name}`)
|
||||
const {
|
||||
names,
|
||||
relays = {},
|
||||
nip46 = {},
|
||||
} = await fetchJson(`https://${domain}/.well-known/nostr.json?name=${name}`)
|
||||
|
||||
const pubkey = names[name]
|
||||
|
||||
@@ -48,14 +52,19 @@ export const fetchHandles = async (nip05s: string[]) => {
|
||||
|
||||
// Use dufflepud if we it's set up to protect user privacy, otherwise fetch directly
|
||||
if (base) {
|
||||
const res: any = await tryCatch(async () => await postJson(`${base}/handle/info`, {handles: nip05s}))
|
||||
const res: any = await tryCatch(
|
||||
async () => await postJson(`${base}/handle/info`, {handles: nip05s}),
|
||||
)
|
||||
|
||||
for (const {handle: nip05, info} of res?.data || []) {
|
||||
handlesByNip05.set(nip05, info)
|
||||
}
|
||||
} else {
|
||||
const results = await Promise.all(
|
||||
nip05s.map(async nip05 => ({nip05, info: await tryCatch(async () => await queryProfile(nip05))}))
|
||||
nip05s.map(async nip05 => ({
|
||||
nip05,
|
||||
info: await tryCatch(async () => await queryProfile(nip05)),
|
||||
})),
|
||||
)
|
||||
|
||||
for (const {nip05, info} of results) {
|
||||
@@ -94,21 +103,21 @@ export const {
|
||||
}),
|
||||
})
|
||||
|
||||
export const deriveHandleForPubkey = (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) =>
|
||||
derived(
|
||||
[handlesByNip05, deriveProfile(pubkey, request)],
|
||||
([$handlesByNip05, $profile]) => {
|
||||
if (!$profile?.nip05) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
loadHandle($profile.nip05)
|
||||
|
||||
return $handlesByNip05.get($profile.nip05)
|
||||
export const deriveHandleForPubkey = (
|
||||
pubkey: string,
|
||||
request: Partial<SubscribeRequestWithHandlers> = {},
|
||||
) =>
|
||||
derived([handlesByNip05, deriveProfile(pubkey, request)], ([$handlesByNip05, $profile]) => {
|
||||
if (!$profile?.nip05) {
|
||||
return undefined
|
||||
}
|
||||
)
|
||||
|
||||
loadHandle($profile.nip05)
|
||||
|
||||
return $handlesByNip05.get($profile.nip05)
|
||||
})
|
||||
|
||||
export const displayNip05 = (nip05: string) =>
|
||||
(nip05?.startsWith("_@") ? last(nip05.split("@")) : nip05)
|
||||
nip05?.startsWith("_@") ? last(nip05.split("@")) : nip05
|
||||
|
||||
export const displayHandle = (handle: Handle) => displayNip05(handle.nip05)
|
||||
|
||||
+36
-26
@@ -1,26 +1,36 @@
|
||||
export * from './context'
|
||||
export * from './core'
|
||||
export * from './collection'
|
||||
export * from './commands'
|
||||
export * from './feeds'
|
||||
export * from './freshness'
|
||||
export * from './follows'
|
||||
export * from './handles'
|
||||
export * from './mutes'
|
||||
export * from './plaintext'
|
||||
export * from './profiles'
|
||||
export * from './relays'
|
||||
export * from './relaySelections'
|
||||
export * from './router'
|
||||
export * from './search'
|
||||
export * from './session'
|
||||
export * from './storage'
|
||||
export * from './subscribe'
|
||||
export * from './sync'
|
||||
export * from './tags'
|
||||
export * from './thunk'
|
||||
export * from './topics'
|
||||
export * from './user'
|
||||
export * from './util'
|
||||
export * from './wot'
|
||||
export * from './zappers'
|
||||
export * from "./context.js"
|
||||
export * from "./core.js"
|
||||
export * from "./collection.js"
|
||||
export * from "./commands.js"
|
||||
export * from "./feeds.js"
|
||||
export * from "./freshness.js"
|
||||
export * from "./follows.js"
|
||||
export * from "./handles.js"
|
||||
export * from "./mutes.js"
|
||||
export * from "./plaintext.js"
|
||||
export * from "./profiles.js"
|
||||
export * from "./relays.js"
|
||||
export * from "./relaySelections.js"
|
||||
export * from "./router.js"
|
||||
export * from "./search.js"
|
||||
export * from "./session.js"
|
||||
export * from "./storage.js"
|
||||
export * from "./subscribe.js"
|
||||
export * from "./sync.js"
|
||||
export * from "./tags.js"
|
||||
export * from "./thunk.js"
|
||||
export * from "./topics.js"
|
||||
export * from "./user.js"
|
||||
export * from "./util.js"
|
||||
export * from "./wot.js"
|
||||
export * from "./zappers.js"
|
||||
|
||||
import type {NetContext} from "@welshman/net"
|
||||
import type {AppContext} from "./context.js"
|
||||
|
||||
declare module "@welshman/lib" {
|
||||
interface Context {
|
||||
net: NetContext
|
||||
app: AppContext
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {MUTES, asDecryptedEvent, readList} from '@welshman/util'
|
||||
import {type TrustedEvent, type PublishedList} from '@welshman/util'
|
||||
import {MUTES, asDecryptedEvent, readList} from "@welshman/util"
|
||||
import {type TrustedEvent, type PublishedList} from "@welshman/util"
|
||||
import {type SubscribeRequestWithHandlers} from "@welshman/net"
|
||||
import {deriveEventsMapped} from '@welshman/store'
|
||||
import {repository} from './core'
|
||||
import {load} from './subscribe'
|
||||
import {collection} from './collection'
|
||||
import {ensurePlaintext} from './plaintext'
|
||||
import {loadRelaySelections} from './relaySelections'
|
||||
import {deriveEventsMapped} from "@welshman/store"
|
||||
import {repository} from "./core.js"
|
||||
import {load} from "./subscribe.js"
|
||||
import {collection} from "./collection.js"
|
||||
import {ensurePlaintext} from "./plaintext.js"
|
||||
import {loadRelaySelections} from "./relaySelections.js"
|
||||
|
||||
export const mutes = deriveEventsMapped<PublishedList>(repository, {
|
||||
filters: [{kinds: [MUTES]}],
|
||||
@@ -32,4 +32,3 @@ export const {
|
||||
await load({...request, filters: [{kinds: [MUTES], authors: [pubkey]}]})
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {writable} from 'svelte/store'
|
||||
import {assoc} from '@welshman/lib'
|
||||
import type {TrustedEvent} from '@welshman/util'
|
||||
import {withGetter} from '@welshman/store'
|
||||
import {writable} from "svelte/store"
|
||||
import {assoc} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {withGetter} from "@welshman/store"
|
||||
import {decrypt} from "@welshman/signer"
|
||||
import {getSigner, getSession} from './session'
|
||||
import {getSigner, getSession} from "./session.js"
|
||||
|
||||
export const plaintext = withGetter(writable<Record<string, string>>({}))
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import {derived, readable} from 'svelte/store'
|
||||
import {readProfile, displayProfile, displayPubkey, PROFILE} from '@welshman/util'
|
||||
import {derived, readable} from "svelte/store"
|
||||
import {readProfile, displayProfile, displayPubkey, PROFILE} from "@welshman/util"
|
||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
||||
import type {PublishedProfile} from "@welshman/util"
|
||||
import {deriveEventsMapped, withGetter} from '@welshman/store'
|
||||
import {repository} from './core'
|
||||
import {load} from './subscribe'
|
||||
import {collection} from './collection'
|
||||
import {loadRelaySelections} from './relaySelections'
|
||||
import {deriveEventsMapped, withGetter} from "@welshman/store"
|
||||
import {repository} from "./core.js"
|
||||
import {load} from "./subscribe.js"
|
||||
import {collection} from "./collection.js"
|
||||
import {loadRelaySelections} from "./relaySelections.js"
|
||||
|
||||
export const profiles = withGetter(
|
||||
deriveEventsMapped<PublishedProfile>(repository, {
|
||||
filters: [{kinds: [PROFILE]}],
|
||||
eventToItem: readProfile,
|
||||
itemToEvent: item => item.event,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const {
|
||||
@@ -45,9 +45,7 @@ export const {
|
||||
})
|
||||
|
||||
export const displayProfileByPubkey = (pubkey: string | undefined) =>
|
||||
pubkey
|
||||
? displayProfile(profilesByPubkey.get().get(pubkey), displayPubkey(pubkey))
|
||||
: ""
|
||||
pubkey ? displayProfile(profilesByPubkey.get().get(pubkey), displayPubkey(pubkey)) : ""
|
||||
|
||||
export const deriveProfileDisplay = (pubkey: string | undefined) =>
|
||||
pubkey
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import {uniq} from '@welshman/lib'
|
||||
import {INBOX_RELAYS, RELAYS, normalizeRelayUrl, asDecryptedEvent, readList, getListTags, getRelayTags, getRelayTagValues} from '@welshman/util'
|
||||
import type {TrustedEvent, PublishedList, List} from '@welshman/util'
|
||||
import {uniq} from "@welshman/lib"
|
||||
import {
|
||||
INBOX_RELAYS,
|
||||
RELAYS,
|
||||
normalizeRelayUrl,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
getListTags,
|
||||
getRelayTags,
|
||||
getRelayTagValues,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, PublishedList, List} from "@welshman/util"
|
||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
||||
import {deriveEventsMapped} from '@welshman/store'
|
||||
import {repository} from './core'
|
||||
import {load} from './subscribe'
|
||||
import {collection} from './collection'
|
||||
import {deriveEventsMapped} from "@welshman/store"
|
||||
import {repository} from "./core.js"
|
||||
import {load} from "./subscribe.js"
|
||||
import {collection} from "./collection.js"
|
||||
|
||||
export const getRelayUrls = (list?: List): string[] =>
|
||||
uniq(getRelayTagValues(getListTags(list)).map(normalizeRelayUrl))
|
||||
@@ -14,21 +23,20 @@ export const getReadRelayUrls = (list?: List): string[] =>
|
||||
uniq(
|
||||
getRelayTags(getListTags(list))
|
||||
.filter((t: string[]) => !t[2] || t[2] === "read")
|
||||
.map((t: string[]) => normalizeRelayUrl(t[1]))
|
||||
.map((t: string[]) => normalizeRelayUrl(t[1])),
|
||||
)
|
||||
|
||||
export const getWriteRelayUrls = (list?: List): string[] =>
|
||||
uniq(
|
||||
getRelayTags(getListTags(list))
|
||||
.filter((t: string[]) => !t[2] || t[2] === "write")
|
||||
.map((t: string[]) => normalizeRelayUrl(t[1]))
|
||||
.map((t: string[]) => normalizeRelayUrl(t[1])),
|
||||
)
|
||||
|
||||
export const relaySelections = deriveEventsMapped<PublishedList>(repository, {
|
||||
filters: [{kinds: [RELAYS]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: (event: TrustedEvent) =>
|
||||
readList(asDecryptedEvent(event)),
|
||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||
})
|
||||
|
||||
export const {
|
||||
@@ -46,8 +54,7 @@ export const {
|
||||
export const inboxRelaySelections = deriveEventsMapped<PublishedList>(repository, {
|
||||
filters: [{kinds: [INBOX_RELAYS]}],
|
||||
itemToEvent: item => item.event,
|
||||
eventToItem: (event: TrustedEvent) =>
|
||||
readList(asDecryptedEvent(event)),
|
||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||
})
|
||||
|
||||
export const {
|
||||
|
||||
+87
-57
@@ -1,11 +1,11 @@
|
||||
import {writable, derived} from 'svelte/store'
|
||||
import {withGetter} from '@welshman/store'
|
||||
import {ctx, groupBy, indexBy, batch, now, ago, uniq, batcher, postJson} from '@welshman/lib'
|
||||
import {writable, derived} from "svelte/store"
|
||||
import {withGetter} from "@welshman/store"
|
||||
import {ctx, groupBy, indexBy, batch, now, ago, uniq, batcher, postJson} from "@welshman/lib"
|
||||
import type {RelayProfile} from "@welshman/util"
|
||||
import {normalizeRelayUrl, displayRelayUrl, displayRelayProfile} from "@welshman/util"
|
||||
import {ConnectionEvent} from '@welshman/net'
|
||||
import type {Connection, Message} from '@welshman/net'
|
||||
import {collection} from './collection'
|
||||
import {ConnectionEvent} from "@welshman/net"
|
||||
import type {Connection, Message} from "@welshman/net"
|
||||
import {collection} from "./collection.js"
|
||||
|
||||
export type RelayStats = {
|
||||
first_seen: number
|
||||
@@ -151,78 +151,108 @@ const updateRelayStats = batch(500, (updates: RelayStatsUpdate[]) => {
|
||||
})
|
||||
|
||||
const onConnectionOpen = ({url}: Connection) =>
|
||||
updateRelayStats([url, stats => {
|
||||
stats.last_open = now()
|
||||
stats.open_count++
|
||||
}])
|
||||
updateRelayStats([
|
||||
url,
|
||||
stats => {
|
||||
stats.last_open = now()
|
||||
stats.open_count++
|
||||
},
|
||||
])
|
||||
|
||||
const onConnectionClose = ({url}: Connection) =>
|
||||
updateRelayStats([url, stats => {
|
||||
stats.last_close = now()
|
||||
stats.close_count++
|
||||
}])
|
||||
updateRelayStats([
|
||||
url,
|
||||
stats => {
|
||||
stats.last_close = now()
|
||||
stats.close_count++
|
||||
},
|
||||
])
|
||||
|
||||
const onConnectionSend = ({url}: Connection, [verb]: Message) => {
|
||||
if (verb === 'REQ') {
|
||||
updateRelayStats([url, stats => {
|
||||
stats.request_count++
|
||||
stats.last_request = now()
|
||||
}])
|
||||
} else if (verb === 'EVENT') {
|
||||
updateRelayStats([url, stats => {
|
||||
stats.publish_count++
|
||||
stats.last_publish = now()
|
||||
}])
|
||||
if (verb === "REQ") {
|
||||
updateRelayStats([
|
||||
url,
|
||||
stats => {
|
||||
stats.request_count++
|
||||
stats.last_request = now()
|
||||
},
|
||||
])
|
||||
} else if (verb === "EVENT") {
|
||||
updateRelayStats([
|
||||
url,
|
||||
stats => {
|
||||
stats.publish_count++
|
||||
stats.last_publish = now()
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const onConnectionReceive = ({url, state}: Connection, [verb, ...extra]: Message) => {
|
||||
if (verb === 'OK') {
|
||||
if (verb === "OK") {
|
||||
const [eventId, ok] = extra
|
||||
const pub = state.pendingPublishes.get(eventId)
|
||||
|
||||
updateRelayStats([url, stats => {
|
||||
if (pub) {
|
||||
stats.publish_timer += ago(pub.sent)
|
||||
}
|
||||
updateRelayStats([
|
||||
url,
|
||||
stats => {
|
||||
if (pub) {
|
||||
stats.publish_timer += ago(pub.sent)
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
stats.publish_success_count++
|
||||
} else {
|
||||
stats.publish_failure_count++
|
||||
}
|
||||
}])
|
||||
} else if (verb === 'AUTH') {
|
||||
updateRelayStats([url, stats => {
|
||||
stats.last_auth = now()
|
||||
}])
|
||||
} else if (verb === 'EVENT') {
|
||||
updateRelayStats([url, stats => {
|
||||
stats.event_count++
|
||||
stats.last_event = now()
|
||||
}])
|
||||
} else if (verb === 'EOSE') {
|
||||
if (ok) {
|
||||
stats.publish_success_count++
|
||||
} else {
|
||||
stats.publish_failure_count++
|
||||
}
|
||||
},
|
||||
])
|
||||
} else if (verb === "AUTH") {
|
||||
updateRelayStats([
|
||||
url,
|
||||
stats => {
|
||||
stats.last_auth = now()
|
||||
},
|
||||
])
|
||||
} else if (verb === "EVENT") {
|
||||
updateRelayStats([
|
||||
url,
|
||||
stats => {
|
||||
stats.event_count++
|
||||
stats.last_event = now()
|
||||
},
|
||||
])
|
||||
} else if (verb === "EOSE") {
|
||||
const request = state.pendingRequests.get(extra[0])
|
||||
|
||||
// Only count the first eose
|
||||
if (request && !request.eose) {
|
||||
updateRelayStats([url, stats => {
|
||||
stats.eose_count++
|
||||
stats.eose_timer += now() - request.sent
|
||||
}])
|
||||
updateRelayStats([
|
||||
url,
|
||||
stats => {
|
||||
stats.eose_count++
|
||||
stats.eose_timer += now() - request.sent
|
||||
},
|
||||
])
|
||||
}
|
||||
} else if (verb === 'NOTICE') {
|
||||
updateRelayStats([url, stats => {
|
||||
stats.notice_count++
|
||||
}])
|
||||
} else if (verb === "NOTICE") {
|
||||
updateRelayStats([
|
||||
url,
|
||||
stats => {
|
||||
stats.notice_count++
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const onConnectionError = ({url}: Connection) =>
|
||||
updateRelayStats([url, stats => {
|
||||
stats.last_error = now()
|
||||
stats.recent_errors = uniq(stats.recent_errors.concat(now())).slice(-10)
|
||||
}])
|
||||
updateRelayStats([
|
||||
url,
|
||||
stats => {
|
||||
stats.last_error = now()
|
||||
stats.recent_errors = uniq(stats.recent_errors.concat(now())).slice(-10)
|
||||
},
|
||||
])
|
||||
|
||||
export const trackRelayStats = (connection: Connection) => {
|
||||
connection.on(ConnectionEvent.Open, onConnectionOpen)
|
||||
|
||||
+70
-70
@@ -32,15 +32,15 @@ import {
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, Filter} from "@welshman/util"
|
||||
import type {RelaysAndFilters} from "@welshman/net"
|
||||
import {pubkey} from "./session"
|
||||
import {pubkey} from "./session.js"
|
||||
import {
|
||||
relaySelectionsByPubkey,
|
||||
inboxRelaySelectionsByPubkey,
|
||||
getReadRelayUrls,
|
||||
getWriteRelayUrls,
|
||||
getRelayUrls,
|
||||
} from "./relaySelections"
|
||||
import {relays, relaysByUrl} from "./relays"
|
||||
} from "./relaySelections.js"
|
||||
import {relays, relaysByUrl} from "./relays.js"
|
||||
|
||||
export const INDEXED_KINDS = [PROFILE, RELAYS, INBOX_RELAYS, FOLLOWS]
|
||||
|
||||
@@ -99,12 +99,14 @@ export type RouterOptions = {
|
||||
}
|
||||
|
||||
export type Selection = {
|
||||
weight: number,
|
||||
relays: string[],
|
||||
weight: number
|
||||
relays: string[]
|
||||
}
|
||||
|
||||
const makeSelection = (relays: string[], weight = 1): Selection =>
|
||||
({relays: relays.map(normalizeRelayUrl), weight})
|
||||
const makeSelection = (relays: string[], weight = 1): Selection => ({
|
||||
relays: relays.map(normalizeRelayUrl),
|
||||
weight,
|
||||
})
|
||||
|
||||
// Fallback policies
|
||||
|
||||
@@ -112,7 +114,7 @@ export type FallbackPolicy = (count: number, limit: number) => number
|
||||
|
||||
export const addNoFallbacks = (count: number, limit: number) => 0
|
||||
|
||||
export const addMinimalFallbacks = (count: number, limit: number) => count > 0 ? 0 : 1
|
||||
export const addMinimalFallbacks = (count: number, limit: number) => (count > 0 ? 0 : 1)
|
||||
|
||||
export const addMaximalFallbacks = (count: number, limit: number) => limit - count
|
||||
|
||||
@@ -142,35 +144,26 @@ export class Router {
|
||||
|
||||
// Routing scenarios
|
||||
|
||||
FromRelays = (relays: string[]) =>
|
||||
this.scenario([makeSelection(relays)])
|
||||
FromRelays = (relays: string[]) => this.scenario([makeSelection(relays)])
|
||||
|
||||
ForUser = () =>
|
||||
this.FromRelays(this.getRelaysForUser(RelayMode.Read))
|
||||
ForUser = () => this.FromRelays(this.getRelaysForUser(RelayMode.Read))
|
||||
|
||||
FromUser = () =>
|
||||
this.FromRelays(this.getRelaysForUser(RelayMode.Write))
|
||||
FromUser = () => this.FromRelays(this.getRelaysForUser(RelayMode.Write))
|
||||
|
||||
UserInbox = () =>
|
||||
this.FromRelays(this.getRelaysForUser(RelayMode.Inbox)).policy(addNoFallbacks)
|
||||
UserInbox = () => this.FromRelays(this.getRelaysForUser(RelayMode.Inbox)).policy(addNoFallbacks)
|
||||
|
||||
ForPubkey = (pubkey: string) =>
|
||||
this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Read))
|
||||
ForPubkey = (pubkey: string) => this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Read))
|
||||
|
||||
FromPubkey = (pubkey: string) =>
|
||||
this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Write))
|
||||
FromPubkey = (pubkey: string) => this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Write))
|
||||
|
||||
PubkeyInbox = (pubkey: string) =>
|
||||
this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Inbox)).policy(addNoFallbacks)
|
||||
|
||||
ForPubkeys = (pubkeys: string[]) =>
|
||||
this.merge(pubkeys.map(pubkey => this.ForPubkey(pubkey)))
|
||||
ForPubkeys = (pubkeys: string[]) => this.merge(pubkeys.map(pubkey => this.ForPubkey(pubkey)))
|
||||
|
||||
FromPubkeys = (pubkeys: string[]) =>
|
||||
this.merge(pubkeys.map(pubkey => this.FromPubkey(pubkey)))
|
||||
FromPubkeys = (pubkeys: string[]) => this.merge(pubkeys.map(pubkey => this.FromPubkey(pubkey)))
|
||||
|
||||
PubkeyInboxes = (pubkeys: string[]) =>
|
||||
this.merge(pubkeys.map(pubkey => this.PubkeyInbox(pubkey)))
|
||||
PubkeyInboxes = (pubkeys: string[]) => this.merge(pubkeys.map(pubkey => this.PubkeyInbox(pubkey)))
|
||||
|
||||
Event = (event: TrustedEvent) =>
|
||||
this.FromRelays(this.getRelaysForPubkey(event.pubkey, RelayMode.Write))
|
||||
@@ -180,10 +173,7 @@ export class Router {
|
||||
|
||||
Quote = (event: TrustedEvent, value: string, relays: string[] = []) => {
|
||||
const tag = event.tags.find(t => t[1] === value)
|
||||
const scenarios = [
|
||||
this.ForPubkey(event.pubkey),
|
||||
this.FromPubkey(event.pubkey),
|
||||
]
|
||||
const scenarios = [this.ForPubkey(event.pubkey), this.FromPubkey(event.pubkey)]
|
||||
|
||||
if (tag?.[2] && isShareableRelayUrl(tag[2])) {
|
||||
scenarios.push(this.FromRelays([tag[2]]))
|
||||
@@ -198,21 +188,19 @@ export class Router {
|
||||
|
||||
EventAncestors = (event: TrustedEvent, type: "mentions" | "replies" | "roots") => {
|
||||
return this.scenario(
|
||||
getAncestorTags(event.tags)[type].flatMap(
|
||||
([_, value, relay, pubkey]) => {
|
||||
const selections = [makeSelection(this.ForUser().getUrls(), 0.5)]
|
||||
getAncestorTags(event.tags)[type].flatMap(([_, value, relay, pubkey]) => {
|
||||
const selections = [makeSelection(this.ForUser().getUrls(), 0.5)]
|
||||
|
||||
if (pubkey) {
|
||||
selections.push(makeSelection(this.FromPubkey(pubkey).getUrls()))
|
||||
}
|
||||
|
||||
if (relay) {
|
||||
selections.push(makeSelection([relay], 0.9))
|
||||
}
|
||||
|
||||
return selections
|
||||
if (pubkey) {
|
||||
selections.push(makeSelection(this.FromPubkey(pubkey).getUrls()))
|
||||
}
|
||||
)
|
||||
|
||||
if (relay) {
|
||||
selections.push(makeSelection([relay], 0.9))
|
||||
}
|
||||
|
||||
return selections
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -240,16 +228,28 @@ export type RouterScenarioOptions = {
|
||||
}
|
||||
|
||||
export class RouterScenario {
|
||||
constructor(readonly router: Router, readonly selections: Selection[], readonly options: RouterScenarioOptions = {}) {}
|
||||
constructor(
|
||||
readonly router: Router,
|
||||
readonly selections: Selection[],
|
||||
readonly options: RouterScenarioOptions = {},
|
||||
) {}
|
||||
|
||||
clone = (options: RouterScenarioOptions) =>
|
||||
new RouterScenario(this.router, this.selections, {...this.options, ...options})
|
||||
|
||||
filter = (f: (selection: Selection) => boolean) =>
|
||||
new RouterScenario(this.router, this.selections.filter(selection => f(selection)), this.options)
|
||||
new RouterScenario(
|
||||
this.router,
|
||||
this.selections.filter(selection => f(selection)),
|
||||
this.options,
|
||||
)
|
||||
|
||||
update = (f: (selection: Selection) => Selection) =>
|
||||
new RouterScenario(this.router, this.selections.map(selection => f(selection)), this.options)
|
||||
new RouterScenario(
|
||||
this.router,
|
||||
this.selections.map(selection => f(selection)),
|
||||
this.options,
|
||||
)
|
||||
|
||||
policy = (policy: FallbackPolicy) => this.clone({policy})
|
||||
|
||||
@@ -290,11 +290,7 @@ export class RouterScenario {
|
||||
|
||||
const relays = take(
|
||||
limit,
|
||||
sortBy(
|
||||
scoreRelay,
|
||||
Array.from(relayWeights.keys())
|
||||
.filter(scoreRelay)
|
||||
)
|
||||
sortBy(scoreRelay, Array.from(relayWeights.keys()).filter(scoreRelay)),
|
||||
)
|
||||
|
||||
const fallbacksNeeded = fallbackPolicy(relays.length, limit)
|
||||
@@ -348,14 +344,14 @@ export const getIndexerRelays = () => ctx.app.indexerRelays || getFallbackRelays
|
||||
export const getFallbackRelays = throttleWithValue(300, () =>
|
||||
sortBy(r => -getRelayQuality(r.url), relays.get())
|
||||
.slice(0, 30)
|
||||
.map(r => r.url)
|
||||
.map(r => r.url),
|
||||
)
|
||||
|
||||
export const getSearchRelays = throttleWithValue(300, () =>
|
||||
sortBy(r => -getRelayQuality(r.url), relays.get())
|
||||
.filter(r => r.profile?.supported_nips?.includes(50))
|
||||
.slice(0, 30)
|
||||
.map(r => r.url)
|
||||
.map(r => r.url),
|
||||
)
|
||||
|
||||
export const makeRouter = (options: Partial<RouterOptions> = {}) =>
|
||||
@@ -372,7 +368,7 @@ export const makeRouter = (options: Partial<RouterOptions> = {}) =>
|
||||
|
||||
// Infer relay selections from filters
|
||||
|
||||
type FilterScenario = {filter: Filter, scenario: RouterScenario}
|
||||
type FilterScenario = {filter: Filter; scenario: RouterScenario}
|
||||
|
||||
type FilterSelectionRule = (filter: Filter) => FilterScenario[]
|
||||
|
||||
@@ -387,10 +383,12 @@ export const getFilterSelectionsForSearch = (filter: Filter) => {
|
||||
export const getFilterSelectionsForWraps = (filter: Filter) => {
|
||||
if (!filter.kinds?.includes(WRAP) || filter.authors) return []
|
||||
|
||||
return [{
|
||||
filter: {...filter, kinds: [WRAP]},
|
||||
scenario: ctx.app.router.UserInbox(),
|
||||
}]
|
||||
return [
|
||||
{
|
||||
filter: {...filter, kinds: [WRAP]},
|
||||
scenario: ctx.app.router.UserInbox(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const getFilterSelectionsForIndexedKinds = (filter: Filter) => {
|
||||
@@ -400,10 +398,12 @@ export const getFilterSelectionsForIndexedKinds = (filter: Filter) => {
|
||||
|
||||
const relays = ctx.app.router.options.getIndexerRelays?.() || []
|
||||
|
||||
return [{
|
||||
filter: {...filter, kinds},
|
||||
scenario: ctx.app.router.FromRelays(relays),
|
||||
}]
|
||||
return [
|
||||
{
|
||||
filter: {...filter, kinds},
|
||||
scenario: ctx.app.router.FromRelays(relays),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const getFilterSelectionsForAuthors = (filter: Filter) => {
|
||||
@@ -411,15 +411,15 @@ export const getFilterSelectionsForAuthors = (filter: Filter) => {
|
||||
|
||||
const chunkCount = clamp([1, 30], Math.round(filter.authors.length / 30))
|
||||
|
||||
return chunks(chunkCount, filter.authors)
|
||||
.map(authors => ({
|
||||
filter: {...filter, authors},
|
||||
scenario: ctx.app.router.FromPubkeys(authors),
|
||||
}))
|
||||
return chunks(chunkCount, filter.authors).map(authors => ({
|
||||
filter: {...filter, authors},
|
||||
scenario: ctx.app.router.FromPubkeys(authors),
|
||||
}))
|
||||
}
|
||||
|
||||
export const getFilterSelectionsForUser = (filter: Filter) =>
|
||||
[{filter, scenario: ctx.app.router.ForUser().weight(0.2)}]
|
||||
export const getFilterSelectionsForUser = (filter: Filter) => [
|
||||
{filter, scenario: ctx.app.router.ForUser().weight(0.2)},
|
||||
]
|
||||
|
||||
export const defaultFilterSelectionRules = [
|
||||
getFilterSelectionsForSearch,
|
||||
@@ -431,7 +431,7 @@ export const defaultFilterSelectionRules = [
|
||||
|
||||
export const getFilterSelections = (
|
||||
filters: Filter[],
|
||||
rules: FilterSelectionRule[] = defaultFilterSelectionRules
|
||||
rules: FilterSelectionRule[] = defaultFilterSelectionRules,
|
||||
): RelaysAndFilters[] => {
|
||||
const filtersById = new Map<string, Filter>()
|
||||
const scenariosById = new Map<string, RouterScenario[]>()
|
||||
|
||||
+18
-19
@@ -1,19 +1,19 @@
|
||||
import Fuse from "fuse.js"
|
||||
import type {IFuseOptions, FuseResult} from "fuse.js"
|
||||
import {debounce} from 'throttle-debounce'
|
||||
import {derived} from 'svelte/store'
|
||||
import {dec, sortBy} from '@welshman/lib'
|
||||
import {PROFILE} from '@welshman/util'
|
||||
import {throttled} from '@welshman/store'
|
||||
import {debounce} from "throttle-debounce"
|
||||
import {derived} from "svelte/store"
|
||||
import {dec, sortBy} from "@welshman/lib"
|
||||
import {PROFILE} from "@welshman/util"
|
||||
import {throttled} from "@welshman/store"
|
||||
import type {PublishedProfile} from "@welshman/util"
|
||||
import {load} from './subscribe'
|
||||
import {wotGraph} from './wot'
|
||||
import {profiles} from './profiles'
|
||||
import {topics} from './topics'
|
||||
import type {Topic} from './topics'
|
||||
import {relays} from './relays'
|
||||
import type {Relay} from './relays'
|
||||
import {handlesByNip05} from './handles'
|
||||
import {load} from "./subscribe.js"
|
||||
import {wotGraph} from "./wot.js"
|
||||
import {profiles} from "./profiles.js"
|
||||
import {topics} from "./topics.js"
|
||||
import type {Topic} from "./topics.js"
|
||||
import {relays} from "./relays.js"
|
||||
import type {Relay} from "./relays.js"
|
||||
import {handlesByNip05} from "./handles.js"
|
||||
|
||||
export type SearchOptions<V, T> = {
|
||||
getValue: (item: T) => V
|
||||
@@ -65,12 +65,11 @@ export const profileSearch = derived(
|
||||
[throttled(800, profiles), throttled(800, handlesByNip05)],
|
||||
([$profiles, $handlesByNip05]) => {
|
||||
// Remove invalid nip05's from profiles
|
||||
const options = $profiles
|
||||
.map(p => {
|
||||
const isNip05Valid = !p.nip05 || $handlesByNip05.get(p.nip05)?.pubkey === p.event.pubkey
|
||||
const options = $profiles.map(p => {
|
||||
const isNip05Valid = !p.nip05 || $handlesByNip05.get(p.nip05)?.pubkey === p.event.pubkey
|
||||
|
||||
return isNip05Valid ? p : {...p, nip05: ""}
|
||||
})
|
||||
return isNip05Valid ? p : {...p, nip05: ""}
|
||||
})
|
||||
|
||||
return createSearch(options, {
|
||||
onSearch: searchProfiles,
|
||||
@@ -93,7 +92,7 @@ export const profileSearch = derived(
|
||||
shouldSort: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const topicSearch = derived(topics, $topics =>
|
||||
|
||||
+12
-12
@@ -4,18 +4,18 @@ import {withGetter, synced} from "@welshman/store"
|
||||
import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer, Nip55Signer} from "@welshman/signer"
|
||||
|
||||
export type SessionNip01 = {
|
||||
method: 'nip01'
|
||||
method: "nip01"
|
||||
pubkey: string
|
||||
secret: string
|
||||
}
|
||||
|
||||
export type SessionNip07 = {
|
||||
method: 'nip07'
|
||||
method: "nip07"
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
export type SessionNip46 = {
|
||||
method: 'nip46'
|
||||
method: "nip46"
|
||||
pubkey: string
|
||||
secret: string
|
||||
handler: {
|
||||
@@ -25,22 +25,22 @@ export type SessionNip46 = {
|
||||
}
|
||||
|
||||
export type SessionNip55 = {
|
||||
method: 'nip55'
|
||||
method: "nip55"
|
||||
pubkey: string
|
||||
signer: string
|
||||
}
|
||||
|
||||
export type SessionPubkey = {
|
||||
method: 'pubkey'
|
||||
method: "pubkey"
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
export type SessionAnyMethod =
|
||||
SessionNip01 |
|
||||
SessionNip07 |
|
||||
SessionNip46 |
|
||||
SessionNip55 |
|
||||
SessionPubkey
|
||||
| SessionNip01
|
||||
| SessionNip07
|
||||
| SessionNip46
|
||||
| SessionNip55
|
||||
| SessionPubkey
|
||||
|
||||
export type Session = SessionAnyMethod & Record<string, any>
|
||||
|
||||
@@ -88,14 +88,14 @@ export const getSigner = cached({
|
||||
clientSecret: session.secret!,
|
||||
relays: session.handler!.relays,
|
||||
signerPubkey: session.handler!.pubkey,
|
||||
})
|
||||
}),
|
||||
)
|
||||
case "nip55":
|
||||
return new Nip55Signer(session.signer!)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const signer = withGetter(derived(session, getSigner))
|
||||
|
||||
+117
-96
@@ -49,35 +49,37 @@ export const initIndexedDbAdapter = async (name: string, adapter: IndexedDbAdapt
|
||||
|
||||
adapter.store.set(prevRecords)
|
||||
|
||||
adapter.store.subscribe(
|
||||
async (currentRecords: any[]) => {
|
||||
if (dead.get()) {
|
||||
return
|
||||
}
|
||||
adapter.store.subscribe(async (currentRecords: any[]) => {
|
||||
if (dead.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIds = new Set(currentRecords.map(item => item[adapter.keyPath]))
|
||||
const removedRecords = prevRecords.filter(r => !currentIds.has(r[adapter.keyPath]))
|
||||
const currentIds = new Set(currentRecords.map(item => item[adapter.keyPath]))
|
||||
const removedRecords = prevRecords.filter(r => !currentIds.has(r[adapter.keyPath]))
|
||||
|
||||
const prevRecordsById = indexBy(item => item[adapter.keyPath], prevRecords)
|
||||
const updatedRecords = currentRecords.filter(r => r !== prevRecordsById.get(r[adapter.keyPath]))
|
||||
const prevRecordsById = indexBy(item => item[adapter.keyPath], prevRecords)
|
||||
const updatedRecords = currentRecords.filter(r => r !== prevRecordsById.get(r[adapter.keyPath]))
|
||||
|
||||
prevRecords = currentRecords
|
||||
prevRecords = currentRecords
|
||||
|
||||
if (updatedRecords.length > 0) {
|
||||
await bulkPut(name, updatedRecords)
|
||||
}
|
||||
if (updatedRecords.length > 0) {
|
||||
await bulkPut(name, updatedRecords)
|
||||
}
|
||||
|
||||
if (removedRecords.length > 0) {
|
||||
await bulkDelete(
|
||||
name,
|
||||
removedRecords.map(item => item[adapter.keyPath]),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
if (removedRecords.length > 0) {
|
||||
await bulkDelete(
|
||||
name,
|
||||
removedRecords.map(item => item[adapter.keyPath]),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const initStorage = async (name: string, version: number, adapters: Record<string, IndexedDbAdapter>) => {
|
||||
export const initStorage = async (
|
||||
name: string,
|
||||
version: number,
|
||||
adapters: Record<string, IndexedDbAdapter>,
|
||||
) => {
|
||||
if (!window.indexedDB) return
|
||||
|
||||
window.addEventListener("beforeunload", () => closeStorage())
|
||||
@@ -131,14 +133,20 @@ const migrate = (data: any[], options: StorageAdapterOptions) =>
|
||||
options.migrate ? options.migrate(data) : data
|
||||
|
||||
export const storageAdapters = {
|
||||
fromObjectStore: <T>(store: Writable<Record<string, T>>, options: StorageAdapterOptions = {}) => ({
|
||||
fromObjectStore: <T>(
|
||||
store: Writable<Record<string, T>>,
|
||||
options: StorageAdapterOptions = {},
|
||||
) => ({
|
||||
options,
|
||||
keyPath: "key",
|
||||
store: adapter({
|
||||
store: throttled(options.throttle || 0, store),
|
||||
forward: (data: Record<string, T>) =>
|
||||
migrate(Object.entries(data).map(([key, value]) => ({key, value})), options),
|
||||
backward: (data: {key: string, value: T}[]) =>
|
||||
migrate(
|
||||
Object.entries(data).map(([key, value]) => ({key, value})),
|
||||
options,
|
||||
),
|
||||
backward: (data: {key: string; value: T}[]) =>
|
||||
fromPairs(data.map(({key, value}) => [key, value])),
|
||||
}),
|
||||
}),
|
||||
@@ -148,100 +156,113 @@ export const storageAdapters = {
|
||||
store: adapter({
|
||||
store: throttled(options.throttle || 0, store),
|
||||
forward: (data: Map<string, T>) =>
|
||||
migrate(Array.from(data.entries()).map(([key, value]) => ({key, value})), options),
|
||||
backward: (data: {key: string, value: T}[]) =>
|
||||
migrate(
|
||||
Array.from(data.entries()).map(([key, value]) => ({key, value})),
|
||||
options,
|
||||
),
|
||||
backward: (data: {key: string; value: T}[]) =>
|
||||
new Map(data.map(({key, value}) => [key, value])),
|
||||
}),
|
||||
}),
|
||||
fromTracker: (tracker: Tracker, options: StorageAdapterOptions = {}) => ({
|
||||
options,
|
||||
keyPath: 'key',
|
||||
store: custom(setter => {
|
||||
let onUpdate = () =>
|
||||
setter(
|
||||
migrate(
|
||||
Array.from(tracker.relaysById.entries())
|
||||
.map(([key, urls]) => ({key, value: Array.from(urls)})),
|
||||
options
|
||||
keyPath: "key",
|
||||
store: custom(
|
||||
setter => {
|
||||
let onUpdate = () =>
|
||||
setter(
|
||||
migrate(
|
||||
Array.from(tracker.relaysById.entries()).map(([key, urls]) => ({
|
||||
key,
|
||||
value: Array.from(urls),
|
||||
})),
|
||||
options,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if (options.throttle) {
|
||||
onUpdate = throttle(options.throttle, onUpdate)
|
||||
}
|
||||
if (options.throttle) {
|
||||
onUpdate = throttle(options.throttle, onUpdate)
|
||||
}
|
||||
|
||||
onUpdate()
|
||||
tracker.on('update', onUpdate)
|
||||
onUpdate()
|
||||
tracker.on("update", onUpdate)
|
||||
|
||||
return () => tracker.off('update', onUpdate)
|
||||
}, {
|
||||
set: (data: {key: string, value: string[]}[]) =>
|
||||
tracker.load(new Map(data.map(({key, value}) => [key, new Set(value)]))),
|
||||
}),
|
||||
return () => tracker.off("update", onUpdate)
|
||||
},
|
||||
{
|
||||
set: (data: {key: string; value: string[]}[]) =>
|
||||
tracker.load(new Map(data.map(({key, value}) => [key, new Set(value)]))),
|
||||
},
|
||||
),
|
||||
}),
|
||||
fromRepository: (repository: Repository, options: StorageAdapterOptions = {}) => ({
|
||||
options,
|
||||
keyPath: 'id',
|
||||
store: custom(setter => {
|
||||
let onUpdate = () => setter(migrate(repository.dump(), options))
|
||||
keyPath: "id",
|
||||
store: custom(
|
||||
setter => {
|
||||
let onUpdate = () => setter(migrate(repository.dump(), options))
|
||||
|
||||
if (options.throttle) {
|
||||
onUpdate = throttle(options.throttle, onUpdate)
|
||||
}
|
||||
if (options.throttle) {
|
||||
onUpdate = throttle(options.throttle, onUpdate)
|
||||
}
|
||||
|
||||
onUpdate()
|
||||
repository.on('update', onUpdate)
|
||||
onUpdate()
|
||||
repository.on("update", onUpdate)
|
||||
|
||||
return () => repository.off('update', onUpdate)
|
||||
}, {
|
||||
set: (events: TrustedEvent[]) => repository.load(events),
|
||||
}),
|
||||
return () => repository.off("update", onUpdate)
|
||||
},
|
||||
{
|
||||
set: (events: TrustedEvent[]) => repository.load(events),
|
||||
},
|
||||
),
|
||||
}),
|
||||
fromRepositoryAndTracker: (
|
||||
repository: Repository,
|
||||
tracker: Tracker,
|
||||
options: StorageAdapterOptions = {}
|
||||
options: StorageAdapterOptions = {},
|
||||
) => ({
|
||||
options,
|
||||
keyPath: 'id',
|
||||
store: custom(setter => {
|
||||
let onUpdate = () => {
|
||||
const events = migrate(repository.dump(), options)
|
||||
keyPath: "id",
|
||||
store: custom(
|
||||
setter => {
|
||||
let onUpdate = () => {
|
||||
const events = migrate(repository.dump(), options)
|
||||
|
||||
setter(
|
||||
events.map(event => {
|
||||
const relays = Array.from(tracker.getRelays(event.id))
|
||||
setter(
|
||||
events.map(event => {
|
||||
const relays = Array.from(tracker.getRelays(event.id))
|
||||
|
||||
return {id: event.id, event, relays}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (options.throttle) {
|
||||
onUpdate = throttle(options.throttle, onUpdate)
|
||||
}
|
||||
|
||||
onUpdate()
|
||||
tracker.on('update', onUpdate)
|
||||
repository.on('update', onUpdate)
|
||||
|
||||
return () => {
|
||||
tracker.off('update', onUpdate)
|
||||
}
|
||||
}, {
|
||||
set: (items: {event: TrustedEvent, relays: string[]}[]) => {
|
||||
const events: TrustedEvent[] = []
|
||||
const relaysById = new Map<string, Set<string>>()
|
||||
|
||||
for (const {event, relays} of items) {
|
||||
events.push(event)
|
||||
relaysById.set(event.id, new Set(relays))
|
||||
return {id: event.id, event, relays}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
repository.load(events)
|
||||
tracker.load(relaysById)
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
if (options.throttle) {
|
||||
onUpdate = throttle(options.throttle, onUpdate)
|
||||
}
|
||||
|
||||
onUpdate()
|
||||
tracker.on("update", onUpdate)
|
||||
repository.on("update", onUpdate)
|
||||
|
||||
return () => {
|
||||
tracker.off("update", onUpdate)
|
||||
}
|
||||
},
|
||||
{
|
||||
set: (items: {event: TrustedEvent; relays: string[]}[]) => {
|
||||
const events: TrustedEvent[] = []
|
||||
const relaysById = new Map<string, Set<string>>()
|
||||
|
||||
for (const {event, relays} of items) {
|
||||
events.push(event)
|
||||
relaysById.set(event.id, new Set(relays))
|
||||
}
|
||||
|
||||
repository.load(events)
|
||||
tracker.load(relaysById)
|
||||
},
|
||||
},
|
||||
),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {LOCAL_RELAY_URL, getFilterResultCardinality} from "@welshman/util"
|
||||
import type {TrustedEvent, Filter} from "@welshman/util"
|
||||
import {subscribe as baseSubscribe, SubscriptionEvent} from "@welshman/net"
|
||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
||||
import {repository} from './core'
|
||||
import {repository} from "./core.js"
|
||||
|
||||
export type PartialSubscribeRequest = Partial<SubscribeRequestWithHandlers> & {filters: Filter[]}
|
||||
|
||||
|
||||
+25
-25
@@ -1,14 +1,21 @@
|
||||
import type {Filter} from '@welshman/util'
|
||||
import {isSignedEvent} from '@welshman/util'
|
||||
import {push as basePush, pull as basePull, sync as baseSync, pushWithoutNegentropy, pullWithoutNegentropy, syncWithoutNegentropy} from "@welshman/net"
|
||||
import {repository} from './core'
|
||||
import {relaysByUrl} from './relays'
|
||||
import type {Filter} from "@welshman/util"
|
||||
import {isSignedEvent} from "@welshman/util"
|
||||
import {
|
||||
push as basePush,
|
||||
pull as basePull,
|
||||
sync as baseSync,
|
||||
pushWithoutNegentropy,
|
||||
pullWithoutNegentropy,
|
||||
syncWithoutNegentropy,
|
||||
} from "@welshman/net"
|
||||
import {repository} from "./core.js"
|
||||
import {relaysByUrl} from "./relays.js"
|
||||
|
||||
export const hasNegentropy = (url: string) => {
|
||||
const p = relaysByUrl.get().get(url)?.profile
|
||||
|
||||
if (p?.supported_nips?.includes(77)) return true
|
||||
if (p?.software?.includes('strfry') && !p?.version?.match(/^0\./)) return true
|
||||
if (p?.software?.includes("strfry") && !p?.version?.match(/^0\./)) return true
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -23,12 +30,10 @@ export const pull = async ({relays, filters}: AppSyncOpts) => {
|
||||
|
||||
await Promise.all(
|
||||
relays.map(async relay => {
|
||||
await (
|
||||
hasNegentropy(relay)
|
||||
? basePull({filters, events, relays: [relay]})
|
||||
: pullWithoutNegentropy({filters, relays: [relay]})
|
||||
)
|
||||
})
|
||||
await (hasNegentropy(relay)
|
||||
? basePull({filters, events, relays: [relay]})
|
||||
: pullWithoutNegentropy({filters, relays: [relay]}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,12 +42,10 @@ export const push = async ({relays, filters}: AppSyncOpts) => {
|
||||
|
||||
await Promise.all(
|
||||
relays.map(async relay => {
|
||||
await (
|
||||
hasNegentropy(relay)
|
||||
? basePush({filters, events, relays: [relay]})
|
||||
: pushWithoutNegentropy({events, relays: [relay]})
|
||||
)
|
||||
})
|
||||
await (hasNegentropy(relay)
|
||||
? basePush({filters, events, relays: [relay]})
|
||||
: pushWithoutNegentropy({events, relays: [relay]}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,12 +54,9 @@ export const sync = async ({relays, filters}: AppSyncOpts) => {
|
||||
|
||||
await Promise.all(
|
||||
relays.map(async relay => {
|
||||
await (
|
||||
hasNegentropy(relay)
|
||||
? baseSync({filters, events, relays: [relay]})
|
||||
: syncWithoutNegentropy({filters, events, relays: [relay]})
|
||||
)
|
||||
})
|
||||
await (hasNegentropy(relay)
|
||||
? baseSync({filters, events, relays: [relay]})
|
||||
: syncWithoutNegentropy({filters, events, relays: [relay]}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import {ctx} from '@welshman/lib'
|
||||
import {getAddress, isReplaceable, getAncestorTags, getPubkeyTagValues, getIdAndAddress} from '@welshman/util'
|
||||
import type {TrustedEvent} from '@welshman/util'
|
||||
import {displayProfileByPubkey} from './profiles'
|
||||
import {pubkey} from './session'
|
||||
import {ctx} from "@welshman/lib"
|
||||
import {
|
||||
getAddress,
|
||||
isReplaceable,
|
||||
getAncestorTags,
|
||||
getPubkeyTagValues,
|
||||
getIdAndAddress,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {displayProfileByPubkey} from "./profiles.js"
|
||||
import {pubkey} from "./session.js"
|
||||
|
||||
export const tagZapSplit = (pubkey: string, split = 1) => [
|
||||
"zap",
|
||||
@@ -102,6 +108,3 @@ export const tagReactionTo = (event: TrustedEvent) => {
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+28
-16
@@ -1,13 +1,26 @@
|
||||
import {writable, derived, get} from 'svelte/store'
|
||||
import type {Writable, Readable} from 'svelte/store'
|
||||
import {Worker, identity, uniq, defer, sleep, assoc} from '@welshman/lib'
|
||||
import type {Deferred} from '@welshman/lib'
|
||||
import {writable, derived, get} from "svelte/store"
|
||||
import type {Writable, Readable} from "svelte/store"
|
||||
import {Worker, identity, uniq, defer, sleep, assoc} from "@welshman/lib"
|
||||
import type {Deferred} from "@welshman/lib"
|
||||
import {stamp, own, hash} from "@welshman/signer"
|
||||
import type {TrustedEvent, HashedEvent, EventTemplate, SignedEvent, StampedEvent, OwnedEvent} from '@welshman/util'
|
||||
import {isStampedEvent, isOwnedEvent, isHashedEvent, isUnwrappedEvent, isSignedEvent} from '@welshman/util'
|
||||
import type {
|
||||
TrustedEvent,
|
||||
HashedEvent,
|
||||
EventTemplate,
|
||||
SignedEvent,
|
||||
StampedEvent,
|
||||
OwnedEvent,
|
||||
} from "@welshman/util"
|
||||
import {
|
||||
isStampedEvent,
|
||||
isOwnedEvent,
|
||||
isHashedEvent,
|
||||
isUnwrappedEvent,
|
||||
isSignedEvent,
|
||||
} from "@welshman/util"
|
||||
import {publish, PublishStatus} from "@welshman/net"
|
||||
import {repository, tracker} from './core'
|
||||
import {pubkey, getSession, getSigner} from './session'
|
||||
import {repository, tracker} from "./core.js"
|
||||
import {pubkey, getSession, getSigner} from "./session.js"
|
||||
|
||||
const {Pending, Success, Failure, Timeout, Aborted} = PublishStatus
|
||||
|
||||
@@ -53,8 +66,8 @@ export const prepEvent = (event: ThunkEvent) => {
|
||||
export const makeThunk = (request: ThunkRequest) => {
|
||||
const event = prepEvent(request.event)
|
||||
const controller = new AbortController()
|
||||
const result: Thunk['result'] = defer()
|
||||
const status: Thunk['status'] = writable({})
|
||||
const result: Thunk["result"] = defer()
|
||||
const status: Thunk["status"] = writable({})
|
||||
|
||||
return {event, request, controller, result, status}
|
||||
}
|
||||
@@ -72,7 +85,7 @@ export const isMergedThunk = (thunk: Thunk | MergedThunk): thunk is MergedThunk
|
||||
export const mergeThunks = (thunks: Thunk[]) => {
|
||||
const controller = new AbortController()
|
||||
|
||||
controller.signal.addEventListener('abort', () => {
|
||||
controller.signal.addEventListener("abort", () => {
|
||||
for (const thunk of thunks) {
|
||||
thunk.controller.abort()
|
||||
}
|
||||
@@ -99,8 +112,8 @@ export const mergeThunks = (thunks: Thunk[]) => {
|
||||
}
|
||||
|
||||
return mergedStatus
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +138,7 @@ export const publishThunk = (request: ThunkRequest) => {
|
||||
|
||||
thunks.update(assoc(thunk.event.id, thunk))
|
||||
|
||||
thunk.controller.signal.addEventListener('abort', () => {
|
||||
thunk.controller.signal.addEventListener("abort", () => {
|
||||
repository.removeEvent(thunk.event.id)
|
||||
})
|
||||
|
||||
@@ -143,7 +156,7 @@ export const publishThunks = (requests: ThunkRequest[]) => {
|
||||
|
||||
thunks.update(assoc(thunk.event.id, mergedThunk))
|
||||
|
||||
thunk.controller.signal.addEventListener('abort', () => {
|
||||
thunk.controller.signal.addEventListener("abort", () => {
|
||||
repository.removeEvent(thunk.event.id)
|
||||
})
|
||||
}
|
||||
@@ -221,4 +234,3 @@ thunkWorker.addGlobalHandler((thunk: Thunk) => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {inc, throttle} from '@welshman/lib'
|
||||
import {custom} from '@welshman/store'
|
||||
import {repository} from './core'
|
||||
import {inc, throttle} from "@welshman/lib"
|
||||
import {custom} from "@welshman/store"
|
||||
import {repository} from "./core.js"
|
||||
|
||||
export type Topic = {
|
||||
name: string
|
||||
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
import type {NetContext} from '@welshman/net'
|
||||
import type {AppContext} from './context'
|
||||
|
||||
declare module "@welshman/lib" {
|
||||
interface Context {
|
||||
net: NetContext
|
||||
app: AppContext
|
||||
}
|
||||
}
|
||||
+29
-33
@@ -1,43 +1,39 @@
|
||||
import {derived} from 'svelte/store'
|
||||
import {pubkey} from './session'
|
||||
import {profilesByPubkey, loadProfile} from './profiles'
|
||||
import {followsByPubkey, loadFollows} from './follows'
|
||||
import {mutesByPubkey, loadMutes} from './mutes'
|
||||
import {relaySelectionsByPubkey, inboxRelaySelectionsByPubkey, loadRelaySelections, loadInboxRelaySelections} from './relaySelections'
|
||||
import {wotGraph} from './wot'
|
||||
import {derived} from "svelte/store"
|
||||
import {pubkey} from "./session.js"
|
||||
import {profilesByPubkey, loadProfile} from "./profiles.js"
|
||||
import {followsByPubkey, loadFollows} from "./follows.js"
|
||||
import {mutesByPubkey, loadMutes} from "./mutes.js"
|
||||
import {
|
||||
relaySelectionsByPubkey,
|
||||
inboxRelaySelectionsByPubkey,
|
||||
loadRelaySelections,
|
||||
loadInboxRelaySelections,
|
||||
} from "./relaySelections.js"
|
||||
import {wotGraph} from "./wot.js"
|
||||
|
||||
export const userProfile = derived(
|
||||
[profilesByPubkey, pubkey],
|
||||
([$profilesByPubkey, $pubkey]) => {
|
||||
if (!$pubkey) return undefined
|
||||
export const userProfile = derived([profilesByPubkey, pubkey], ([$profilesByPubkey, $pubkey]) => {
|
||||
if (!$pubkey) return undefined
|
||||
|
||||
loadProfile($pubkey)
|
||||
loadProfile($pubkey)
|
||||
|
||||
return $profilesByPubkey.get($pubkey)
|
||||
}
|
||||
)
|
||||
return $profilesByPubkey.get($pubkey)
|
||||
})
|
||||
|
||||
export const userFollows = derived(
|
||||
[followsByPubkey, pubkey],
|
||||
([$followsByPubkey, $pubkey]) => {
|
||||
if (!$pubkey) return undefined
|
||||
export const userFollows = derived([followsByPubkey, pubkey], ([$followsByPubkey, $pubkey]) => {
|
||||
if (!$pubkey) return undefined
|
||||
|
||||
loadFollows($pubkey)
|
||||
loadFollows($pubkey)
|
||||
|
||||
return $followsByPubkey.get($pubkey)
|
||||
}
|
||||
)
|
||||
return $followsByPubkey.get($pubkey)
|
||||
})
|
||||
|
||||
export const userMutes = derived(
|
||||
[mutesByPubkey, pubkey],
|
||||
([$mutesByPubkey, $pubkey]) => {
|
||||
if (!$pubkey) return undefined
|
||||
export const userMutes = derived([mutesByPubkey, pubkey], ([$mutesByPubkey, $pubkey]) => {
|
||||
if (!$pubkey) return undefined
|
||||
|
||||
loadMutes($pubkey)
|
||||
loadMutes($pubkey)
|
||||
|
||||
return $mutesByPubkey.get($pubkey)
|
||||
}
|
||||
)
|
||||
return $mutesByPubkey.get($pubkey)
|
||||
})
|
||||
|
||||
export const userRelaySelections = derived(
|
||||
[relaySelectionsByPubkey, pubkey],
|
||||
@@ -47,7 +43,7 @@ export const userRelaySelections = derived(
|
||||
loadRelaySelections($pubkey)
|
||||
|
||||
return $relaySelectionsByPubkey.get($pubkey)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const userInboxRelaySelections = derived(
|
||||
@@ -58,7 +54,7 @@ export const userInboxRelaySelections = derived(
|
||||
loadInboxRelaySelections($pubkey)
|
||||
|
||||
return $inboxRelaySelectionsByPubkey.get($pubkey)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const getUserWotScore = (tpk: string) => wotGraph.get().get(tpk) || 0
|
||||
|
||||
+24
-32
@@ -1,10 +1,10 @@
|
||||
import {derived, writable} from 'svelte/store'
|
||||
import {max, throttle, addToMapKey, inc, dec} from '@welshman/lib'
|
||||
import {getListTags, getPubkeyTagValues} from '@welshman/util'
|
||||
import {throttled, withGetter} from '@welshman/store'
|
||||
import {pubkey} from './session'
|
||||
import {follows, followsByPubkey} from './follows'
|
||||
import {mutes, mutesByPubkey} from './mutes'
|
||||
import {derived, writable} from "svelte/store"
|
||||
import {max, throttle, addToMapKey, inc, dec} from "@welshman/lib"
|
||||
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||
import {throttled, withGetter} from "@welshman/store"
|
||||
import {pubkey} from "./session.js"
|
||||
import {follows, followsByPubkey} from "./follows.js"
|
||||
import {mutes, mutesByPubkey} from "./mutes.js"
|
||||
|
||||
export const getFollows = (pubkey: string) =>
|
||||
getPubkeyTagValues(getListTags(followsByPubkey.get().get(pubkey)))
|
||||
@@ -27,46 +27,38 @@ export const getNetwork = (pubkey: string) => {
|
||||
return Array.from(network)
|
||||
}
|
||||
|
||||
|
||||
export const followersByPubkey = withGetter(
|
||||
derived(
|
||||
throttled(1000, follows),
|
||||
lists => {
|
||||
const $followersByPubkey = new Map<string, Set<string>>()
|
||||
derived(throttled(1000, follows), lists => {
|
||||
const $followersByPubkey = new Map<string, Set<string>>()
|
||||
|
||||
for (const list of lists) {
|
||||
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
||||
addToMapKey($followersByPubkey, pubkey, list.event.pubkey)
|
||||
}
|
||||
for (const list of lists) {
|
||||
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
||||
addToMapKey($followersByPubkey, pubkey, list.event.pubkey)
|
||||
}
|
||||
|
||||
return $followersByPubkey
|
||||
}
|
||||
)
|
||||
|
||||
return $followersByPubkey
|
||||
}),
|
||||
)
|
||||
|
||||
export const mutersByPubkey = withGetter(
|
||||
derived(
|
||||
throttled(1000, mutes),
|
||||
lists => {
|
||||
const $mutersByPubkey = new Map<string, Set<string>>()
|
||||
derived(throttled(1000, mutes), lists => {
|
||||
const $mutersByPubkey = new Map<string, Set<string>>()
|
||||
|
||||
for (const list of lists) {
|
||||
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
||||
addToMapKey($mutersByPubkey, pubkey, list.event.pubkey)
|
||||
}
|
||||
for (const list of lists) {
|
||||
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
||||
addToMapKey($mutersByPubkey, pubkey, list.event.pubkey)
|
||||
}
|
||||
|
||||
return $mutersByPubkey
|
||||
}
|
||||
)
|
||||
|
||||
return $mutersByPubkey
|
||||
}),
|
||||
)
|
||||
|
||||
export const getFollowers = (pubkey: string) =>
|
||||
Array.from(followersByPubkey.get().get(pubkey) || [])
|
||||
|
||||
export const getMuters = (pubkey: string) =>
|
||||
Array.from(mutersByPubkey.get().get(pubkey) || [])
|
||||
export const getMuters = (pubkey: string) => Array.from(mutersByPubkey.get().get(pubkey) || [])
|
||||
|
||||
export const getFollowsWhoFollow = (pubkey: string, target: string) =>
|
||||
getFollows(pubkey).filter(other => getFollows(other).includes(target))
|
||||
|
||||
+30
-19
@@ -1,9 +1,19 @@
|
||||
import {writable, derived} from 'svelte/store'
|
||||
import {type Zapper} from '@welshman/util'
|
||||
import {writable, derived} from "svelte/store"
|
||||
import {type Zapper} from "@welshman/util"
|
||||
import {type SubscribeRequestWithHandlers} from "@welshman/net"
|
||||
import {ctx, identity, fetchJson, uniq, bech32ToHex, hexToBech32, tryCatch, batcher, postJson} from '@welshman/lib'
|
||||
import {collection} from './collection'
|
||||
import {deriveProfile} from './profiles'
|
||||
import {
|
||||
ctx,
|
||||
identity,
|
||||
fetchJson,
|
||||
uniq,
|
||||
bech32ToHex,
|
||||
hexToBech32,
|
||||
tryCatch,
|
||||
batcher,
|
||||
postJson,
|
||||
} from "@welshman/lib"
|
||||
import {collection} from "./collection.js"
|
||||
import {deriveProfile} from "./profiles.js"
|
||||
|
||||
export const zappers = writable<Zapper[]>([])
|
||||
|
||||
@@ -16,7 +26,9 @@ export const fetchZappers = async (lnurls: string[]) => {
|
||||
const hexUrls = lnurls.map(lnurl => tryCatch(() => bech32ToHex(lnurl))).filter(identity)
|
||||
|
||||
if (hexUrls.length > 0) {
|
||||
const res: any = await tryCatch(async () => await postJson(`${base}/zapper/info`, {lnurls: hexUrls}))
|
||||
const res: any = await tryCatch(
|
||||
async () => await postJson(`${base}/zapper/info`, {lnurls: hexUrls}),
|
||||
)
|
||||
|
||||
for (const {lnurl, info} of res?.data || []) {
|
||||
tryCatch(() => zappersByLnurl.set(hexToBech32("lnurl", lnurl), info))
|
||||
@@ -29,7 +41,7 @@ export const fetchZappers = async (lnurls: string[]) => {
|
||||
const info = hexUrl ? await tryCatch(async () => await fetchJson(hexUrl)) : undefined
|
||||
|
||||
return {lnurl, hexUrl, info}
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
for (const {lnurl, info} of results) {
|
||||
@@ -68,17 +80,16 @@ export const {
|
||||
}),
|
||||
})
|
||||
|
||||
export const deriveZapperForPubkey = (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) =>
|
||||
derived(
|
||||
[zappersByLnurl, deriveProfile(pubkey, request)],
|
||||
([$zappersByLnurl, $profile]) => {
|
||||
if (!$profile?.lnurl) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
loadZapper($profile.lnurl)
|
||||
|
||||
return $zappersByLnurl.get($profile.lnurl)
|
||||
export const deriveZapperForPubkey = (
|
||||
pubkey: string,
|
||||
request: Partial<SubscribeRequestWithHandlers> = {},
|
||||
) =>
|
||||
derived([zappersByLnurl, deriveProfile(pubkey, request)], ([$zappersByLnurl, $profile]) => {
|
||||
if (!$profile?.lnurl) {
|
||||
return undefined
|
||||
}
|
||||
)
|
||||
|
||||
loadZapper($profile.lnurl)
|
||||
|
||||
return $zappersByLnurl.get($profile.lnurl)
|
||||
})
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"targets": [
|
||||
{"extname": ".cjs", "module": "commonjs"},
|
||||
{"extname": ".mjs", "module": "esnext", "moduleResolution": "node"}
|
||||
],
|
||||
"projects": ["tsconfig.json"]
|
||||
}
|
||||
@@ -3,9 +3,12 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "build",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["esnext", "dom", "dom.iterable"]
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"lib": ["esnext", "dom"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,25 +11,23 @@
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"types": "./build/src/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/src/index.d.ts",
|
||||
"import": "./build/src/index.mjs",
|
||||
"require": "./build/src/index.cjs"
|
||||
"import": "./build/src/index.js",
|
||||
"require": "./build/src/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"pub": "npm run lint && npm run build && npm publish",
|
||||
"build": "gts clean && tsc-multi",
|
||||
"build": "gts clean && tsc",
|
||||
"lint": "gts lint",
|
||||
"fix": "gts fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gts": "^5.0.1",
|
||||
"tsc-multi": "^1.1.0",
|
||||
"typescript": "~5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.2",
|
||||
"@welshman/lib": "~0.0.33",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {nip19} from "nostr-tools"
|
||||
import {sanitizeUrl} from '@braintree/sanitize-url'
|
||||
import {decode, neventEncode, nprofileEncode, naddrEncode} from "nostr-tools/nip19"
|
||||
import {sanitizeUrl} from "@braintree/sanitize-url"
|
||||
|
||||
const last = <T>(xs: T[], ...args: unknown[]) => xs[xs.length - 1]
|
||||
|
||||
@@ -33,7 +33,7 @@ type ProfilePointer = {
|
||||
|
||||
export type ParseContext = {
|
||||
results: Parsed[]
|
||||
content: string,
|
||||
content: string
|
||||
tags: string[][]
|
||||
}
|
||||
|
||||
@@ -124,31 +124,36 @@ export type ParsedAddress = {
|
||||
}
|
||||
|
||||
export type Parsed =
|
||||
ParsedAddress |
|
||||
ParsedCashu |
|
||||
ParsedCode |
|
||||
ParsedEllipsis |
|
||||
ParsedEvent |
|
||||
ParsedInvoice |
|
||||
ParsedLink |
|
||||
ParsedNewline |
|
||||
ParsedProfile |
|
||||
ParsedText |
|
||||
ParsedTopic
|
||||
| ParsedAddress
|
||||
| ParsedCashu
|
||||
| ParsedCode
|
||||
| ParsedEllipsis
|
||||
| ParsedEvent
|
||||
| ParsedInvoice
|
||||
| ParsedLink
|
||||
| ParsedNewline
|
||||
| ParsedProfile
|
||||
| ParsedText
|
||||
| ParsedTopic
|
||||
|
||||
// Matchers
|
||||
|
||||
export const isAddress = (parsed: Parsed): parsed is ParsedAddress => parsed.type === ParsedType.Address
|
||||
export const isCashu = (parsed: Parsed): parsed is ParsedCashu => parsed.type === ParsedType.Cashu
|
||||
export const isCode = (parsed: Parsed): parsed is ParsedCode => parsed.type === ParsedType.Code
|
||||
export const isEllipsis = (parsed: Parsed): parsed is ParsedEllipsis => parsed.type === ParsedType.Ellipsis
|
||||
export const isEvent = (parsed: Parsed): parsed is ParsedEvent => parsed.type === ParsedType.Event
|
||||
export const isInvoice = (parsed: Parsed): parsed is ParsedInvoice => parsed.type === ParsedType.Invoice
|
||||
export const isLink = (parsed: Parsed): parsed is ParsedLink => parsed.type === ParsedType.Link
|
||||
export const isNewline = (parsed: Parsed): parsed is ParsedNewline => parsed.type === ParsedType.Newline
|
||||
export const isProfile = (parsed: Parsed): parsed is ParsedProfile => parsed.type === ParsedType.Profile
|
||||
export const isText = (parsed: Parsed): parsed is ParsedText => parsed.type === ParsedType.Text
|
||||
export const isTopic = (parsed: Parsed): parsed is ParsedTopic => parsed.type === ParsedType.Topic
|
||||
export const isAddress = (parsed: Parsed): parsed is ParsedAddress =>
|
||||
parsed.type === ParsedType.Address
|
||||
export const isCashu = (parsed: Parsed): parsed is ParsedCashu => parsed.type === ParsedType.Cashu
|
||||
export const isCode = (parsed: Parsed): parsed is ParsedCode => parsed.type === ParsedType.Code
|
||||
export const isEllipsis = (parsed: Parsed): parsed is ParsedEllipsis =>
|
||||
parsed.type === ParsedType.Ellipsis
|
||||
export const isEvent = (parsed: Parsed): parsed is ParsedEvent => parsed.type === ParsedType.Event
|
||||
export const isInvoice = (parsed: Parsed): parsed is ParsedInvoice =>
|
||||
parsed.type === ParsedType.Invoice
|
||||
export const isLink = (parsed: Parsed): parsed is ParsedLink => parsed.type === ParsedType.Link
|
||||
export const isNewline = (parsed: Parsed): parsed is ParsedNewline =>
|
||||
parsed.type === ParsedType.Newline
|
||||
export const isProfile = (parsed: Parsed): parsed is ParsedProfile =>
|
||||
parsed.type === ParsedType.Profile
|
||||
export const isText = (parsed: Parsed): parsed is ParsedText => parsed.type === ParsedType.Text
|
||||
export const isTopic = (parsed: Parsed): parsed is ParsedTopic => parsed.type === ParsedType.Topic
|
||||
|
||||
// Parsers for known formats
|
||||
|
||||
@@ -157,7 +162,7 @@ export const parseAddress = (text: string, context: ParseContext): ParsedAddress
|
||||
|
||||
if (naddr) {
|
||||
try {
|
||||
const {data} = nip19.decode(fromNostrURI(naddr))
|
||||
const {data} = decode(fromNostrURI(naddr))
|
||||
|
||||
return {type: ParsedType.Address, value: data as AddressPointer, raw: naddr}
|
||||
} catch (e) {
|
||||
@@ -195,10 +200,8 @@ export const parseEvent = (text: string, context: ParseContext): ParsedEvent | v
|
||||
|
||||
if (entity) {
|
||||
try {
|
||||
const {type, data} = nip19.decode(fromNostrURI(entity))
|
||||
const value = type === "note"
|
||||
? {id: data as string, relays: []}
|
||||
: data as EventPointer
|
||||
const {type, data} = decode(fromNostrURI(entity))
|
||||
const value = type === "note" ? {id: data as string, relays: []} : (data as EventPointer)
|
||||
|
||||
return {type: ParsedType.Event, value, raw: entity}
|
||||
} catch (e) {
|
||||
@@ -217,15 +220,16 @@ export const parseInvoice = (text: string, context: ParseContext): ParsedInvoice
|
||||
|
||||
export const parseLink = (text: string, context: ParseContext): ParsedLink | void => {
|
||||
const prev = last(context.results)
|
||||
const [link] = text.match(/^([a-z\+:]{2,30}:\/\/)?[-\.~\w]+\.[\w]{2,6}([^\s]*[^<>"'\.!,:\s\)\(]+)?/gi) || []
|
||||
const [link] =
|
||||
text.match(/^([a-z\+:]{2,30}:\/\/)?[-\.~\w]+\.[\w]{2,6}([^\s]*[^<>"'\.!,:\s\)\(]+)?/gi) || []
|
||||
|
||||
// Skip url if it's just the end of a filepath or an ellipse
|
||||
if (!link || prev?.type === ParsedType.Text && prev.value.endsWith("/") || link.match(/\.\./)) {
|
||||
if (!link || (prev?.type === ParsedType.Text && prev.value.endsWith("/")) || link.match(/\.\./)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip it if it looks like an IP address but doesn't have a protocol
|
||||
if (link.match(/\d+\.\d+/) && !link.includes('://')) {
|
||||
if (link.match(/\d+\.\d+/) && !link.includes("://")) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -240,7 +244,7 @@ export const parseLink = (text: string, context: ParseContext): ParsedLink | voi
|
||||
const meta = Object.fromEntries(new URLSearchParams(url.hash.slice(1)).entries())
|
||||
|
||||
for (const tag of context.tags) {
|
||||
if (tag[0] === 'imeta' && tag.find(t => t.includes(`url ${link}`))) {
|
||||
if (tag[0] === "imeta" && tag.find(t => t.includes(`url ${link}`))) {
|
||||
Object.assign(meta, Object.fromEntries(tag.slice(1).map((m: string) => m.split(" "))))
|
||||
}
|
||||
}
|
||||
@@ -261,10 +265,9 @@ export const parseProfile = (text: string, context: ParseContext): ParsedProfile
|
||||
|
||||
if (entity) {
|
||||
try {
|
||||
const {type, data} = nip19.decode(fromNostrURI(entity.replace('@', '')))
|
||||
const value = type === "npub"
|
||||
? {pubkey: data as string, relays: []}
|
||||
: data as ProfilePointer
|
||||
const {type, data} = decode(fromNostrURI(entity.replace("@", "")))
|
||||
const value =
|
||||
type === "npub" ? {pubkey: data as string, relays: []} : (data as ProfilePointer)
|
||||
|
||||
return {type: ParsedType.Profile, value, raw: entity}
|
||||
} catch (e) {
|
||||
@@ -282,10 +285,12 @@ export const parseTopic = (text: string, context: ParseContext): ParsedTopic | v
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Parse other formats to known types
|
||||
|
||||
export const parseLegacyMention = (text: string, context: ParseContext): ParsedProfile | ParsedEvent | void => {
|
||||
export const parseLegacyMention = (
|
||||
text: string,
|
||||
context: ParseContext,
|
||||
): ParsedProfile | ParsedEvent | void => {
|
||||
const mentionMatch = text.match(/^#\[(\d+)\]/i) || []
|
||||
|
||||
if (mentionMatch) {
|
||||
@@ -313,7 +318,7 @@ export const parsers = [
|
||||
parseEvent,
|
||||
parseCashu,
|
||||
parseInvoice,
|
||||
parseLink
|
||||
parseLink,
|
||||
]
|
||||
|
||||
export const parseNext = (raw: string, context: ParseContext): Parsed | void => {
|
||||
@@ -371,12 +376,7 @@ type TruncateOpts = {
|
||||
|
||||
export const truncate = (
|
||||
content: Parsed[],
|
||||
{
|
||||
minLength = 500,
|
||||
maxLength = 700,
|
||||
mediaLength = 200,
|
||||
entityLength = 30,
|
||||
}: TruncateOpts = {},
|
||||
{minLength = 500, maxLength = 700, mediaLength = 200, entityLength = 30}: TruncateOpts = {},
|
||||
) => {
|
||||
// Get a list of content sizes so we know where to truncate
|
||||
// Non-plaintext things might take up more or less room if rendered
|
||||
@@ -429,7 +429,7 @@ export class Renderer {
|
||||
toString = () => this.value
|
||||
|
||||
addText = (value: string) => {
|
||||
const element = document.createElement('div')
|
||||
const element = document.createElement("div")
|
||||
|
||||
element.innerText = value
|
||||
|
||||
@@ -459,17 +459,17 @@ export type RenderOptions = {
|
||||
}
|
||||
|
||||
export const textRenderOptions = {
|
||||
newline: '\n',
|
||||
entityBase: '',
|
||||
newline: "\n",
|
||||
entityBase: "",
|
||||
renderLink: (href: string, display: string) => href,
|
||||
renderEntity: (entity: string) => entity.slice(0, 16) + '…',
|
||||
renderEntity: (entity: string) => entity.slice(0, 16) + "…",
|
||||
}
|
||||
|
||||
export const htmlRenderOptions = {
|
||||
newline: '\n',
|
||||
entityBase: 'https://njump.me/',
|
||||
newline: "\n",
|
||||
entityBase: "https://njump.me/",
|
||||
renderLink: (href: string, display: string) => {
|
||||
const element = document.createElement('a')
|
||||
const element = document.createElement("a")
|
||||
|
||||
element.href = sanitizeUrl(href)
|
||||
element.target = "_blank"
|
||||
@@ -477,7 +477,7 @@ export const htmlRenderOptions = {
|
||||
|
||||
return element.outerHTML
|
||||
},
|
||||
renderEntity: (entity: string) => entity.slice(0, 16) + '…',
|
||||
renderEntity: (entity: string) => entity.slice(0, 16) + "…",
|
||||
}
|
||||
|
||||
export const makeTextRenderer = (options: Partial<RenderOptions> = {}) =>
|
||||
@@ -499,31 +499,56 @@ export const renderInvoice = (p: ParsedInvoice, r: Renderer) => r.addText(p.valu
|
||||
export const renderLink = (p: ParsedLink, r: Renderer) =>
|
||||
r.addLink(p.value.url.toString(), p.value.url.host + p.value.url.pathname)
|
||||
|
||||
export const renderNewline = (p: ParsedNewline, r: Renderer) => r.addNewlines(Array.from(p.value).length)
|
||||
export const renderNewline = (p: ParsedNewline, r: Renderer) =>
|
||||
r.addNewlines(Array.from(p.value).length)
|
||||
|
||||
export const renderText = (p: ParsedText, r: Renderer) => r.addText(p.value)
|
||||
|
||||
export const renderTopic = (p: ParsedTopic, r: Renderer) => r.addText(p.value)
|
||||
|
||||
export const renderEvent = (p: ParsedEvent, r: Renderer) => r.addEntityLink(nip19.neventEncode(p.value))
|
||||
export const renderEvent = (p: ParsedEvent, r: Renderer) => r.addEntityLink(neventEncode(p.value))
|
||||
|
||||
export const renderProfile = (p: ParsedProfile, r: Renderer) => r.addEntityLink(nip19.nprofileEncode(p.value))
|
||||
export const renderProfile = (p: ParsedProfile, r: Renderer) =>
|
||||
r.addEntityLink(nprofileEncode(p.value))
|
||||
|
||||
export const renderAddress = (p: ParsedAddress, r: Renderer) => r.addEntityLink(nip19.naddrEncode(p.value))
|
||||
export const renderAddress = (p: ParsedAddress, r: Renderer) =>
|
||||
r.addEntityLink(naddrEncode(p.value))
|
||||
|
||||
export const renderOne = (parsed: Parsed, renderer: Renderer) => {
|
||||
switch (parsed.type) {
|
||||
case ParsedType.Address: renderAddress(parsed as ParsedAddress, renderer); break
|
||||
case ParsedType.Cashu: renderCashu(parsed as ParsedCashu, renderer); break
|
||||
case ParsedType.Code: renderCode(parsed as ParsedCode, renderer); break
|
||||
case ParsedType.Ellipsis: renderEllipsis(parsed as ParsedEllipsis, renderer); break
|
||||
case ParsedType.Event: renderEvent(parsed as ParsedEvent, renderer); break
|
||||
case ParsedType.Invoice: renderInvoice(parsed as ParsedInvoice, renderer); break
|
||||
case ParsedType.Link: renderLink(parsed as ParsedLink, renderer); break
|
||||
case ParsedType.Newline: renderNewline(parsed as ParsedNewline, renderer); break
|
||||
case ParsedType.Profile: renderProfile(parsed as ParsedProfile, renderer); break
|
||||
case ParsedType.Text: renderText(parsed as ParsedText, renderer); break
|
||||
case ParsedType.Topic: renderTopic(parsed as ParsedTopic, renderer); break
|
||||
case ParsedType.Address:
|
||||
renderAddress(parsed as ParsedAddress, renderer)
|
||||
break
|
||||
case ParsedType.Cashu:
|
||||
renderCashu(parsed as ParsedCashu, renderer)
|
||||
break
|
||||
case ParsedType.Code:
|
||||
renderCode(parsed as ParsedCode, renderer)
|
||||
break
|
||||
case ParsedType.Ellipsis:
|
||||
renderEllipsis(parsed as ParsedEllipsis, renderer)
|
||||
break
|
||||
case ParsedType.Event:
|
||||
renderEvent(parsed as ParsedEvent, renderer)
|
||||
break
|
||||
case ParsedType.Invoice:
|
||||
renderInvoice(parsed as ParsedInvoice, renderer)
|
||||
break
|
||||
case ParsedType.Link:
|
||||
renderLink(parsed as ParsedLink, renderer)
|
||||
break
|
||||
case ParsedType.Newline:
|
||||
renderNewline(parsed as ParsedNewline, renderer)
|
||||
break
|
||||
case ParsedType.Profile:
|
||||
renderProfile(parsed as ParsedProfile, renderer)
|
||||
break
|
||||
case ParsedType.Text:
|
||||
renderText(parsed as ParsedText, renderer)
|
||||
break
|
||||
case ParsedType.Topic:
|
||||
renderTopic(parsed as ParsedTopic, renderer)
|
||||
break
|
||||
}
|
||||
|
||||
return renderer
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"targets": [
|
||||
{"extname": ".cjs", "module": "commonjs"},
|
||||
{"extname": ".mjs", "module": "esnext", "moduleResolution": "node"}
|
||||
],
|
||||
"projects": ["tsconfig.json"]
|
||||
}
|
||||
@@ -3,9 +3,12 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "build",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["esnext", "dom", "dom.iterable"]
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"lib": ["esnext", "dom"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ req.emitter.on(DVMEvent.Result, (url, event) => console.log(event))
|
||||
# Handler example
|
||||
|
||||
```javascript
|
||||
const {bytesToHex} = require('@noble/hashes/utils')
|
||||
const {generateSecretKey} = require('nostr-tools')
|
||||
const {createEvent} = require('@welshman/util')
|
||||
const {subscribe} = require('@welshman/net')
|
||||
const {DVM} = require('@welshman/dvm')
|
||||
import {bytesToHex} from '@noble/hashes/utils'
|
||||
import {generateSecretKey} from 'nostr-tools'
|
||||
import {createEvent} from '@welshman/util'
|
||||
import {subscribe} from '@welshman/net'
|
||||
import {DVM} from '@welshman/dvm'
|
||||
|
||||
// Your DVM's private key. Store this somewhere safe
|
||||
// const hexPrivateKey = bytesToHex(generateSecretKey())
|
||||
|
||||
@@ -15,22 +15,18 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/src/index.d.ts",
|
||||
"import": "./build/src/index.mjs",
|
||||
"require": "./build/src/index.cjs"
|
||||
"import": "./build/src/index.js",
|
||||
"require": "./build/src/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"pub": "npm run lint && npm run build && npm publish",
|
||||
"build": "gts clean && tsc-multi",
|
||||
"build": "gts clean && tsc",
|
||||
"lint": "gts lint",
|
||||
"fix": "gts fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gts": "^5.0.1",
|
||||
"tsc-multi": "^1.1.0",
|
||||
"typescript": "~5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.6.1",
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@welshman/net": "~0.0.41",
|
||||
"@welshman/util": "~0.0.50",
|
||||
|
||||
+16
-16
@@ -1,8 +1,8 @@
|
||||
import {hexToBytes} from '@noble/hashes/utils'
|
||||
import {getPublicKey, finalizeEvent} from 'nostr-tools'
|
||||
import {now} from '@welshman/lib'
|
||||
import type {TrustedEvent, StampedEvent, Filter} from '@welshman/util'
|
||||
import {subscribe, publish} from '@welshman/net'
|
||||
import {hexToBytes} from "@noble/hashes/utils"
|
||||
import {getPublicKey, finalizeEvent} from "nostr-tools/pure"
|
||||
import {now} from "@welshman/lib"
|
||||
import type {TrustedEvent, StampedEvent, Filter} from "@welshman/util"
|
||||
import {subscribe, publish} from "@welshman/net"
|
||||
|
||||
export type DVMHandler = {
|
||||
stop?: () => void
|
||||
@@ -43,14 +43,14 @@ export class DVM {
|
||||
const filter: Filter = {kinds, since}
|
||||
|
||||
if (requireMention) {
|
||||
filter['#p'] = [getPublicKey(hexToBytes(sk))]
|
||||
filter["#p"] = [getPublicKey(hexToBytes(sk))]
|
||||
}
|
||||
|
||||
const filters = [filter]
|
||||
const sub = subscribe({relays, filters})
|
||||
|
||||
sub.emitter.on('event', (url: string, e: TrustedEvent) => this.onEvent(e))
|
||||
sub.emitter.on('complete', () => resolve())
|
||||
sub.emitter.on("event", (url: string, e: TrustedEvent) => this.onEvent(e))
|
||||
sub.emitter.on("complete", () => resolve())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -79,29 +79,29 @@ export class DVM {
|
||||
this.seen.add(request.id)
|
||||
|
||||
if (this.logEvents) {
|
||||
console.info('Handling request', request)
|
||||
console.info("Handling request", request)
|
||||
}
|
||||
|
||||
for await (const event of handler.handleEvent(request)) {
|
||||
if (event.kind !== 7000) {
|
||||
event.tags.push(['request', JSON.stringify(request)])
|
||||
event.tags.push(["request", JSON.stringify(request)])
|
||||
|
||||
const inputTag = request.tags.find((t: string[]) => t[0] === 'i')
|
||||
const inputTag = request.tags.find((t: string[]) => t[0] === "i")
|
||||
|
||||
if (inputTag) {
|
||||
event.tags.push(inputTag)
|
||||
}
|
||||
}
|
||||
|
||||
event.tags.push(['p', request.pubkey])
|
||||
event.tags.push(['e', request.id])
|
||||
event.tags.push(["p", request.pubkey])
|
||||
event.tags.push(["e", request.id])
|
||||
|
||||
if (expireAfter) {
|
||||
event.tags.push(['expiration', String(now() + expireAfter)])
|
||||
event.tags.push(["expiration", String(now() + expireAfter)])
|
||||
}
|
||||
|
||||
if (this.logEvents) {
|
||||
console.info('Publishing event', event)
|
||||
console.info("Publishing event", event)
|
||||
}
|
||||
|
||||
this.publish(event)
|
||||
@@ -113,7 +113,7 @@ export class DVM {
|
||||
const event = finalizeEvent(template, hexToBytes(sk))
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
publish({event, relays}).emitter.on('success', () => resolve())
|
||||
publish({event, relays}).emitter.on("success", () => resolve())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './handler'
|
||||
export * from './request'
|
||||
export * from "./handler.js"
|
||||
export * from "./request.js"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Emitter, now} from '@welshman/lib'
|
||||
import type {TrustedEvent, SignedEvent, Filter} from '@welshman/util'
|
||||
import {subscribe, publish, SubscriptionEvent} from '@welshman/net'
|
||||
import type {Subscription, Publish} from '@welshman/net'
|
||||
import {Emitter, now} from "@welshman/lib"
|
||||
import type {TrustedEvent, SignedEvent, Filter} from "@welshman/util"
|
||||
import {subscribe, publish, SubscriptionEvent} from "@welshman/net"
|
||||
import type {Subscription, Publish} from "@welshman/net"
|
||||
|
||||
export enum DVMEvent {
|
||||
Progress = "progress",
|
||||
@@ -18,7 +18,7 @@ export type DVMRequestOptions = {
|
||||
|
||||
export type DVMRequest = {
|
||||
request: DVMRequestOptions
|
||||
emitter: Emitter,
|
||||
emitter: Emitter
|
||||
sub: Subscription
|
||||
pub: Publish
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"targets": [
|
||||
{"extname": ".cjs", "module": "commonjs"},
|
||||
{"extname": ".mjs", "module": "esnext", "moduleResolution": "node"}
|
||||
],
|
||||
"projects": ["tsconfig.json"]
|
||||
}
|
||||
@@ -3,9 +3,12 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "build",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["esnext", "dom", "dom.iterable"]
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"lib": ["esnext", "dom"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,21 +15,16 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/src/index.d.ts",
|
||||
"import": "./build/src/index.mjs",
|
||||
"require": "./build/src/index.cjs"
|
||||
"import": "./build/src/index.js",
|
||||
"require": "./build/src/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"pub": "npm run lint && npm run build && npm publish",
|
||||
"build": "gts clean && tsc-multi",
|
||||
"build": "gts clean && tsc",
|
||||
"lint": "gts lint",
|
||||
"fix": "gts fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gts": "^5.0.1",
|
||||
"tsc-multi": "^1.1.0",
|
||||
"typescript": "~5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@welshman/util": "~0.0.50"
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import {uniq, identity, flatten, pushToMapKey, intersection, tryCatch, now} from '@welshman/lib'
|
||||
import type {TrustedEvent, Filter} from '@welshman/util'
|
||||
import {intersectFilters, matchFilter, getAddress, getIdFilters, unionFilters} from '@welshman/util'
|
||||
import type {CreatedAtItem, RequestItem, ListItem, LabelItem, WOTItem, DVMItem, Scope, Feed, FeedOptions} from './core'
|
||||
import {getFeedArgs, feedsFromTags} from './utils'
|
||||
import {FeedType} from './core'
|
||||
import {uniq, identity, flatten, pushToMapKey, intersection, tryCatch, now} from "@welshman/lib"
|
||||
import type {TrustedEvent, Filter} from "@welshman/util"
|
||||
import {intersectFilters, matchFilter, getAddress, getIdFilters, unionFilters} from "@welshman/util"
|
||||
import type {
|
||||
CreatedAtItem,
|
||||
RequestItem,
|
||||
ListItem,
|
||||
LabelItem,
|
||||
WOTItem,
|
||||
DVMItem,
|
||||
Scope,
|
||||
Feed,
|
||||
FeedOptions,
|
||||
} from "./core.js"
|
||||
import {getFeedArgs, feedsFromTags} from "./utils.js"
|
||||
import {FeedType} from "./core.js"
|
||||
|
||||
export class FeedCompiler {
|
||||
constructor(readonly options: FeedOptions) {}
|
||||
|
||||
canCompile(feed: Feed): boolean {
|
||||
switch(feed[0]) {
|
||||
switch (feed[0]) {
|
||||
case FeedType.Union:
|
||||
case FeedType.Intersection:
|
||||
return getFeedArgs(feed).every(f => this.canCompile(f))
|
||||
@@ -34,22 +44,37 @@ export class FeedCompiler {
|
||||
}
|
||||
|
||||
async compile(feed: Feed): Promise<RequestItem[]> {
|
||||
switch(feed[0]) {
|
||||
case FeedType.ID: return this._compileFilter('ids', getFeedArgs(feed))
|
||||
case FeedType.Kind: return this._compileFilter('kinds', getFeedArgs(feed))
|
||||
case FeedType.Author: return this._compileFilter('authors', getFeedArgs(feed))
|
||||
case FeedType.DVM: return await this._compileDvms(getFeedArgs(feed))
|
||||
case FeedType.Intersection: return await this._compileIntersection(getFeedArgs(feed))
|
||||
case FeedType.List: return await this._compileLists(getFeedArgs(feed))
|
||||
case FeedType.Label: return await this._compileLabels(getFeedArgs(feed))
|
||||
case FeedType.Union: return await this._compileUnion(getFeedArgs(feed))
|
||||
case FeedType.Address: return this._compileAddresses(getFeedArgs(feed))
|
||||
case FeedType.CreatedAt: return this._compileCreatedAt(getFeedArgs(feed))
|
||||
case FeedType.Scope: return this._compileScopes(getFeedArgs(feed))
|
||||
case FeedType.Search: return this._compileSearches(getFeedArgs(feed))
|
||||
case FeedType.WOT: return this._compileWot(getFeedArgs(feed))
|
||||
case FeedType.Relay: return [{relays: getFeedArgs(feed)}]
|
||||
case FeedType.Global: return [{filters: [{}]}]
|
||||
switch (feed[0]) {
|
||||
case FeedType.ID:
|
||||
return this._compileFilter("ids", getFeedArgs(feed))
|
||||
case FeedType.Kind:
|
||||
return this._compileFilter("kinds", getFeedArgs(feed))
|
||||
case FeedType.Author:
|
||||
return this._compileFilter("authors", getFeedArgs(feed))
|
||||
case FeedType.DVM:
|
||||
return await this._compileDvms(getFeedArgs(feed))
|
||||
case FeedType.Intersection:
|
||||
return await this._compileIntersection(getFeedArgs(feed))
|
||||
case FeedType.List:
|
||||
return await this._compileLists(getFeedArgs(feed))
|
||||
case FeedType.Label:
|
||||
return await this._compileLabels(getFeedArgs(feed))
|
||||
case FeedType.Union:
|
||||
return await this._compileUnion(getFeedArgs(feed))
|
||||
case FeedType.Address:
|
||||
return this._compileAddresses(getFeedArgs(feed))
|
||||
case FeedType.CreatedAt:
|
||||
return this._compileCreatedAt(getFeedArgs(feed))
|
||||
case FeedType.Scope:
|
||||
return this._compileScopes(getFeedArgs(feed))
|
||||
case FeedType.Search:
|
||||
return this._compileSearches(getFeedArgs(feed))
|
||||
case FeedType.WOT:
|
||||
return this._compileWot(getFeedArgs(feed))
|
||||
case FeedType.Relay:
|
||||
return [{relays: getFeedArgs(feed)}]
|
||||
case FeedType.Global:
|
||||
return [{filters: [{}]}]
|
||||
case FeedType.Tag: {
|
||||
const [key, ...value] = getFeedArgs(feed)
|
||||
|
||||
@@ -99,7 +124,13 @@ export class FeedCompiler {
|
||||
}
|
||||
|
||||
_compileWot(wotItems: WOTItem[]) {
|
||||
return [{filters: wotItems.map(({min = 0, max = 1}) => ({authors: this.options.getPubkeysForWOTRange(min, max)}))}]
|
||||
return [
|
||||
{
|
||||
filters: wotItems.map(({min = 0, max = 1}) => ({
|
||||
authors: this.options.getPubkeysForWOTRange(min, max),
|
||||
})),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async _compileDvms(items: DVMItem[]): Promise<RequestItem[]> {
|
||||
@@ -110,14 +141,14 @@ export class FeedCompiler {
|
||||
this.options.requestDVM({
|
||||
...request,
|
||||
onEvent: async (e: TrustedEvent) => {
|
||||
const tags = await tryCatch(() => JSON.parse(e.content)) || []
|
||||
const tags = (await tryCatch(() => JSON.parse(e.content))) || []
|
||||
|
||||
for (const feed of feedsFromTags(tags, mappings)) {
|
||||
feeds.push(feed)
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return await this._compileUnion(feeds)
|
||||
@@ -129,16 +160,15 @@ export class FeedCompiler {
|
||||
const result = []
|
||||
|
||||
for (let {filters, relays} of head || []) {
|
||||
const matchingGroups = tail.map(
|
||||
items => items.filter(
|
||||
it => (
|
||||
(!relays || !it.relays || intersection(relays, it.relays).length > 0) &&
|
||||
(!filters || !it.filters || intersectFilters([filters, it.filters]).length > 0)
|
||||
)
|
||||
const matchingGroups = tail
|
||||
.map(items =>
|
||||
items.filter(
|
||||
it =>
|
||||
(!relays || !it.relays || intersection(relays, it.relays).length > 0) &&
|
||||
(!filters || !it.filters || intersectFilters([filters, it.filters]).length > 0),
|
||||
),
|
||||
)
|
||||
).filter(
|
||||
items => items.length > 0
|
||||
)
|
||||
.filter(items => items.length > 0)
|
||||
|
||||
if (matchingGroups.length < tail.length) {
|
||||
continue
|
||||
@@ -190,7 +220,7 @@ export class FeedCompiler {
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
const items: RequestItem[] = []
|
||||
@@ -238,8 +268,8 @@ export class FeedCompiler {
|
||||
}
|
||||
|
||||
return feeds
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return this._compileUnion(feeds)
|
||||
@@ -254,8 +284,8 @@ export class FeedCompiler {
|
||||
relays,
|
||||
filters: [{kinds: [1985], ...filter}],
|
||||
onEvent: (e: TrustedEvent) => events.push(e),
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const feeds = flatten(
|
||||
@@ -272,8 +302,8 @@ export class FeedCompiler {
|
||||
}
|
||||
|
||||
return feedsFromTags(tags, mappings)
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return this._compileUnion(feeds)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {inc, memoize, omitVals, max, min, now} from '@welshman/lib'
|
||||
import type {TrustedEvent, Filter} from '@welshman/util'
|
||||
import {EPOCH, trimFilters, guessFilterDelta} from '@welshman/util'
|
||||
import type {Feed, RequestItem, FeedOptions} from './core'
|
||||
import {FeedType} from './core'
|
||||
import {FeedCompiler} from './compiler'
|
||||
import {inc, memoize, omitVals, max, min, now} from "@welshman/lib"
|
||||
import type {TrustedEvent, Filter} from "@welshman/util"
|
||||
import {EPOCH, trimFilters, guessFilterDelta} from "@welshman/util"
|
||||
import type {Feed, RequestItem, FeedOptions} from "./core.js"
|
||||
import {FeedType} from "./core.js"
|
||||
import {FeedCompiler} from "./compiler.js"
|
||||
|
||||
export class FeedController {
|
||||
compiler: FeedCompiler
|
||||
@@ -26,7 +26,7 @@ export class FeedController {
|
||||
return this._getRequestsLoader(requestItems)
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
switch (type) {
|
||||
case FeedType.Difference:
|
||||
return this._getDifferenceLoader(feed as Feed[])
|
||||
case FeedType.Intersection:
|
||||
@@ -45,8 +45,8 @@ export class FeedController {
|
||||
const seen = new Set()
|
||||
const exhausted = new Set()
|
||||
const loaders = await Promise.all(
|
||||
requests.map(
|
||||
request => this._getRequestLoader(request, {
|
||||
requests.map(request =>
|
||||
this._getRequestLoader(request, {
|
||||
onExhausted: () => exhausted.add(request),
|
||||
onEvent: e => {
|
||||
if (!seen.has(e.id)) {
|
||||
@@ -54,8 +54,8 @@ export class FeedController {
|
||||
seen.add(e.id)
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return async (limit: number) => {
|
||||
@@ -75,8 +75,8 @@ export class FeedController {
|
||||
filters = [{}]
|
||||
}
|
||||
|
||||
const untils = filters.flatMap((filter: Filter) => filter.until ? [filter.until] : [])
|
||||
const sinces = filters.flatMap((filter: Filter) => filter.since ? [filter.since] : [])
|
||||
const untils = filters.flatMap((filter: Filter) => (filter.until ? [filter.until] : []))
|
||||
const sinces = filters.flatMap((filter: Filter) => (filter.since ? [filter.since] : []))
|
||||
const maxUntil = untils.length === filters.length ? max(untils) : now()
|
||||
const minSince = sinces.length === filters.length ? min(sinces) : EPOCH
|
||||
const initialDelta = guessFilterDelta(filters)
|
||||
@@ -110,15 +110,17 @@ export class FeedController {
|
||||
|
||||
let count = 0
|
||||
|
||||
await request(omitVals([undefined], {
|
||||
relays,
|
||||
filters: trimFilters(requestFilters),
|
||||
onEvent: (event: TrustedEvent) => {
|
||||
count += 1
|
||||
until = Math.min(until, event.created_at - 1)
|
||||
onEvent?.(event)
|
||||
},
|
||||
}))
|
||||
await request(
|
||||
omitVals([undefined], {
|
||||
relays,
|
||||
filters: trimFilters(requestFilters),
|
||||
onEvent: (event: TrustedEvent) => {
|
||||
count += 1
|
||||
until = Math.min(until, event.created_at - 1)
|
||||
onEvent?.(event)
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
if (useWindowing) {
|
||||
if (since === minSince) {
|
||||
@@ -149,20 +151,21 @@ export class FeedController {
|
||||
const seen = new Set()
|
||||
|
||||
const controllers = await Promise.all(
|
||||
feeds.map((thisFeed: Feed, i: number) =>
|
||||
new FeedController({
|
||||
...options,
|
||||
feed: thisFeed,
|
||||
onExhausted: () => exhausted.add(i),
|
||||
onEvent: (event: TrustedEvent) => {
|
||||
if (i === 0) {
|
||||
events.push(event)
|
||||
} else {
|
||||
skip.add(event.id)
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
feeds.map(
|
||||
(thisFeed: Feed, i: number) =>
|
||||
new FeedController({
|
||||
...options,
|
||||
feed: thisFeed,
|
||||
onExhausted: () => exhausted.add(i),
|
||||
onEvent: (event: TrustedEvent) => {
|
||||
if (i === 0) {
|
||||
events.push(event)
|
||||
} else {
|
||||
skip.add(event.id)
|
||||
}
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return async (limit: number) => {
|
||||
@@ -173,7 +176,7 @@ export class FeedController {
|
||||
}
|
||||
|
||||
await controller.load(limit)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
for (const event of events.splice(0)) {
|
||||
@@ -197,17 +200,18 @@ export class FeedController {
|
||||
const seen = new Set()
|
||||
|
||||
const controllers = await Promise.all(
|
||||
feeds.map((thisFeed: Feed, i: number) =>
|
||||
new FeedController({
|
||||
...options,
|
||||
feed: thisFeed,
|
||||
onExhausted: () => exhausted.add(i),
|
||||
onEvent: (event: TrustedEvent) => {
|
||||
events.push(event)
|
||||
counts.set(event.id, inc(counts.get(event.id)))
|
||||
},
|
||||
})
|
||||
)
|
||||
feeds.map(
|
||||
(thisFeed: Feed, i: number) =>
|
||||
new FeedController({
|
||||
...options,
|
||||
feed: thisFeed,
|
||||
onExhausted: () => exhausted.add(i),
|
||||
onEvent: (event: TrustedEvent) => {
|
||||
events.push(event)
|
||||
counts.set(event.id, inc(counts.get(event.id)))
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return async (limit: number) => {
|
||||
@@ -218,7 +222,7 @@ export class FeedController {
|
||||
}
|
||||
|
||||
await controller.load(limit)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
for (const event of events.splice(0)) {
|
||||
@@ -240,19 +244,20 @@ export class FeedController {
|
||||
const seen = new Set()
|
||||
|
||||
const controllers = await Promise.all(
|
||||
feeds.map((thisFeed: Feed, i: number) =>
|
||||
new FeedController({
|
||||
...options,
|
||||
feed: thisFeed,
|
||||
onExhausted: () => exhausted.add(i),
|
||||
onEvent: (event: TrustedEvent) => {
|
||||
if (!seen.has(event.id)) {
|
||||
onEvent?.(event)
|
||||
seen.add(event.id)
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
feeds.map(
|
||||
(thisFeed: Feed, i: number) =>
|
||||
new FeedController({
|
||||
...options,
|
||||
feed: thisFeed,
|
||||
onExhausted: () => exhausted.add(i),
|
||||
onEvent: (event: TrustedEvent) => {
|
||||
if (!seen.has(event.id)) {
|
||||
onEvent?.(event)
|
||||
seen.add(event.id)
|
||||
}
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return async (limit: number) => {
|
||||
@@ -263,7 +268,7 @@ export class FeedController {
|
||||
}
|
||||
|
||||
await controller.load(limit)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
if (exhausted.size === controllers.length) {
|
||||
|
||||
+40
-40
@@ -1,4 +1,4 @@
|
||||
import type {TrustedEvent, Filter} from '@welshman/util'
|
||||
import type {TrustedEvent, Filter} from "@welshman/util"
|
||||
|
||||
export enum FeedType {
|
||||
Address = "address",
|
||||
@@ -28,43 +28,43 @@ export enum Scope {
|
||||
}
|
||||
|
||||
export type FilterFeedType =
|
||||
FeedType.ID |
|
||||
FeedType.Address |
|
||||
FeedType.Author |
|
||||
FeedType.Kind |
|
||||
FeedType.Relay |
|
||||
FeedType.Tag
|
||||
| FeedType.ID
|
||||
| FeedType.Address
|
||||
| FeedType.Author
|
||||
| FeedType.Kind
|
||||
| FeedType.Relay
|
||||
| FeedType.Tag
|
||||
|
||||
export type TagFeedMapping = [string, Feed]
|
||||
|
||||
export type DVMItem = {
|
||||
kind: number,
|
||||
tags?: string[][],
|
||||
relays?: string[],
|
||||
mappings?: TagFeedMapping[],
|
||||
kind: number
|
||||
tags?: string[][]
|
||||
relays?: string[]
|
||||
mappings?: TagFeedMapping[]
|
||||
}
|
||||
|
||||
export type ListItem = {
|
||||
addresses: string[],
|
||||
mappings?: TagFeedMapping[],
|
||||
addresses: string[]
|
||||
mappings?: TagFeedMapping[]
|
||||
}
|
||||
|
||||
export type LabelItem = {
|
||||
relays?: string[],
|
||||
relays?: string[]
|
||||
authors?: string[]
|
||||
[key: `#${string}`]: string[]
|
||||
mappings?: TagFeedMapping[],
|
||||
mappings?: TagFeedMapping[]
|
||||
}
|
||||
|
||||
export type WOTItem = {
|
||||
min?: number,
|
||||
max?: number,
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
|
||||
export type CreatedAtItem = {
|
||||
since?: number,
|
||||
until?: number,
|
||||
relative?: string[],
|
||||
since?: number
|
||||
until?: number
|
||||
relative?: string[]
|
||||
}
|
||||
|
||||
export type AddressFeed = [type: FeedType.Address, ...addresses: string[]]
|
||||
@@ -86,23 +86,23 @@ export type TagFeed = [type: FeedType.Tag, key: string, ...values: string[]]
|
||||
export type UnionFeed = [type: FeedType.Union, ...feeds: Feed[]]
|
||||
|
||||
export type Feed =
|
||||
AddressFeed |
|
||||
AuthorFeed |
|
||||
CreatedAtFeed |
|
||||
DVMFeed |
|
||||
DifferenceFeed |
|
||||
IDFeed |
|
||||
IntersectionFeed |
|
||||
GlobalFeed |
|
||||
KindFeed |
|
||||
ListFeed |
|
||||
LabelFeed |
|
||||
WOTFeed |
|
||||
RelayFeed |
|
||||
ScopeFeed |
|
||||
SearchFeed |
|
||||
TagFeed |
|
||||
UnionFeed
|
||||
| AddressFeed
|
||||
| AuthorFeed
|
||||
| CreatedAtFeed
|
||||
| DVMFeed
|
||||
| DifferenceFeed
|
||||
| IDFeed
|
||||
| IntersectionFeed
|
||||
| GlobalFeed
|
||||
| KindFeed
|
||||
| ListFeed
|
||||
| LabelFeed
|
||||
| WOTFeed
|
||||
| RelayFeed
|
||||
| ScopeFeed
|
||||
| SearchFeed
|
||||
| TagFeed
|
||||
| UnionFeed
|
||||
|
||||
export type RequestItem = {
|
||||
relays?: string[]
|
||||
@@ -114,9 +114,9 @@ export type RequestOpts = RequestItem & {
|
||||
}
|
||||
|
||||
export type DVMRequest = {
|
||||
kind: number,
|
||||
tags?: string[][],
|
||||
relays?: string[],
|
||||
kind: number
|
||||
tags?: string[][]
|
||||
relays?: string[]
|
||||
}
|
||||
|
||||
export type DVMOpts = DVMRequest & {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './core'
|
||||
export * from './compiler'
|
||||
export * from './controller'
|
||||
export * from './utils'
|
||||
export * from "./core.js"
|
||||
export * from "./compiler.js"
|
||||
export * from "./controller.js"
|
||||
export * from "./utils.js"
|
||||
|
||||
+105
-67
@@ -1,6 +1,6 @@
|
||||
import {ensureNumber} from '@welshman/lib'
|
||||
import type {Filter} from '@welshman/util'
|
||||
import {getTagValues} from '@welshman/util'
|
||||
import {ensureNumber} from "@welshman/lib"
|
||||
import type {Filter} from "@welshman/util"
|
||||
import {getTagValues} from "@welshman/util"
|
||||
import {
|
||||
FeedType,
|
||||
Feed,
|
||||
@@ -28,46 +28,66 @@ import {
|
||||
ListItem,
|
||||
LabelItem,
|
||||
CreatedAtItem,
|
||||
} from './core'
|
||||
} from "./core.js"
|
||||
|
||||
export const makeAddressFeed = (...addresses: string[]): AddressFeed => [FeedType.Address, ...addresses]
|
||||
export const makeAuthorFeed = (...pubkeys: string[]): AuthorFeed => [FeedType.Author, ...pubkeys]
|
||||
export const makeCreatedAtFeed = (...items: CreatedAtItem[]): CreatedAtFeed => [FeedType.CreatedAt, ...items]
|
||||
export const makeDVMFeed = (...items: DVMItem[]): DVMFeed => [FeedType.DVM, ...items]
|
||||
export const makeDifferenceFeed = (...feeds: Feed[]): DifferenceFeed => [FeedType.Difference, ...feeds]
|
||||
export const makeIDFeed = (...ids: string[]): IDFeed => [FeedType.ID, ...ids]
|
||||
export const makeIntersectionFeed = (...feeds: Feed[]): IntersectionFeed => [FeedType.Intersection, ...feeds]
|
||||
export const makeGlobalFeed = (): GlobalFeed => [FeedType.Global]
|
||||
export const makeKindFeed = (...kinds: number[]): KindFeed => [FeedType.Kind, ...kinds]
|
||||
export const makeListFeed = (...items: ListItem[]): ListFeed => [FeedType.List, ...items]
|
||||
export const makeLabelFeed = (...items: LabelItem[]): LabelFeed => [FeedType.Label, ...items]
|
||||
export const makeWOTFeed = (...items: WOTItem[]): WOTFeed => [FeedType.WOT, ...items]
|
||||
export const makeRelayFeed = (...urls: string[]): RelayFeed => [FeedType.Relay, ...urls]
|
||||
export const makeScopeFeed = (...scopes: Scope[]): ScopeFeed => [FeedType.Scope, ...scopes]
|
||||
export const makeSearchFeed = (...searches: string[]): SearchFeed => [FeedType.Search, ...searches]
|
||||
export const makeTagFeed = (key: string, ...values: string[]): TagFeed => [FeedType.Tag, key, ...values]
|
||||
export const makeUnionFeed = (...feeds: Feed[]): UnionFeed => [FeedType.Union, ...feeds]
|
||||
export const makeAddressFeed = (...addresses: string[]): AddressFeed => [
|
||||
FeedType.Address,
|
||||
...addresses,
|
||||
]
|
||||
export const makeAuthorFeed = (...pubkeys: string[]): AuthorFeed => [FeedType.Author, ...pubkeys]
|
||||
export const makeCreatedAtFeed = (...items: CreatedAtItem[]): CreatedAtFeed => [
|
||||
FeedType.CreatedAt,
|
||||
...items,
|
||||
]
|
||||
export const makeDVMFeed = (...items: DVMItem[]): DVMFeed => [FeedType.DVM, ...items]
|
||||
export const makeDifferenceFeed = (...feeds: Feed[]): DifferenceFeed => [
|
||||
FeedType.Difference,
|
||||
...feeds,
|
||||
]
|
||||
export const makeIDFeed = (...ids: string[]): IDFeed => [FeedType.ID, ...ids]
|
||||
export const makeIntersectionFeed = (...feeds: Feed[]): IntersectionFeed => [
|
||||
FeedType.Intersection,
|
||||
...feeds,
|
||||
]
|
||||
export const makeGlobalFeed = (): GlobalFeed => [FeedType.Global]
|
||||
export const makeKindFeed = (...kinds: number[]): KindFeed => [FeedType.Kind, ...kinds]
|
||||
export const makeListFeed = (...items: ListItem[]): ListFeed => [FeedType.List, ...items]
|
||||
export const makeLabelFeed = (...items: LabelItem[]): LabelFeed => [FeedType.Label, ...items]
|
||||
export const makeWOTFeed = (...items: WOTItem[]): WOTFeed => [FeedType.WOT, ...items]
|
||||
export const makeRelayFeed = (...urls: string[]): RelayFeed => [FeedType.Relay, ...urls]
|
||||
export const makeScopeFeed = (...scopes: Scope[]): ScopeFeed => [FeedType.Scope, ...scopes]
|
||||
export const makeSearchFeed = (...searches: string[]): SearchFeed => [FeedType.Search, ...searches]
|
||||
export const makeTagFeed = (key: string, ...values: string[]): TagFeed => [
|
||||
FeedType.Tag,
|
||||
key,
|
||||
...values,
|
||||
]
|
||||
export const makeUnionFeed = (...feeds: Feed[]): UnionFeed => [FeedType.Union, ...feeds]
|
||||
|
||||
export const isAddressFeed = (feed: Feed): feed is AddressFeed => feed[0] === FeedType.Address
|
||||
export const isAuthorFeed = (feed: Feed): feed is AuthorFeed => feed[0] === FeedType.Author
|
||||
export const isCreatedAtFeed = (feed: Feed): feed is CreatedAtFeed => feed[0] === FeedType.CreatedAt
|
||||
export const isDVMFeed = (feed: Feed): feed is DVMFeed => feed[0] === FeedType.DVM
|
||||
export const isDifferenceFeed = (feed: Feed): feed is DifferenceFeed => feed[0] === FeedType.Difference
|
||||
export const isIDFeed = (feed: Feed): feed is IDFeed => feed[0] === FeedType.ID
|
||||
export const isIntersectionFeed = (feed: Feed): feed is IntersectionFeed => feed[0] === FeedType.Intersection
|
||||
export const isGlobalFeed = (feed: Feed): feed is GlobalFeed => feed[0] === FeedType.Global
|
||||
export const isKindFeed = (feed: Feed): feed is KindFeed => feed[0] === FeedType.Kind
|
||||
export const isListFeed = (feed: Feed): feed is ListFeed => feed[0] === FeedType.List
|
||||
export const isLabelFeed = (feed: Feed): feed is LabelFeed => feed[0] === FeedType.Label
|
||||
export const isWOTFeed = (feed: Feed): feed is WOTFeed => feed[0] === FeedType.WOT
|
||||
export const isRelayFeed = (feed: Feed): feed is RelayFeed => feed[0] === FeedType.Relay
|
||||
export const isScopeFeed = (feed: Feed): feed is ScopeFeed => feed[0] === FeedType.Scope
|
||||
export const isSearchFeed = (feed: Feed): feed is SearchFeed => feed[0] === FeedType.Search
|
||||
export const isTagFeed = (feed: Feed): feed is TagFeed => feed[0] === FeedType.Tag
|
||||
export const isUnionFeed = (feed: Feed): feed is UnionFeed => feed[0] === FeedType.Union
|
||||
export const isAddressFeed = (feed: Feed): feed is AddressFeed => feed[0] === FeedType.Address
|
||||
export const isAuthorFeed = (feed: Feed): feed is AuthorFeed => feed[0] === FeedType.Author
|
||||
export const isCreatedAtFeed = (feed: Feed): feed is CreatedAtFeed => feed[0] === FeedType.CreatedAt
|
||||
export const isDVMFeed = (feed: Feed): feed is DVMFeed => feed[0] === FeedType.DVM
|
||||
export const isDifferenceFeed = (feed: Feed): feed is DifferenceFeed =>
|
||||
feed[0] === FeedType.Difference
|
||||
export const isIDFeed = (feed: Feed): feed is IDFeed => feed[0] === FeedType.ID
|
||||
export const isIntersectionFeed = (feed: Feed): feed is IntersectionFeed =>
|
||||
feed[0] === FeedType.Intersection
|
||||
export const isGlobalFeed = (feed: Feed): feed is GlobalFeed => feed[0] === FeedType.Global
|
||||
export const isKindFeed = (feed: Feed): feed is KindFeed => feed[0] === FeedType.Kind
|
||||
export const isListFeed = (feed: Feed): feed is ListFeed => feed[0] === FeedType.List
|
||||
export const isLabelFeed = (feed: Feed): feed is LabelFeed => feed[0] === FeedType.Label
|
||||
export const isWOTFeed = (feed: Feed): feed is WOTFeed => feed[0] === FeedType.WOT
|
||||
export const isRelayFeed = (feed: Feed): feed is RelayFeed => feed[0] === FeedType.Relay
|
||||
export const isScopeFeed = (feed: Feed): feed is ScopeFeed => feed[0] === FeedType.Scope
|
||||
export const isSearchFeed = (feed: Feed): feed is SearchFeed => feed[0] === FeedType.Search
|
||||
export const isTagFeed = (feed: Feed): feed is TagFeed => feed[0] === FeedType.Tag
|
||||
export const isUnionFeed = (feed: Feed): feed is UnionFeed => feed[0] === FeedType.Union
|
||||
|
||||
export function getFeedArgs(feed: IntersectionFeed | UnionFeed | DifferenceFeed): Feed[]
|
||||
export function getFeedArgs(feed: AddressFeed | AuthorFeed | IDFeed | RelayFeed | SearchFeed): string[]
|
||||
export function getFeedArgs(
|
||||
feed: AddressFeed | AuthorFeed | IDFeed | RelayFeed | SearchFeed,
|
||||
): string[]
|
||||
export function getFeedArgs(feed: CreatedAtFeed): CreatedAtItem[]
|
||||
export function getFeedArgs(feed: ListFeed): ListItem[]
|
||||
export function getFeedArgs(feed: LabelFeed): LabelItem[]
|
||||
@@ -79,23 +99,42 @@ export function getFeedArgs(feed: TagFeed): [string, ...string[]]
|
||||
export function getFeedArgs(feed: GlobalFeed): []
|
||||
export function getFeedArgs(feed: Feed) {
|
||||
switch (feed[0]) {
|
||||
case FeedType.Intersection: return feed.slice(1) as Feed[]
|
||||
case FeedType.Union: return feed.slice(1) as Feed[]
|
||||
case FeedType.Difference: return feed.slice(1) as Feed[]
|
||||
case FeedType.Address: return feed.slice(1) as string[]
|
||||
case FeedType.Author: return feed.slice(1) as string[]
|
||||
case FeedType.ID: return feed.slice(1) as string[]
|
||||
case FeedType.Relay: return feed.slice(1) as string[]
|
||||
case FeedType.Search: return feed.slice(1) as string[]
|
||||
case FeedType.Tag: return feed.slice(1) as [string, ...string[]]
|
||||
case FeedType.CreatedAt: return feed.slice(1) as CreatedAtItem[]
|
||||
case FeedType.List: return feed.slice(1) as ListItem[]
|
||||
case FeedType.Label: return feed.slice(1) as LabelItem[]
|
||||
case FeedType.DVM: return feed.slice(1) as DVMItem[]
|
||||
case FeedType.WOT: return feed.slice(1) as WOTItem[]
|
||||
case FeedType.Scope: return feed.slice(1) as Scope[]
|
||||
case FeedType.Kind: return feed.slice(1) as number[]
|
||||
case FeedType.Global: return feed.slice(1) as never[]
|
||||
case FeedType.Intersection:
|
||||
return feed.slice(1) as Feed[]
|
||||
case FeedType.Union:
|
||||
return feed.slice(1) as Feed[]
|
||||
case FeedType.Difference:
|
||||
return feed.slice(1) as Feed[]
|
||||
case FeedType.Address:
|
||||
return feed.slice(1) as string[]
|
||||
case FeedType.Author:
|
||||
return feed.slice(1) as string[]
|
||||
case FeedType.ID:
|
||||
return feed.slice(1) as string[]
|
||||
case FeedType.Relay:
|
||||
return feed.slice(1) as string[]
|
||||
case FeedType.Search:
|
||||
return feed.slice(1) as string[]
|
||||
case FeedType.Tag:
|
||||
return feed.slice(1) as [string, ...string[]]
|
||||
case FeedType.CreatedAt:
|
||||
return feed.slice(1) as CreatedAtItem[]
|
||||
case FeedType.List:
|
||||
return feed.slice(1) as ListItem[]
|
||||
case FeedType.Label:
|
||||
return feed.slice(1) as LabelItem[]
|
||||
case FeedType.DVM:
|
||||
return feed.slice(1) as DVMItem[]
|
||||
case FeedType.WOT:
|
||||
return feed.slice(1) as WOTItem[]
|
||||
case FeedType.Scope:
|
||||
return feed.slice(1) as Scope[]
|
||||
case FeedType.Kind:
|
||||
return feed.slice(1) as number[]
|
||||
case FeedType.Global:
|
||||
return feed.slice(1) as never[]
|
||||
default:
|
||||
throw new Error(`Invalid feed type ${feed[0]}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,11 +142,11 @@ export const hasSubFeeds = (feed: Feed): feed is IntersectionFeed | UnionFeed |
|
||||
[FeedType.Union, FeedType.Intersection, FeedType.Difference].includes(feed[0])
|
||||
|
||||
export const defaultTagFeedMappings: TagFeedMapping[] = [
|
||||
['a', [FeedType.Address]],
|
||||
['e', [FeedType.ID]],
|
||||
['p', [FeedType.Author]],
|
||||
['r', [FeedType.Relay]],
|
||||
['t', [FeedType.Tag, '#t']],
|
||||
["a", [FeedType.Address]],
|
||||
["e", [FeedType.ID]],
|
||||
["p", [FeedType.Author]],
|
||||
["r", [FeedType.Relay]],
|
||||
["t", [FeedType.Tag, "#t"]],
|
||||
]
|
||||
|
||||
export const feedsFromTags = (tags: string[][], mappings?: TagFeedMapping[]) => {
|
||||
@@ -143,18 +182,17 @@ export const feedsFromFilter = ({since, until, ...filter}: Filter) => {
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(filter)) {
|
||||
if (k === 'ids') feeds.push(makeIDFeed(...v as string[]))
|
||||
else if (k === 'kinds') feeds.push(makeKindFeed(...v as number[]))
|
||||
else if (k === 'authors') feeds.push(makeAuthorFeed(...v as string[]))
|
||||
else if (k.startsWith('#')) feeds.push(makeTagFeed(k as string, ...v as string[]))
|
||||
if (k === "ids") feeds.push(makeIDFeed(...(v as string[])))
|
||||
else if (k === "kinds") feeds.push(makeKindFeed(...(v as number[])))
|
||||
else if (k === "authors") feeds.push(makeAuthorFeed(...(v as string[])))
|
||||
else if (k.startsWith("#")) feeds.push(makeTagFeed(k as string, ...(v as string[])))
|
||||
else throw new Error(`Unable to create feed from filter ${k}: ${v}`)
|
||||
}
|
||||
|
||||
return feeds
|
||||
}
|
||||
|
||||
export const feedFromFilter = (filter: Filter) =>
|
||||
makeIntersectionFeed(...feedsFromFilter(filter))
|
||||
export const feedFromFilter = (filter: Filter) => makeIntersectionFeed(...feedsFromFilter(filter))
|
||||
|
||||
export const feedsFromFilters = (filters: Filter[]) =>
|
||||
makeUnionFeed(...filters.map(filter => feedFromFilter(filter)))
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"targets": [
|
||||
{"extname": ".cjs", "module": "commonjs"},
|
||||
{"extname": ".mjs", "module": "esnext", "moduleResolution": "node"}
|
||||
],
|
||||
"projects": ["tsconfig.json"]
|
||||
}
|
||||
@@ -3,9 +3,12 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "build",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["esnext", "dom", "dom.iterable"]
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"lib": ["esnext", "dom"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,24 +15,19 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/src/index.d.ts",
|
||||
"import": "./build/src/index.mjs",
|
||||
"require": "./build/src/index.cjs"
|
||||
"import": "./build/src/index.js",
|
||||
"require": "./build/src/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"pub": "npm run lint && npm run build && npm publish",
|
||||
"build": "gts clean && tsc-multi",
|
||||
"build": "gts clean && tsc",
|
||||
"lint": "gts lint",
|
||||
"fix": "gts fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gts": "^5.0.1",
|
||||
"tsc-multi": "^1.1.0",
|
||||
"typescript": "~5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/events": "^3.0.3",
|
||||
"@scure/base": "^1.1.6",
|
||||
"@types/events": "^3.0.3",
|
||||
"events": "^3.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type {Context} from '@welshman/lib'
|
||||
import type {Context} from "@welshman/lib"
|
||||
|
||||
/**
|
||||
* A global context variable for configuring libraries and applications.
|
||||
|
||||
@@ -9,10 +9,7 @@ export type CustomPromise<T, E> = Promise<T> & {
|
||||
* @returns Promise with typed error
|
||||
*/
|
||||
export function makePromise<T, E>(
|
||||
executor: (
|
||||
resolve: (value: T | PromiseLike<T>) => void,
|
||||
reject: (reason: E) => void
|
||||
) => void
|
||||
executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason: E) => void) => void,
|
||||
): CustomPromise<T, E> {
|
||||
return new Promise(executor) as CustomPromise<T, E>
|
||||
}
|
||||
@@ -34,5 +31,5 @@ export const defer = <T, E = T>(): Deferred<T, E> => {
|
||||
reject = reject_
|
||||
})
|
||||
|
||||
return (Object.assign(p, {resolve, reject}) as unknown) as Deferred<T, E>
|
||||
return Object.assign(p, {resolve, reject}) as unknown as Deferred<T, E>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {EventEmitter} from 'events'
|
||||
import {EventEmitter} from "events"
|
||||
|
||||
/**
|
||||
* Extended EventEmitter that also emits all events to '*' listeners
|
||||
@@ -12,7 +12,7 @@ export class Emitter extends EventEmitter {
|
||||
*/
|
||||
emit(type: string, ...args: any[]) {
|
||||
const a = super.emit(type, ...args)
|
||||
const b = super.emit('*', type, ...args)
|
||||
const b = super.emit("*", type, ...args)
|
||||
|
||||
return a && b
|
||||
}
|
||||
|
||||
@@ -77,5 +77,5 @@ export function cached<T, V, Args extends any[]>({
|
||||
* @template Args - Function argument types
|
||||
*/
|
||||
export function simpleCache<V, Args extends any[]>(getValue: (args: Args) => V) {
|
||||
return cached({maxSize: 10**5, getKey: xs => xs.join(':'), getValue})
|
||||
return cached({maxSize: 10 ** 5, getKey: xs => xs.join(":"), getValue})
|
||||
}
|
||||
|
||||
+61
-23
@@ -15,7 +15,8 @@ export type Maybe<T> = T | undefined
|
||||
* @param f - Function to execute if x is defined
|
||||
* @returns Result of f(x) if x is defined, undefined otherwise
|
||||
*/
|
||||
export const ifLet = <T>(x: T | undefined, f: (x: T) => void) => x === undefined ? undefined : f(x)
|
||||
export const ifLet = <T>(x: T | undefined, f: (x: T) => void) =>
|
||||
x === undefined ? undefined : f(x)
|
||||
|
||||
/** Function that does nothing and returns undefined */
|
||||
export const noop = (...args: unknown[]) => undefined
|
||||
@@ -53,7 +54,10 @@ export const identity = <T>(x: T, ...args: unknown[]) => x
|
||||
* @param x - Value to return
|
||||
* @returns Function that returns x
|
||||
*/
|
||||
export const always = <T>(x: T, ...args: unknown[]) => () => x
|
||||
export const always =
|
||||
<T>(x: T, ...args: unknown[]) =>
|
||||
() =>
|
||||
x
|
||||
|
||||
/**
|
||||
* Returns the logical NOT of a value
|
||||
@@ -63,7 +67,10 @@ export const always = <T>(x: T, ...args: unknown[]) => () => x
|
||||
export const not = (x: any, ...args: unknown[]) => !x
|
||||
|
||||
/** Returns a function that returns the boolean negation of the given function */
|
||||
export const complement = <T extends unknown[]>(f: (...args: T) => any) => (...args: T) => !f(...args)
|
||||
export const complement =
|
||||
<T extends unknown[]>(f: (...args: T) => any) =>
|
||||
(...args: T) =>
|
||||
!f(...args)
|
||||
|
||||
/** Converts a Maybe<number> to a number, defaulting to 0 */
|
||||
export const num = (x: Maybe<number>) => x || 0
|
||||
@@ -227,17 +234,23 @@ export const mapVals = <V, U>(f: (v: V) => U, x: Record<string, V>) => {
|
||||
* Merges two objects, with left object taking precedence
|
||||
* @param a - Left object
|
||||
* @param b - Right object
|
||||
* @returns Merged object with a's properties overriding b's
|
||||
* @returns Merged object with a"s properties overriding b"s
|
||||
*/
|
||||
export const mergeLeft = <T extends Record<string, any>>(a: T, b: T) => ({...b, ...a})
|
||||
export const mergeLeft = <T extends Record<string, any>>(a: T, b: T) => ({
|
||||
...b,
|
||||
...a,
|
||||
})
|
||||
|
||||
/**
|
||||
* Merges two objects, with right object taking precedence
|
||||
* @param a - Left object
|
||||
* @param b - Right object
|
||||
* @returns Merged object with b's properties overriding a's
|
||||
* @returns Merged object with b"s properties overriding a"s
|
||||
*/
|
||||
export const mergeRight = <T extends Record<string, any>>(a: T, b: T) => ({...a, ...b})
|
||||
export const mergeRight = <T extends Record<string, any>>(a: T, b: T) => ({
|
||||
...a,
|
||||
...b,
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks if a number is between two values (exclusive)
|
||||
@@ -281,7 +294,10 @@ export const stripProtocol = (url: string) => url.replace(/.*:\/\//, "")
|
||||
* @param url - URL to format
|
||||
* @returns Formatted URL
|
||||
*/
|
||||
export const displayUrl = (url: string) => stripProtocol(url).replace(/^(www\.)?/i, "").replace(/\/$/, "")
|
||||
export const displayUrl = (url: string) =>
|
||||
stripProtocol(url)
|
||||
.replace(/^(www\.)?/i, "")
|
||||
.replace(/\/$/, "")
|
||||
|
||||
/**
|
||||
* Extracts and formats domain from URL
|
||||
@@ -302,7 +318,7 @@ export const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t
|
||||
* @param xs - Arrays to concatenate
|
||||
* @returns Combined array
|
||||
*/
|
||||
export const concat = <T>(...xs: T[][]) => xs.flatMap(x => isNil(x) ? [] : x)
|
||||
export const concat = <T>(...xs: T[][]) => xs.flatMap(x => (isNil(x) ? [] : x))
|
||||
|
||||
/**
|
||||
* Appends element to array
|
||||
@@ -366,7 +382,7 @@ export const without = <T>(a: T[], b: T[]) => b.filter(x => !a.includes(x))
|
||||
* @param xs - Source array
|
||||
* @returns New array with element added or removed
|
||||
*/
|
||||
export const toggle = <T>(x: T, xs: T[]) => xs.includes(x) ? remove(x, xs) : append(x, xs)
|
||||
export const toggle = <T>(x: T, xs: T[]) => (xs.includes(x) ? remove(x, xs) : append(x, xs))
|
||||
|
||||
/**
|
||||
* Constrains number between min and max values
|
||||
@@ -434,13 +450,13 @@ export const tryCatch = <T>(f: () => T, onError?: (e: Error) => void): T | undef
|
||||
* @param suffix - String to append if truncated
|
||||
* @returns Truncated string
|
||||
*/
|
||||
export const ellipsize = (s: string, l: number, suffix = '...') => {
|
||||
export const ellipsize = (s: string, l: number, suffix = "...") => {
|
||||
if (s.length < l * 1.1) {
|
||||
return s
|
||||
}
|
||||
|
||||
while (s.length > l && s.includes(' ')) {
|
||||
s = s.split(' ').slice(0, -1).join(' ')
|
||||
while (s.length > l && s.includes(" ")) {
|
||||
s = s.split(" ").slice(0, -1).join(" ")
|
||||
}
|
||||
|
||||
return s + suffix
|
||||
@@ -522,13 +538,22 @@ export const equals = (a: any, b: any) => {
|
||||
// Curried utils
|
||||
|
||||
/** Returns a function that gets the nth element of an array */
|
||||
export const nth = (i: number) => <T>(xs: T[], ...args: unknown[]) => xs[i]
|
||||
export const nth =
|
||||
(i: number) =>
|
||||
<T>(xs: T[], ...args: unknown[]) =>
|
||||
xs[i]
|
||||
|
||||
/** Returns a function that checks if nth element equals value */
|
||||
export const nthEq = (i: number, v: any) => (xs: any[], ...args: unknown[]) => xs[i] === v
|
||||
export const nthEq =
|
||||
(i: number, v: any) =>
|
||||
(xs: any[], ...args: unknown[]) =>
|
||||
xs[i] === v
|
||||
|
||||
/** Returns a function that checks if nth element does not equal value */
|
||||
export const nthNe = (i: number, v: any) => (xs: any[], ...args: unknown[]) => xs[i] !== v
|
||||
export const nthNe =
|
||||
(i: number, v: any) =>
|
||||
(xs: any[], ...args: unknown[]) =>
|
||||
xs[i] !== v
|
||||
|
||||
/** Returns a function that checks if key/value pairs of x match all pairs in spec */
|
||||
export const spec = (values: Record<string, any>) => (x: Record<string, any>) => {
|
||||
@@ -540,16 +565,28 @@ export const spec = (values: Record<string, any>) => (x: Record<string, any>) =>
|
||||
}
|
||||
|
||||
/** Returns a function that checks equality with value */
|
||||
export const eq = <T>(v: T) => (x: T) => x === v
|
||||
export const eq =
|
||||
<T>(v: T) =>
|
||||
(x: T) =>
|
||||
x === v
|
||||
|
||||
/** Returns a function that checks inequality with value */
|
||||
export const ne = <T>(v: T) => (x: T) => x !== v
|
||||
export const ne =
|
||||
<T>(v: T) =>
|
||||
(x: T) =>
|
||||
x !== v
|
||||
|
||||
/** Returns a function that gets property value from object */
|
||||
export const prop = <T>(k: string) => (x: Record<string, unknown>) => x[k] as T
|
||||
export const prop =
|
||||
<T>(k: string) =>
|
||||
(x: Record<string, unknown>) =>
|
||||
x[k] as T
|
||||
|
||||
/** Returns a function that adds/updates property on object */
|
||||
export const assoc = <K extends string, T, U>(k: K, v: T) => (o: U) => ({...o, [k as K]: v}) as U & Record<K, T>
|
||||
export const assoc =
|
||||
<K extends string, T, U>(k: K, v: T) =>
|
||||
(o: U) =>
|
||||
({...o, [k as K]: v}) as U & Record<K, T>
|
||||
|
||||
/** Generates a hash string from input string */
|
||||
export const hash = (s: string) =>
|
||||
@@ -567,7 +604,8 @@ export const insert = <T>(n: number, x: T, xs: T[]) => [...xs.slice(0, n), x, ..
|
||||
export const choice = <T>(xs: T[]): T => xs[Math.floor(xs.length * Math.random())]
|
||||
|
||||
/** Returns shuffled copy of iterable */
|
||||
export const shuffle = <T>(xs: Iterable<T>): T[] => Array.from(xs).sort(() => Math.random() > 0.5 ? 1 : -1)
|
||||
export const shuffle = <T>(xs: Iterable<T>): T[] =>
|
||||
Array.from(xs).sort(() => (Math.random() > 0.5 ? 1 : -1))
|
||||
|
||||
/** Returns n random elements from array */
|
||||
export const sample = <T>(n: number, xs: T[]) => shuffle(xs).slice(0, n)
|
||||
@@ -576,7 +614,7 @@ export const sample = <T>(n: number, xs: T[]) => shuffle(xs).slice(0, n)
|
||||
export const isIterable = (x: any) => Symbol.iterator in Object(x)
|
||||
|
||||
/** Ensures value is iterable by wrapping in array if needed */
|
||||
export const toIterable = (x: any) => isIterable(x) ? x : [x]
|
||||
export const toIterable = (x: any) => (isIterable(x) ? x : [x])
|
||||
|
||||
/** Ensures value is array by wrapping if needed */
|
||||
export const ensurePlural = <T>(x: T | T[]) => (x instanceof Array ? x : [x])
|
||||
@@ -893,7 +931,7 @@ export const batch = <T>(t: number, f: (xs: T[]) => void) => {
|
||||
* @returns Function that returns promise of result
|
||||
*/
|
||||
export const batcher = <T, U>(t: number, execute: (request: T[]) => U[] | Promise<U[]>) => {
|
||||
const queue: {request: T, resolve: (x: U) => void}[] = []
|
||||
const queue: {request: T; resolve: (x: U) => void}[] = []
|
||||
|
||||
const _execute = async () => {
|
||||
const items = queue.splice(0)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
export * from './Context'
|
||||
export * from './Deferred'
|
||||
export * from './Emitter'
|
||||
export * from './LRUCache'
|
||||
export * from './Tools'
|
||||
export * from './Worker'
|
||||
export {default as normalizeUrl} from './normalize-url'
|
||||
export * from "./Context.js"
|
||||
export * from "./Deferred.js"
|
||||
export * from "./Emitter.js"
|
||||
export * from "./LRUCache.js"
|
||||
export * from "./Tools.js"
|
||||
export * from "./Worker.js"
|
||||
export {default as normalizeUrl} from "./normalize-url/index.js"
|
||||
|
||||
declare module "@welshman/lib" {
|
||||
export interface Context {}
|
||||
}
|
||||
|
||||
@@ -514,7 +514,7 @@ export default function normalizeUrl(urlString: string, opts?: Options): string
|
||||
|
||||
// Remove query unwanted parameters
|
||||
if (Array.isArray(options.removeQueryParameters)) {
|
||||
// eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.
|
||||
// @ts-ignore
|
||||
for (const key of [...urlObject.searchParams.keys()]) {
|
||||
if (testParameter(key, options.removeQueryParameters)) {
|
||||
urlObject.searchParams.delete(key)
|
||||
@@ -528,7 +528,7 @@ export default function normalizeUrl(urlString: string, opts?: Options): string
|
||||
|
||||
// Keep wanted query parameters
|
||||
if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) {
|
||||
// eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.
|
||||
// @ts-ignore
|
||||
for (const key of [...urlObject.searchParams.keys()]) {
|
||||
if (!testParameter(key, options.keepQueryParameters)) {
|
||||
urlObject.searchParams.delete(key)
|
||||
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
declare module "@welshman/lib" {
|
||||
export interface Context {}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"targets": [
|
||||
{"extname": ".cjs", "module": "commonjs"},
|
||||
{"extname": ".mjs", "module": "esnext", "moduleResolution": "node"}
|
||||
],
|
||||
"projects": ["tsconfig.json"]
|
||||
}
|
||||
@@ -3,9 +3,12 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "build",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["esnext", "dom", "dom.iterable"]
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"lib": ["esnext", "dom"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,22 +15,19 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/src/index.d.ts",
|
||||
"import": "./build/src/index.mjs",
|
||||
"require": "./build/src/index.cjs"
|
||||
"import": "./build/src/index.js",
|
||||
"require": "./build/src/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"pub": "npm run lint && npm run build && npm publish",
|
||||
"build": "gts clean && tsc-multi",
|
||||
"build": "gts clean && tsc",
|
||||
"lint": "gts lint",
|
||||
"fix": "gts fix",
|
||||
"test": "mocha"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gts": "^5.0.1",
|
||||
"mocha": "^10.7.3",
|
||||
"tsc-multi": "^1.1.0",
|
||||
"typescript": "~5.1.6"
|
||||
"mocha": "^10.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@welshman/lib": "~0.0.33",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {Emitter} from '@welshman/lib'
|
||||
import {normalizeRelayUrl} from '@welshman/util'
|
||||
import {Socket} from './Socket'
|
||||
import type {Message} from './Socket'
|
||||
import {ConnectionEvent} from './ConnectionEvent'
|
||||
import {ConnectionState} from './ConnectionState'
|
||||
import {ConnectionStats} from './ConnectionStats'
|
||||
import {ConnectionAuth} from './ConnectionAuth'
|
||||
import {ConnectionSender} from './ConnectionSender'
|
||||
import {Emitter} from "@welshman/lib"
|
||||
import {normalizeRelayUrl} from "@welshman/util"
|
||||
import {Socket} from "./Socket.js"
|
||||
import type {Message} from "./Socket.js"
|
||||
import {ConnectionEvent} from "./ConnectionEvent.js"
|
||||
import {ConnectionState} from "./ConnectionState.js"
|
||||
import {ConnectionStats} from "./ConnectionStats.js"
|
||||
import {ConnectionAuth} from "./ConnectionAuth.js"
|
||||
import {ConnectionSender} from "./ConnectionSender.js"
|
||||
|
||||
export enum ConnectionStatus {
|
||||
Open = "open",
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
import {ctx, sleep} from '@welshman/lib'
|
||||
import {CLIENT_AUTH, createEvent} from '@welshman/util'
|
||||
import {ConnectionEvent} from './ConnectionEvent'
|
||||
import type {Connection} from './Connection'
|
||||
import type {Message} from './Socket'
|
||||
import {ctx, sleep} from "@welshman/lib"
|
||||
import {CLIENT_AUTH, createEvent} from "@welshman/util"
|
||||
import {ConnectionEvent} from "./ConnectionEvent.js"
|
||||
import type {Connection} from "./Connection.js"
|
||||
import type {Message} from "./Socket.js"
|
||||
|
||||
export enum AuthMode {
|
||||
Implicit = 'implicit',
|
||||
Explicit = 'explicit',
|
||||
Implicit = "implicit",
|
||||
Explicit = "explicit",
|
||||
}
|
||||
|
||||
export enum AuthStatus {
|
||||
None = 'none',
|
||||
Requested = 'requested',
|
||||
PendingSignature = 'pending_signature',
|
||||
DeniedSignature = 'denied_signature',
|
||||
PendingResponse = 'pending_response',
|
||||
Forbidden = 'forbidden',
|
||||
Ok = 'ok',
|
||||
None = "none",
|
||||
Requested = "requested",
|
||||
PendingSignature = "pending_signature",
|
||||
DeniedSignature = "denied_signature",
|
||||
PendingResponse = "pending_response",
|
||||
Forbidden = "forbidden",
|
||||
Ok = "ok",
|
||||
}
|
||||
|
||||
const {
|
||||
None,
|
||||
Requested,
|
||||
PendingSignature,
|
||||
DeniedSignature,
|
||||
PendingResponse,
|
||||
Forbidden,
|
||||
Ok,
|
||||
} = AuthStatus
|
||||
const {None, Requested, PendingSignature, DeniedSignature, PendingResponse, Forbidden, Ok} =
|
||||
AuthStatus
|
||||
|
||||
export class ConnectionAuth {
|
||||
challenge: string | undefined
|
||||
@@ -41,7 +34,7 @@ export class ConnectionAuth {
|
||||
}
|
||||
|
||||
#onReceive = (cxn: Connection, [verb, ...extra]: Message) => {
|
||||
if (verb === 'OK') {
|
||||
if (verb === "OK") {
|
||||
const [id, ok, message] = extra
|
||||
|
||||
if (id === this.request) {
|
||||
@@ -50,7 +43,7 @@ export class ConnectionAuth {
|
||||
}
|
||||
}
|
||||
|
||||
if (verb === 'AUTH' && extra[0] !== this.challenge) {
|
||||
if (verb === "AUTH" && extra[0] !== this.challenge) {
|
||||
this.challenge = extra[0]
|
||||
this.request = undefined
|
||||
this.message = undefined
|
||||
@@ -81,8 +74,7 @@ export class ConnectionAuth {
|
||||
}
|
||||
}
|
||||
|
||||
waitForChallenge = async (timeout = 300) =>
|
||||
this.waitFor(() => Boolean(this.challenge), timeout)
|
||||
waitForChallenge = async (timeout = 300) => this.waitFor(() => Boolean(this.challenge), timeout)
|
||||
|
||||
waitForResolution = async (timeout = 300) =>
|
||||
this.waitFor(() => [None, DeniedSignature, Forbidden, Ok].includes(this.status), timeout)
|
||||
@@ -105,14 +97,11 @@ export class ConnectionAuth {
|
||||
],
|
||||
})
|
||||
|
||||
const [event] = await Promise.all([
|
||||
ctx.net.signEvent(template),
|
||||
this.cxn.socket.open(),
|
||||
])
|
||||
const [event] = await Promise.all([ctx.net.signEvent(template), this.cxn.socket.open()])
|
||||
|
||||
if (event) {
|
||||
this.request = event.id
|
||||
this.cxn.send(['AUTH', event])
|
||||
this.cxn.send(["AUTH", event])
|
||||
this.status = PendingResponse
|
||||
} else {
|
||||
this.status = DeniedSignature
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export enum ConnectionEvent {
|
||||
InvalidUrl = 'invalid:url',
|
||||
InvalidMessage = 'invalid:message:receive',
|
||||
Open = 'socket:open',
|
||||
Reset = 'socket:reset',
|
||||
Close = 'socket:close',
|
||||
Error = 'socket:error',
|
||||
Receive = 'receive:message',
|
||||
Notice = 'receive:notice',
|
||||
Send = 'send:message',
|
||||
InvalidUrl = "invalid:url",
|
||||
InvalidMessage = "invalid:message:receive",
|
||||
Open = "socket:open",
|
||||
Reset = "socket:reset",
|
||||
Close = "socket:close",
|
||||
Error = "socket:error",
|
||||
Receive = "receive:message",
|
||||
Notice = "receive:notice",
|
||||
Send = "send:message",
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Worker} from '@welshman/lib'
|
||||
import {AUTH_JOIN} from '@welshman/util'
|
||||
import {SocketStatus} from './Socket'
|
||||
import type {Message} from './Socket'
|
||||
import type {Connection} from './Connection'
|
||||
import {AuthStatus} from './ConnectionAuth'
|
||||
import {Worker} from "@welshman/lib"
|
||||
import {AUTH_JOIN} from "@welshman/util"
|
||||
import {SocketStatus} from "./Socket.js"
|
||||
import type {Message} from "./Socket.js"
|
||||
import type {Connection} from "./Connection.js"
|
||||
import {AuthStatus} from "./ConnectionAuth.js"
|
||||
|
||||
export class ConnectionSender {
|
||||
worker: Worker<Message>
|
||||
@@ -12,22 +12,22 @@ export class ConnectionSender {
|
||||
this.worker = new Worker({
|
||||
shouldDefer: ([verb, ...extra]: Message) => {
|
||||
// Always send CLOSE to clean up pending requests, even if the connection is closed
|
||||
if (verb === 'CLOSE') return false
|
||||
if (verb === "CLOSE") return false
|
||||
|
||||
// If we're not connected, nothing we can do
|
||||
if (cxn.socket.status !== SocketStatus.Open) return true
|
||||
|
||||
// Always allow sending AUTH
|
||||
if (verb === 'AUTH') return false
|
||||
if (verb === "AUTH") return false
|
||||
|
||||
// Always allow sending join requests
|
||||
if (verb === 'EVENT' && extra[0].kind === AUTH_JOIN) return false
|
||||
if (verb === "EVENT" && extra[0].kind === AUTH_JOIN) return false
|
||||
|
||||
// Wait for auth
|
||||
if (![AuthStatus.None, AuthStatus.Ok].includes(cxn.auth.status)) return true
|
||||
|
||||
// Limit concurrent requests
|
||||
if (verb === 'REQ') return cxn.state.pendingRequests.size >= 8
|
||||
if (verb === "REQ") return cxn.state.pendingRequests.size >= 8
|
||||
|
||||
return false
|
||||
},
|
||||
@@ -35,8 +35,8 @@ export class ConnectionSender {
|
||||
|
||||
this.worker.addGlobalHandler(([verb, ...extra]: Message) => {
|
||||
// If we ended up handling a CLOSE before we handled the REQ, don't send the REQ
|
||||
if (verb === 'CLOSE') {
|
||||
this.worker.buffer = this.worker.buffer.filter(m => !(m[0] === 'REQ' && m[1] === extra[0]))
|
||||
if (verb === "CLOSE") {
|
||||
this.worker.buffer = this.worker.buffer.filter(m => !(m[0] === "REQ" && m[1] === extra[0]))
|
||||
}
|
||||
|
||||
// Re-check socket status since we let CLOSE through
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {sleep} from '@welshman/lib'
|
||||
import {AUTH_JOIN} from '@welshman/util'
|
||||
import type {SignedEvent, Filter} from '@welshman/util'
|
||||
import type {Message} from './Socket'
|
||||
import type {Connection} from './Connection'
|
||||
import {ConnectionEvent} from './ConnectionEvent'
|
||||
import {sleep} from "@welshman/lib"
|
||||
import {AUTH_JOIN} from "@welshman/util"
|
||||
import type {SignedEvent, Filter} from "@welshman/util"
|
||||
import type {Message} from "./Socket.js"
|
||||
import type {Connection} from "./Connection.js"
|
||||
import {ConnectionEvent} from "./ConnectionEvent.js"
|
||||
|
||||
export type PublishState = {
|
||||
sent: number
|
||||
@@ -22,19 +22,19 @@ export class ConnectionState {
|
||||
|
||||
constructor(readonly cxn: Connection) {
|
||||
cxn.sender.worker.addGlobalHandler(([verb, ...extra]: Message) => {
|
||||
if (verb === 'REQ') {
|
||||
if (verb === "REQ") {
|
||||
const [reqId, ...filters] = extra
|
||||
|
||||
this.pendingRequests.set(reqId, {filters, sent: Date.now()})
|
||||
}
|
||||
|
||||
if (verb === 'CLOSE') {
|
||||
if (verb === "CLOSE") {
|
||||
const [reqId] = extra
|
||||
|
||||
this.pendingRequests.delete(reqId)
|
||||
}
|
||||
|
||||
if (verb === 'EVENT') {
|
||||
if (verb === "EVENT") {
|
||||
const [event] = extra
|
||||
|
||||
this.pendingPublishes.set(event.id, {sent: Date.now(), event})
|
||||
@@ -42,21 +42,21 @@ export class ConnectionState {
|
||||
})
|
||||
|
||||
cxn.socket.worker.addGlobalHandler(([verb, ...extra]: Message) => {
|
||||
if (verb === 'OK') {
|
||||
if (verb === "OK") {
|
||||
const [eventId, _ok, notice] = extra
|
||||
const pub = this.pendingPublishes.get(eventId)
|
||||
|
||||
if (!pub) return
|
||||
|
||||
// Re-enqueue pending events when auth challenge is received
|
||||
if (notice?.startsWith('auth-required:') && pub.event.kind !== AUTH_JOIN) {
|
||||
this.cxn.send(['EVENT', pub.event])
|
||||
if (notice?.startsWith("auth-required:") && pub.event.kind !== AUTH_JOIN) {
|
||||
this.cxn.send(["EVENT", pub.event])
|
||||
} else {
|
||||
this.pendingPublishes.delete(eventId)
|
||||
}
|
||||
}
|
||||
|
||||
if (verb === 'EOSE') {
|
||||
if (verb === "EOSE") {
|
||||
const [reqId] = extra
|
||||
const req = this.pendingRequests.get(reqId)
|
||||
|
||||
@@ -65,15 +65,15 @@ export class ConnectionState {
|
||||
}
|
||||
}
|
||||
|
||||
if (verb === 'CLOSED') {
|
||||
if (verb === "CLOSED") {
|
||||
const [reqId] = extra
|
||||
|
||||
// Re-enqueue pending reqs when auth challenge is received
|
||||
if (extra[1]?.startsWith('auth-required:')) {
|
||||
if (extra[1]?.startsWith("auth-required:")) {
|
||||
const req = this.pendingRequests.get(reqId)
|
||||
|
||||
if (req) {
|
||||
this.cxn.send(['REQ', reqId, ...req.filters])
|
||||
this.cxn.send(["REQ", reqId, ...req.filters])
|
||||
}
|
||||
|
||||
if (extra[1]) {
|
||||
@@ -84,7 +84,7 @@ export class ConnectionState {
|
||||
this.pendingRequests.delete(reqId)
|
||||
}
|
||||
|
||||
if (verb === 'NOTICE') {
|
||||
if (verb === "NOTICE") {
|
||||
const [notice] = extra
|
||||
|
||||
this.cxn.emit(ConnectionEvent.Notice, notice)
|
||||
@@ -101,11 +101,11 @@ export class ConnectionState {
|
||||
}
|
||||
|
||||
for (const [reqId, req] of this.pendingRequests.entries()) {
|
||||
this.cxn.send(['REQ', reqId, ...req.filters])
|
||||
this.cxn.send(["REQ", reqId, ...req.filters])
|
||||
}
|
||||
|
||||
for (const [_, pub] of this.pendingPublishes.entries()) {
|
||||
this.cxn.send(['EVENT', pub.event])
|
||||
this.cxn.send(["EVENT", pub.event])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {Message} from './Socket'
|
||||
import type {Connection} from './Connection'
|
||||
import {ConnectionEvent} from './ConnectionEvent'
|
||||
import type {Message} from "./Socket.js"
|
||||
import type {Connection} from "./Connection.js"
|
||||
import {ConnectionEvent} from "./ConnectionEvent.js"
|
||||
|
||||
export class ConnectionStats {
|
||||
openCount = 0
|
||||
@@ -40,19 +40,19 @@ export class ConnectionStats {
|
||||
})
|
||||
|
||||
cxn.on(ConnectionEvent.Send, (cxn: Connection, [verb]: Message) => {
|
||||
if (verb === 'REQ') {
|
||||
if (verb === "REQ") {
|
||||
this.requestCount++
|
||||
this.lastRequest = Date.now()
|
||||
}
|
||||
|
||||
if (verb === 'EVENT') {
|
||||
if (verb === "EVENT") {
|
||||
this.publishCount++
|
||||
this.lastPublish = Date.now()
|
||||
}
|
||||
})
|
||||
|
||||
cxn.on(ConnectionEvent.Receive, (cxn: Connection, [verb, ...extra]: Message) => {
|
||||
if (verb === 'OK') {
|
||||
if (verb === "OK") {
|
||||
const pub = this.cxn.state.pendingPublishes.get(extra[0])
|
||||
|
||||
if (pub) {
|
||||
@@ -66,16 +66,16 @@ export class ConnectionStats {
|
||||
}
|
||||
}
|
||||
|
||||
if (verb === 'AUTH') {
|
||||
if (verb === "AUTH") {
|
||||
this.lastAuth = Date.now()
|
||||
}
|
||||
|
||||
if (verb === 'EVENT') {
|
||||
if (verb === "EVENT") {
|
||||
this.eventCount++
|
||||
this.lastEvent = Date.now()
|
||||
}
|
||||
|
||||
if (verb === 'EOSE') {
|
||||
if (verb === "EOSE") {
|
||||
const request = this.cxn.state.pendingRequests.get(extra[0])
|
||||
|
||||
// Only count the first eose
|
||||
@@ -85,13 +85,14 @@ export class ConnectionStats {
|
||||
}
|
||||
}
|
||||
|
||||
if (verb === 'NOTICE') {
|
||||
if (verb === "NOTICE") {
|
||||
this.noticeCount++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getRequestSpeed = () => this.eoseCount ? this.eoseTimer / this.eoseCount : 0
|
||||
getRequestSpeed = () => (this.eoseCount ? this.eoseTimer / this.eoseCount : 0)
|
||||
|
||||
getPublishSpeed = () => this.publishSuccessCount ? this.publishTimer / this.publishSuccessCount : 0
|
||||
getPublishSpeed = () =>
|
||||
this.publishSuccessCount ? this.publishTimer / this.publishSuccessCount : 0
|
||||
}
|
||||
|
||||
+24
-17
@@ -1,15 +1,21 @@
|
||||
import {ctx, randomInt, uniq, noop, always} from '@welshman/lib'
|
||||
import {LOCAL_RELAY_URL, matchFilters, unionFilters, isSignedEvent, hasValidSignature} from '@welshman/util'
|
||||
import type {StampedEvent, SignedEvent, Filter, TrustedEvent} from '@welshman/util'
|
||||
import {Pool} from "./Pool"
|
||||
import {Executor} from "./Executor"
|
||||
import {AuthMode} from "./ConnectionAuth"
|
||||
import {Relays} from "./target/Relays"
|
||||
import type {Subscription, RelaysAndFilters} from "./Subscribe"
|
||||
import {ctx, randomInt, uniq, noop, always} from "@welshman/lib"
|
||||
import {
|
||||
LOCAL_RELAY_URL,
|
||||
matchFilters,
|
||||
unionFilters,
|
||||
isSignedEvent,
|
||||
hasValidSignature,
|
||||
} from "@welshman/util"
|
||||
import type {StampedEvent, SignedEvent, Filter, TrustedEvent} from "@welshman/util"
|
||||
import {Pool} from "./Pool.js"
|
||||
import {Executor} from "./Executor.js"
|
||||
import {AuthMode} from "./ConnectionAuth.js"
|
||||
import {Relays} from "./target/Relays.js"
|
||||
import type {Subscription, RelaysAndFilters} from "./Subscribe.js"
|
||||
|
||||
export type NetContext = {
|
||||
pool: Pool
|
||||
authMode: AuthMode,
|
||||
authMode: AuthMode
|
||||
onEvent: (url: string, event: TrustedEvent) => void
|
||||
signEvent: (event: StampedEvent) => Promise<SignedEvent | undefined>
|
||||
getExecutor: (relays: string[]) => Executor
|
||||
@@ -20,13 +26,12 @@ export type NetContext = {
|
||||
}
|
||||
|
||||
export const defaultOptimizeSubscriptions = (subs: Subscription[]) =>
|
||||
uniq(subs.flatMap(sub => sub.request.relays || []))
|
||||
.map(relay => {
|
||||
const relaySubs = subs.filter(sub => sub.request.relays.includes(relay))
|
||||
const filters = unionFilters(relaySubs.flatMap(sub => sub.request.filters))
|
||||
uniq(subs.flatMap(sub => sub.request.relays || [])).map(relay => {
|
||||
const relaySubs = subs.filter(sub => sub.request.relays.includes(relay))
|
||||
const filters = unionFilters(relaySubs.flatMap(sub => sub.request.filters))
|
||||
|
||||
return {relays: [relay], filters}
|
||||
})
|
||||
return {relays: [relay], filters}
|
||||
})
|
||||
|
||||
export const eventValidationScores = new Map<string, number>()
|
||||
|
||||
@@ -56,8 +61,10 @@ export const getDefaultNetContext = (overrides: Partial<NetContext> = {}) => ({
|
||||
signEvent: noop,
|
||||
isDeleted: always(false),
|
||||
isValid: isEventValid,
|
||||
getExecutor: (relays: string[]) => new Executor(new Relays(relays.map((relay: string) => ctx.net.pool.get(relay)))),
|
||||
matchFilters: (url: string, filters: Filter[], event: TrustedEvent) => matchFilters(filters, event),
|
||||
getExecutor: (relays: string[]) =>
|
||||
new Executor(new Relays(relays.map((relay: string) => ctx.net.pool.get(relay)))),
|
||||
matchFilters: (url: string, filters: Filter[], event: TrustedEvent) =>
|
||||
matchFilters(filters, event),
|
||||
optimizeSubscriptions: defaultOptimizeSubscriptions,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {ctx, noop} from '@welshman/lib'
|
||||
import type {Emitter} from '@welshman/lib'
|
||||
import type {SignedEvent, TrustedEvent, Filter} from '@welshman/util'
|
||||
import type {Message} from './Socket'
|
||||
import type {Connection} from './Connection'
|
||||
import {Negentropy, NegentropyStorageVector} from './Negentropy'
|
||||
import {ctx, noop} from "@welshman/lib"
|
||||
import type {Emitter} from "@welshman/lib"
|
||||
import type {SignedEvent, TrustedEvent, Filter} from "@welshman/util"
|
||||
import type {Message} from "./Socket.js"
|
||||
import type {Connection} from "./Connection.js"
|
||||
import {Negentropy, NegentropyStorageVector} from "./Negentropy.js"
|
||||
|
||||
export type Target = Emitter & {
|
||||
connections: Connection[]
|
||||
@@ -21,21 +21,21 @@ type EoseCallback = (url: string) => void
|
||||
type CloseCallback = () => void
|
||||
type OkCallback = (url: string, id: string, ...extra: any[]) => void
|
||||
type ErrorCallback = (url: string, id: string, ...extra: any[]) => void
|
||||
type DiffMessageCallback = (url: string, {have, need}: {have: string[], need: string[]}) => void
|
||||
type SubscribeOpts = {onEvent?: EventCallback, onEose?: EoseCallback}
|
||||
type PublishOpts = {verb?: string, onOk?: OkCallback, onError?: ErrorCallback}
|
||||
type DiffOpts = {onError?: ErrorCallback, onMessage?: DiffMessageCallback, onClose?: CloseCallback}
|
||||
type DiffMessage = {have: string[]; need: string[]}
|
||||
type DiffMessageCallback = (url: string, {have, need}: DiffMessage) => void
|
||||
type SubscribeOpts = {onEvent?: EventCallback; onEose?: EoseCallback}
|
||||
type PublishOpts = {verb?: string; onOk?: OkCallback; onError?: ErrorCallback}
|
||||
type DiffOpts = {onError?: ErrorCallback; onMessage?: DiffMessageCallback; onClose?: CloseCallback}
|
||||
|
||||
const createSubId = (prefix: string) => [prefix, Math.random().toString().slice(2, 10)].join('-')
|
||||
const createSubId = (prefix: string) => [prefix, Math.random().toString().slice(2, 10)].join("-")
|
||||
|
||||
export class Executor {
|
||||
|
||||
constructor(readonly target: Target) {}
|
||||
|
||||
subscribe(filters: Filter[], {onEvent, onEose}: SubscribeOpts = {}) {
|
||||
let closed = false
|
||||
|
||||
const id = createSubId('REQ')
|
||||
const id = createSubId("REQ")
|
||||
|
||||
const eventListener = (url: string, subid: string, e: TrustedEvent) => {
|
||||
if (subid === id) {
|
||||
@@ -50,8 +50,8 @@ export class Executor {
|
||||
}
|
||||
}
|
||||
|
||||
this.target.on('EVENT', eventListener)
|
||||
this.target.on('EOSE', eoseListener)
|
||||
this.target.on("EVENT", eventListener)
|
||||
this.target.on("EOSE", eoseListener)
|
||||
this.target.send("REQ", id, ...filters)
|
||||
|
||||
return {
|
||||
@@ -59,15 +59,15 @@ export class Executor {
|
||||
if (closed) return
|
||||
|
||||
this.target.send("CLOSE", id).catch(noop)
|
||||
this.target.off('EVENT', eventListener)
|
||||
this.target.off('EOSE', eoseListener)
|
||||
this.target.off("EVENT", eventListener)
|
||||
this.target.off("EOSE", eoseListener)
|
||||
|
||||
closed = true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
publish(event: SignedEvent, {verb = 'EVENT', onOk, onError}: PublishOpts = {}) {
|
||||
publish(event: SignedEvent, {verb = "EVENT", onOk, onError}: PublishOpts = {}) {
|
||||
const okListener = (url: string, id: string, ok: boolean, message: string) => {
|
||||
if (id === event.id) {
|
||||
if (ok) {
|
||||
@@ -84,22 +84,22 @@ export class Executor {
|
||||
}
|
||||
}
|
||||
|
||||
this.target.on('OK', okListener)
|
||||
this.target.on('ERROR', errorListener)
|
||||
this.target.on("OK", okListener)
|
||||
this.target.on("ERROR", errorListener)
|
||||
this.target.send(verb, event)
|
||||
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
this.target.off('OK', okListener)
|
||||
this.target.off('ERROR', errorListener)
|
||||
}
|
||||
this.target.off("OK", okListener)
|
||||
this.target.off("ERROR", errorListener)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
diff(filter: Filter, events: TrustedEvent[], {onMessage, onError, onClose}: DiffOpts = {}) {
|
||||
let closed = false
|
||||
|
||||
const id = createSubId('NEG')
|
||||
const id = createSubId("NEG")
|
||||
const storage = new NegentropyStorageVector()
|
||||
const neg = new Negentropy(storage, 50_000)
|
||||
|
||||
@@ -116,7 +116,7 @@ export class Executor {
|
||||
onMessage?.(url, {have, need})
|
||||
|
||||
if (newMsg) {
|
||||
this.target.send('NEG-MSG', id, newMsg)
|
||||
this.target.send("NEG-MSG", id, newMsg)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
@@ -132,16 +132,16 @@ export class Executor {
|
||||
const close = () => {
|
||||
if (closed) return
|
||||
|
||||
this.target.send('NEG-CLOSE', id).catch(noop)
|
||||
this.target.off('NEG-MSG', msgListener)
|
||||
this.target.off('NEG-ERR', errListener)
|
||||
this.target.send("NEG-CLOSE", id).catch(noop)
|
||||
this.target.off("NEG-MSG", msgListener)
|
||||
this.target.off("NEG-ERR", errListener)
|
||||
|
||||
closed = true
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
this.target.on('NEG-MSG', msgListener)
|
||||
this.target.on('NEG-ERR', errListener)
|
||||
this.target.on("NEG-MSG", msgListener)
|
||||
this.target.on("NEG-ERR", errListener)
|
||||
|
||||
neg.initiate().then((msg: string) => {
|
||||
this.target.send("NEG-OPEN", id, filter, msg)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Emitter} from '@welshman/lib'
|
||||
import {Connection} from "./Connection"
|
||||
import {Emitter} from "@welshman/lib"
|
||||
import {Connection} from "./Connection.js"
|
||||
|
||||
export class Pool extends Emitter {
|
||||
data: Map<string, Connection>
|
||||
@@ -24,7 +24,7 @@ export class Pool extends Emitter {
|
||||
const newConnection = new Connection(url)
|
||||
|
||||
this.data.set(url, newConnection)
|
||||
this.emit('init', newConnection)
|
||||
this.emit("init", newConnection)
|
||||
|
||||
return newConnection
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {ctx, Emitter, now, randomId, defer} from '@welshman/lib'
|
||||
import type {Deferred} from '@welshman/lib'
|
||||
import {asSignedEvent} from '@welshman/util'
|
||||
import type {SignedEvent} from '@welshman/util'
|
||||
import {ctx, Emitter, now, randomId, defer} from "@welshman/lib"
|
||||
import type {Deferred} from "@welshman/lib"
|
||||
import {asSignedEvent} from "@welshman/util"
|
||||
import type {SignedEvent} from "@welshman/util"
|
||||
|
||||
export enum PublishStatus {
|
||||
Pending = "pending",
|
||||
@@ -34,8 +34,8 @@ export const makePublish = (request: PublishRequest) => {
|
||||
const id = randomId()
|
||||
const created_at = now()
|
||||
const emitter = new Emitter()
|
||||
const result: Publish['result'] = defer()
|
||||
const status: Publish['status'] = new Map()
|
||||
const result: Publish["result"] = defer()
|
||||
const status: Publish["status"] = new Map()
|
||||
|
||||
return {id, created_at, request, emitter, result, status}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export const publish = (request: PublishRequest) => {
|
||||
const timeout = setTimeout(() => abort(PublishStatus.Timeout), request.timeout || 10_000)
|
||||
|
||||
// If we have a signal, use it
|
||||
request.signal?.addEventListener('abort', () => abort(PublishStatus.Aborted))
|
||||
request.signal?.addEventListener("abort", () => abort(PublishStatus.Aborted))
|
||||
|
||||
// Delegate to our executor
|
||||
const executorSub = executor.publish(event, {
|
||||
@@ -96,4 +96,3 @@ export const publish = (request: PublishRequest) => {
|
||||
|
||||
return pub
|
||||
}
|
||||
|
||||
|
||||
+32
-36
@@ -1,30 +1,20 @@
|
||||
import WebSocket from "isomorphic-ws"
|
||||
import {Worker, sleep} from '@welshman/lib'
|
||||
import {ConnectionEvent} from './ConnectionEvent'
|
||||
import type {Connection} from './Connection'
|
||||
import {Worker, sleep} from "@welshman/lib"
|
||||
import {ConnectionEvent} from "./ConnectionEvent.js"
|
||||
import type {Connection} from "./Connection.js"
|
||||
|
||||
export type Message = [string, ...any[]]
|
||||
|
||||
export enum SocketStatus {
|
||||
New = 'new',
|
||||
Open = 'open',
|
||||
Opening = 'opening',
|
||||
Closing = 'closing',
|
||||
Closed = 'closed',
|
||||
Error = 'error',
|
||||
Invalid = 'invalid',
|
||||
New = "new",
|
||||
Open = "open",
|
||||
Opening = "opening",
|
||||
Closing = "closing",
|
||||
Closed = "closed",
|
||||
Error = "error",
|
||||
Invalid = "invalid",
|
||||
}
|
||||
|
||||
const {
|
||||
New,
|
||||
Open,
|
||||
Opening,
|
||||
Closing,
|
||||
Closed,
|
||||
Error,
|
||||
Invalid,
|
||||
} = SocketStatus
|
||||
|
||||
export class Socket {
|
||||
lastError = 0
|
||||
status = SocketStatus.New
|
||||
@@ -39,7 +29,7 @@ export class Socket {
|
||||
}
|
||||
|
||||
wait = async () => {
|
||||
while ([Opening, Closing].includes(this.status)) {
|
||||
while ([SocketStatus.Opening, SocketStatus.Closing].includes(this.status)) {
|
||||
await sleep(100)
|
||||
}
|
||||
}
|
||||
@@ -49,19 +39,19 @@ export class Socket {
|
||||
await this.wait()
|
||||
|
||||
// If the socket is closed, reset
|
||||
if (this.status === Closed) {
|
||||
this.status = New
|
||||
if (this.status === SocketStatus.Closed) {
|
||||
this.status = SocketStatus.New
|
||||
this.cxn.emit(ConnectionEvent.Reset)
|
||||
}
|
||||
|
||||
// If we're closed due to an error retry after a delay
|
||||
if (this.status === Error && Date.now() - this.lastError > 15_000) {
|
||||
this.status = New
|
||||
if (this.status === SocketStatus.Error && Date.now() - this.lastError > 15_000) {
|
||||
this.status = SocketStatus.New
|
||||
this.cxn.emit(ConnectionEvent.Reset)
|
||||
}
|
||||
|
||||
// If the socket is new, connect
|
||||
if (this.status === New) {
|
||||
if (this.status === SocketStatus.New) {
|
||||
this.#init()
|
||||
}
|
||||
|
||||
@@ -84,6 +74,10 @@ export class Socket {
|
||||
send = async (message: Message) => {
|
||||
await this.open()
|
||||
|
||||
if (!this.ws) {
|
||||
throw new Error(`No websocket available when sending to ${this.cxn.url}`)
|
||||
}
|
||||
|
||||
this.cxn.emit(ConnectionEvent.Send, message)
|
||||
this.ws.send(JSON.stringify(message))
|
||||
}
|
||||
@@ -91,42 +85,44 @@ export class Socket {
|
||||
#init = () => {
|
||||
try {
|
||||
this.ws = new WebSocket(this.cxn.url)
|
||||
this.status = Opening
|
||||
this.status = SocketStatus.Opening
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.status = Open
|
||||
this.status = SocketStatus.Open
|
||||
this.cxn.emit(ConnectionEvent.Open)
|
||||
}
|
||||
|
||||
this.ws.onerror = () => {
|
||||
this.status = Error
|
||||
this.status = SocketStatus.Error
|
||||
this.lastError = Date.now()
|
||||
this.cxn.emit(ConnectionEvent.Error)
|
||||
}
|
||||
|
||||
this.ws.onclose = () => {
|
||||
if (this.status !== Error) {
|
||||
this.status = Closed
|
||||
if (this.status !== SocketStatus.Error) {
|
||||
this.status = SocketStatus.Closed
|
||||
}
|
||||
|
||||
this.cxn.emit(ConnectionEvent.Close)
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event: {data: string}) => {
|
||||
this.ws.onmessage = (event: any) => {
|
||||
const data = event.data as string
|
||||
|
||||
try {
|
||||
const message = JSON.parse(event.data as string)
|
||||
const message = JSON.parse(data)
|
||||
|
||||
if (Array.isArray(message)) {
|
||||
this.worker.push(message as Message)
|
||||
} else {
|
||||
this.cxn.emit(ConnectionEvent.InvalidMessage, event.data)
|
||||
this.cxn.emit(ConnectionEvent.InvalidMessage, data)
|
||||
}
|
||||
} catch (e) {
|
||||
this.cxn.emit(ConnectionEvent.InvalidMessage, event.data)
|
||||
this.cxn.emit(ConnectionEvent.InvalidMessage, data)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.status = Invalid
|
||||
this.status = SocketStatus.Invalid
|
||||
this.cxn.emit(ConnectionEvent.InvalidUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import {ctx, Emitter, max, chunk, randomId, once, groupBy, uniq} from '@welshman/lib'
|
||||
import {LOCAL_RELAY_URL, matchFilters, normalizeRelayUrl, unionFilters, TrustedEvent} from '@welshman/util'
|
||||
import type {Filter} from '@welshman/util'
|
||||
import {Tracker} from "./Tracker"
|
||||
import {Connection} from './Connection'
|
||||
import {ConnectionEvent} from './ConnectionEvent'
|
||||
import {ctx, Emitter, max, chunk, randomId, once, groupBy, uniq} from "@welshman/lib"
|
||||
import {
|
||||
LOCAL_RELAY_URL,
|
||||
matchFilters,
|
||||
normalizeRelayUrl,
|
||||
unionFilters,
|
||||
TrustedEvent,
|
||||
} from "@welshman/util"
|
||||
import type {Filter} from "@welshman/util"
|
||||
import {Tracker} from "./Tracker.js"
|
||||
import {Connection} from "./Connection.js"
|
||||
import {ConnectionEvent} from "./ConnectionEvent.js"
|
||||
|
||||
// `subscribe` is a super function that handles batching subscriptions by merging
|
||||
// them based on parameters (filters and subscribe opts), then splits them by relay.
|
||||
@@ -71,9 +77,9 @@ export const calculateSubscriptionGroup = (sub: Subscription) => {
|
||||
|
||||
if (sub.request.timeout) parts.push(`timeout:${sub.request.timeout}`)
|
||||
if (sub.request.authTimeout) parts.push(`authTimeout:${sub.request.authTimeout}`)
|
||||
if (sub.request.closeOnEose) parts.push('closeOnEose')
|
||||
if (sub.request.closeOnEose) parts.push("closeOnEose")
|
||||
|
||||
return parts.join('|')
|
||||
return parts.join("|")
|
||||
}
|
||||
|
||||
export const mergeSubscriptions = (subs: Subscription[]) => {
|
||||
@@ -85,7 +91,7 @@ export const mergeSubscriptions = (subs: Subscription[]) => {
|
||||
closeOnEose: subs.every(sub => sub.request.closeOnEose),
|
||||
})
|
||||
|
||||
mergedSub.controller.signal.addEventListener('abort', () => {
|
||||
mergedSub.controller.signal.addEventListener("abort", () => {
|
||||
for (const sub of subs) {
|
||||
sub.close()
|
||||
}
|
||||
@@ -130,93 +136,92 @@ export const mergeSubscriptions = (subs: Subscription[]) => {
|
||||
}
|
||||
|
||||
export const optimizeSubscriptions = (subs: Subscription[]) => {
|
||||
return Array.from(groupBy(calculateSubscriptionGroup, subs).values())
|
||||
.flatMap(group => {
|
||||
const timeout = max(group.map(sub => sub.request.timeout || 0))
|
||||
const authTimeout = max(group.map(sub => sub.request.authTimeout || 0))
|
||||
const closeOnEose = group.every(sub => sub.request.closeOnEose)
|
||||
const completedSubs = new Set<string>()
|
||||
const abortedSubs = new Set<string>()
|
||||
const closedSubs = new Set<string>()
|
||||
const eosedSubs = new Set<string>()
|
||||
const sentSubs = new Set<string>()
|
||||
const mergedSubs: Subscription[] = []
|
||||
return Array.from(groupBy(calculateSubscriptionGroup, subs).values()).flatMap(group => {
|
||||
const timeout = max(group.map(sub => sub.request.timeout || 0))
|
||||
const authTimeout = max(group.map(sub => sub.request.authTimeout || 0))
|
||||
const closeOnEose = group.every(sub => sub.request.closeOnEose)
|
||||
const completedSubs = new Set<string>()
|
||||
const abortedSubs = new Set<string>()
|
||||
const closedSubs = new Set<string>()
|
||||
const eosedSubs = new Set<string>()
|
||||
const sentSubs = new Set<string>()
|
||||
const mergedSubs: Subscription[] = []
|
||||
|
||||
for (const {relays, filters} of ctx.net.optimizeSubscriptions(group)) {
|
||||
for (const filter of filters) {
|
||||
const mergedSub = makeSubscription({
|
||||
filters: [filter],
|
||||
relays,
|
||||
timeout,
|
||||
authTimeout,
|
||||
closeOnEose
|
||||
})
|
||||
for (const {relays, filters} of ctx.net.optimizeSubscriptions(group)) {
|
||||
for (const filter of filters) {
|
||||
const mergedSub = makeSubscription({
|
||||
filters: [filter],
|
||||
relays,
|
||||
timeout,
|
||||
authTimeout,
|
||||
closeOnEose,
|
||||
})
|
||||
|
||||
for (const {id, controller, request} of group) {
|
||||
const onAbort = () => {
|
||||
abortedSubs.add(id)
|
||||
for (const {id, controller, request} of group) {
|
||||
const onAbort = () => {
|
||||
abortedSubs.add(id)
|
||||
|
||||
if (abortedSubs.size === group.length) {
|
||||
mergedSub.close()
|
||||
}
|
||||
if (abortedSubs.size === group.length) {
|
||||
mergedSub.close()
|
||||
}
|
||||
|
||||
request.signal?.addEventListener('abort', onAbort)
|
||||
controller.signal.addEventListener('abort', onAbort)
|
||||
}
|
||||
|
||||
mergedSub.emitter.on(SubscriptionEvent.Event, (url: string, event: TrustedEvent) => {
|
||||
request.signal?.addEventListener("abort", onAbort)
|
||||
controller.signal.addEventListener("abort", onAbort)
|
||||
}
|
||||
|
||||
mergedSub.emitter.on(SubscriptionEvent.Event, (url: string, event: TrustedEvent) => {
|
||||
for (const sub of group) {
|
||||
if (matchFilters(sub.request.filters, event) && !sub.tracker.track(event.id, url)) {
|
||||
sub.emitter.emit(SubscriptionEvent.Event, url, event)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Pass events back to caller
|
||||
const propagateEvent = (type: SubscriptionEvent) =>
|
||||
mergedSub.emitter.on(type, (url: string, event: TrustedEvent) => {
|
||||
for (const sub of group) {
|
||||
if (matchFilters(sub.request.filters, event) && !sub.tracker.track(event.id, url)) {
|
||||
sub.emitter.emit(SubscriptionEvent.Event, url, event)
|
||||
if (matchFilters(sub.request.filters, event)) {
|
||||
sub.emitter.emit(type, url, event)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Pass events back to caller
|
||||
const propagateEvent = (type: SubscriptionEvent) =>
|
||||
mergedSub.emitter.on(type, (url: string, event: TrustedEvent) => {
|
||||
propagateEvent(SubscriptionEvent.Duplicate)
|
||||
propagateEvent(SubscriptionEvent.DeletedEvent)
|
||||
propagateEvent(SubscriptionEvent.Invalid)
|
||||
|
||||
const propagateFinality = (type: SubscriptionEvent, subIds: Set<string>) =>
|
||||
mergedSub.emitter.on(type, (...args: any[]) => {
|
||||
subIds.add(mergedSub.id)
|
||||
|
||||
// Wait for all subscriptions to complete before reporting finality to the caller.
|
||||
// This is sub-optimal, but because we're outsourcing filter/relay optimization
|
||||
// we can't make any assumptions about which caller subscriptions have completed
|
||||
// at any given time.
|
||||
if (subIds.size === mergedSubs.length) {
|
||||
for (const sub of group) {
|
||||
if (matchFilters(sub.request.filters, event)) {
|
||||
sub.emitter.emit(type, url, event)
|
||||
}
|
||||
sub.emitter.emit(type, ...args)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
propagateEvent(SubscriptionEvent.Duplicate)
|
||||
propagateEvent(SubscriptionEvent.DeletedEvent)
|
||||
propagateEvent(SubscriptionEvent.Invalid)
|
||||
if (type === SubscriptionEvent.Complete) {
|
||||
mergedSub.emitter.removeAllListeners()
|
||||
}
|
||||
})
|
||||
|
||||
const propagateFinality = (type: SubscriptionEvent, subIds: Set<string>) =>
|
||||
mergedSub.emitter.on(type, (...args: any[]) => {
|
||||
subIds.add(mergedSub.id)
|
||||
propagateFinality(SubscriptionEvent.Send, sentSubs)
|
||||
propagateFinality(SubscriptionEvent.Eose, eosedSubs)
|
||||
propagateFinality(SubscriptionEvent.Close, closedSubs)
|
||||
propagateFinality(SubscriptionEvent.Complete, completedSubs)
|
||||
|
||||
// Wait for all subscriptions to complete before reporting finality to the caller.
|
||||
// This is sub-optimal, but because we're outsourcing filter/relay optimization
|
||||
// we can't make any assumptions about which caller subscriptions have completed
|
||||
// at any given time.
|
||||
if (subIds.size === mergedSubs.length) {
|
||||
for (const sub of group) {
|
||||
sub.emitter.emit(type, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
if (type === SubscriptionEvent.Complete) {
|
||||
mergedSub.emitter.removeAllListeners()
|
||||
}
|
||||
})
|
||||
|
||||
propagateFinality(SubscriptionEvent.Send, sentSubs)
|
||||
propagateFinality(SubscriptionEvent.Eose, eosedSubs)
|
||||
propagateFinality(SubscriptionEvent.Close, closedSubs)
|
||||
propagateFinality(SubscriptionEvent.Complete, completedSubs)
|
||||
|
||||
mergedSubs.push(mergedSub)
|
||||
}
|
||||
mergedSubs.push(mergedSub)
|
||||
}
|
||||
}
|
||||
|
||||
return mergedSubs
|
||||
})
|
||||
return mergedSubs
|
||||
})
|
||||
}
|
||||
|
||||
const _executeSubscription = (sub: Subscription) => {
|
||||
@@ -267,19 +272,17 @@ const _executeSubscription = (sub: Subscription) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onEose = (url: string) =>
|
||||
emitter.emit(SubscriptionEvent.Eose, url)
|
||||
const onEose = (url: string) => emitter.emit(SubscriptionEvent.Eose, url)
|
||||
|
||||
const onClose = (connection: Connection) =>
|
||||
emitter.emit(SubscriptionEvent.Close, connection.url)
|
||||
const onClose = (connection: Connection) => emitter.emit(SubscriptionEvent.Close, connection.url)
|
||||
|
||||
const onComplete = once(() => emitter.emit(SubscriptionEvent.Complete))
|
||||
|
||||
// Listen for abort via caller signal
|
||||
signal?.addEventListener('abort', onComplete)
|
||||
signal?.addEventListener("abort", onComplete)
|
||||
|
||||
// Listen for abort via our own internal signal
|
||||
controller.signal.addEventListener('abort', onComplete)
|
||||
controller.signal.addEventListener("abort", onComplete)
|
||||
|
||||
// If we have a timeout, complete the subscription automatically
|
||||
if (timeout) setTimeout(onComplete, timeout + authTimeout)
|
||||
@@ -297,7 +300,7 @@ const _executeSubscription = (sub: Subscription) => {
|
||||
if (authTimeout) {
|
||||
await connection.auth.attempt(authTimeout)
|
||||
}
|
||||
})
|
||||
}),
|
||||
).then(() => {
|
||||
// If we send too many filters in a request relays will refuse to respond. REQs are rate
|
||||
// limited client-side by Connection, so this will throttle concurrent requests.
|
||||
@@ -333,9 +336,7 @@ export const executeSubscriptionBatched = (() => {
|
||||
|
||||
return (sub: Subscription) => {
|
||||
subs.push(sub)
|
||||
timeouts.push(
|
||||
setTimeout(executeAll, Math.max(16, sub.request.delay!)) as unknown as number
|
||||
)
|
||||
timeouts.push(setTimeout(executeAll, Math.max(16, sub.request.delay!)) as unknown as number)
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -346,7 +347,13 @@ export type SubscribeRequestWithHandlers = SubscribeRequest & {
|
||||
onComplete?: () => void
|
||||
}
|
||||
|
||||
export const subscribe = ({onEvent, onEose, onClose, onComplete, ...request}: SubscribeRequestWithHandlers) => {
|
||||
export const subscribe = ({
|
||||
onEvent,
|
||||
onEose,
|
||||
onClose,
|
||||
onComplete,
|
||||
...request
|
||||
}: SubscribeRequestWithHandlers) => {
|
||||
const sub: Subscription = makeSubscription({delay: 50, ...request})
|
||||
|
||||
for (const relay of request.relays) {
|
||||
@@ -362,7 +369,8 @@ export const subscribe = ({onEvent, onEose, onClose, onComplete, ...request}: Su
|
||||
}
|
||||
|
||||
// Signature for onEvent is different from emitter signature for historical reasons and convenience
|
||||
if (onEvent) sub.emitter.on(SubscriptionEvent.Event, (url: string, event: TrustedEvent) => onEvent(event))
|
||||
if (onEvent)
|
||||
sub.emitter.on(SubscriptionEvent.Event, (url: string, event: TrustedEvent) => onEvent(event))
|
||||
if (onEose) sub.emitter.on(SubscriptionEvent.Eose, onEose)
|
||||
if (onClose) sub.emitter.on(SubscriptionEvent.Close, onClose)
|
||||
if (onComplete) sub.emitter.on(SubscriptionEvent.Complete, onComplete)
|
||||
|
||||
+33
-30
@@ -1,7 +1,7 @@
|
||||
import {ctx, assoc, lt, groupBy, now, pushToMapKey, inc, flatten, chunk} from '@welshman/lib'
|
||||
import type {SignedEvent, TrustedEvent, Filter} from '@welshman/util'
|
||||
import {subscribe} from './Subscribe'
|
||||
import {publish} from './Publish'
|
||||
import {ctx, assoc, lt, groupBy, now, pushToMapKey, inc, flatten, chunk} from "@welshman/lib"
|
||||
import type {SignedEvent, TrustedEvent, Filter} from "@welshman/util"
|
||||
import {subscribe} from "./Subscribe.js"
|
||||
import {publish} from "./Publish.js"
|
||||
|
||||
export type DiffOpts = {
|
||||
relays: string[]
|
||||
@@ -19,11 +19,11 @@ export const diff = async ({relays, filters, events}: DiffOpts) => {
|
||||
const have = new Set<string>()
|
||||
const need = new Set<string>()
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
executor.diff(filter, events, {
|
||||
onClose: resolve,
|
||||
onError: (_, message) => reject(message),
|
||||
onMessage: (_, message) => {
|
||||
onError: (url, message) => reject(message),
|
||||
onMessage: (url, message) => {
|
||||
for (const id of message.have) {
|
||||
have.add(id)
|
||||
}
|
||||
@@ -36,29 +36,28 @@ export const diff = async ({relays, filters, events}: DiffOpts) => {
|
||||
})
|
||||
|
||||
return {relay, have, need}
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return Array.from(groupBy(diff => diff.relay, diffs).entries())
|
||||
.map(([relay, diffs]) => {
|
||||
const have = new Set<string>()
|
||||
const need = new Set<string>()
|
||||
return Array.from(groupBy(diff => diff.relay, diffs).entries()).map(([relay, diffs]) => {
|
||||
const have = new Set<string>()
|
||||
const need = new Set<string>()
|
||||
|
||||
for (const diff of diffs) {
|
||||
for (const id of diff.have) {
|
||||
have.add(id)
|
||||
}
|
||||
|
||||
for (const id of diff.need) {
|
||||
need.add(id)
|
||||
}
|
||||
for (const diff of diffs) {
|
||||
for (const id of diff.have) {
|
||||
have.add(id)
|
||||
}
|
||||
|
||||
return {relay, have: Array.from(have), need: Array.from(need)}
|
||||
})
|
||||
for (const id of diff.need) {
|
||||
need.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
return {relay, have: Array.from(have), need: Array.from(need)}
|
||||
})
|
||||
}
|
||||
|
||||
export type PullOpts = {
|
||||
@@ -103,9 +102,9 @@ export const pull = async ({relays, filters, events, onEvent}: PullOpts) => {
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -133,7 +132,7 @@ export const push = async ({relays, filters, events}: PushOpts) => {
|
||||
if (relays) {
|
||||
await publish({event, relays}).result
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -156,7 +155,11 @@ export type PullWithoutNegentropyOpts = {
|
||||
onEvent?: (event: TrustedEvent) => void
|
||||
}
|
||||
|
||||
export const pullWithoutNegentropy = async ({relays, filters, onEvent}: PullWithoutNegentropyOpts) => {
|
||||
export const pullWithoutNegentropy = async ({
|
||||
relays,
|
||||
filters,
|
||||
onEvent,
|
||||
}: PullWithoutNegentropyOpts) => {
|
||||
let done = false
|
||||
let until = now() + 30
|
||||
|
||||
@@ -168,7 +171,7 @@ export const pullWithoutNegentropy = async ({relays, filters, onEvent}: PullWith
|
||||
await new Promise<void>(resolve => {
|
||||
subscribe({
|
||||
relays,
|
||||
filters: filters.filter(f => lt(f.since, until)).map(assoc('until', until)),
|
||||
filters: filters.filter(f => lt(f.since, until)).map(assoc("until", until)),
|
||||
closeOnEose: true,
|
||||
onComplete: () => {
|
||||
done = !anyResults
|
||||
@@ -196,7 +199,7 @@ export const pushWithoutNegentropy = ({relays, events}: PushWithoutNegentropyOpt
|
||||
Promise.all(
|
||||
events.map(async event => {
|
||||
await publish({event, relays}).result
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const syncWithoutNegentropy = async (opts: SyncOpts) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Emitter, addToMapKey} from '@welshman/lib'
|
||||
import {Emitter, addToMapKey} from "@welshman/lib"
|
||||
|
||||
export class Tracker extends Emitter {
|
||||
relaysById = new Map<string, Set<string>>()
|
||||
@@ -36,7 +36,7 @@ export class Tracker extends Emitter {
|
||||
this.relaysById.set(eventId, relays)
|
||||
this.idsByRelay.set(eventId, relays)
|
||||
|
||||
this.emit('update')
|
||||
this.emit("update")
|
||||
}
|
||||
|
||||
removeRelay = (eventId: string, relay: string) => {
|
||||
@@ -45,7 +45,7 @@ export class Tracker extends Emitter {
|
||||
|
||||
if (!didDeleteRelay && !didDeleteId) return
|
||||
|
||||
this.emit('update')
|
||||
this.emit("update")
|
||||
}
|
||||
|
||||
track = (eventId: string, relay: string) => {
|
||||
@@ -62,7 +62,7 @@ export class Tracker extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
load = (relaysById: Tracker['relaysById']) => {
|
||||
load = (relaysById: Tracker["relaysById"]) => {
|
||||
this.relaysById.clear()
|
||||
this.idsByRelay.clear()
|
||||
|
||||
@@ -73,13 +73,13 @@ export class Tracker extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('update')
|
||||
this.emit("update")
|
||||
}
|
||||
|
||||
clear = () => {
|
||||
this.relaysById.clear()
|
||||
this.idsByRelay.clear()
|
||||
|
||||
this.emit('update')
|
||||
this.emit("update")
|
||||
}
|
||||
}
|
||||
|
||||
+27
-19
@@ -1,19 +1,27 @@
|
||||
export * from "./Connection"
|
||||
export * from "./ConnectionAuth"
|
||||
export * from "./ConnectionEvent"
|
||||
export * from "./ConnectionSender"
|
||||
export * from "./ConnectionState"
|
||||
export * from "./ConnectionStats"
|
||||
export * from "./Context"
|
||||
export * from "./Executor"
|
||||
export * from "./Pool"
|
||||
export * from "./Publish"
|
||||
export * from "./Socket"
|
||||
export * from "./Subscribe"
|
||||
export * from "./Sync"
|
||||
export * from "./Tracker"
|
||||
export * from "./target/Echo"
|
||||
export * from "./target/Multi"
|
||||
export * from "./target/Relay"
|
||||
export * from "./target/Relays"
|
||||
export * from "./target/Local"
|
||||
export * from "./Connection.js"
|
||||
export * from "./ConnectionAuth.js"
|
||||
export * from "./ConnectionEvent.js"
|
||||
export * from "./ConnectionSender.js"
|
||||
export * from "./ConnectionState.js"
|
||||
export * from "./ConnectionStats.js"
|
||||
export * from "./Context.js"
|
||||
export * from "./Executor.js"
|
||||
export * from "./Pool.js"
|
||||
export * from "./Publish.js"
|
||||
export * from "./Socket.js"
|
||||
export * from "./Subscribe.js"
|
||||
export * from "./Sync.js"
|
||||
export * from "./Tracker.js"
|
||||
export * from "./target/Echo.js"
|
||||
export * from "./target/Multi.js"
|
||||
export * from "./target/Relay.js"
|
||||
export * from "./target/Relays.js"
|
||||
export * from "./target/Local.js"
|
||||
|
||||
import type {NetContext} from "./Context.js"
|
||||
|
||||
declare module "@welshman/lib" {
|
||||
interface Context {
|
||||
net: NetContext
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Emitter} from '@welshman/lib'
|
||||
import type {Message} from '../Socket'
|
||||
import {Emitter} from "@welshman/lib"
|
||||
import type {Message} from "../Socket.js"
|
||||
|
||||
export class Echo extends Emitter {
|
||||
get connections() {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {Emitter} from '@welshman/lib'
|
||||
import {Relay, LOCAL_RELAY_URL} from '@welshman/util'
|
||||
import type {Message} from '../Socket'
|
||||
import {Emitter} from "@welshman/lib"
|
||||
import {Relay, LOCAL_RELAY_URL} from "@welshman/util"
|
||||
import type {Message} from "../Socket.js"
|
||||
|
||||
export class Local extends Emitter {
|
||||
constructor(readonly relay: Relay) {
|
||||
super()
|
||||
|
||||
relay.on('*', this.onMessage)
|
||||
relay.on("*", this.onMessage)
|
||||
}
|
||||
|
||||
get connections() {
|
||||
@@ -25,6 +25,6 @@ export class Local extends Emitter {
|
||||
|
||||
cleanup = () => {
|
||||
this.removeAllListeners()
|
||||
this.relay.off('*', this.onMessage)
|
||||
this.relay.off("*", this.onMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {Emitter} from '@welshman/lib'
|
||||
import type {Message} from '../Socket'
|
||||
import type {Target} from '../Executor'
|
||||
import {Emitter} from "@welshman/lib"
|
||||
import type {Message} from "../Socket.js"
|
||||
import type {Target} from "../Executor.js"
|
||||
|
||||
export class Multi extends Emitter {
|
||||
constructor(readonly targets: Target[]) {
|
||||
super()
|
||||
|
||||
targets.forEach(t => {
|
||||
t.on('*', (verb, ...args) => this.emit(verb, ...args))
|
||||
t.on("*", (verb, ...args) => this.emit(verb, ...args))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Emitter} from '@welshman/lib'
|
||||
import {ConnectionEvent} from '../ConnectionEvent'
|
||||
import type {Message} from '../Socket'
|
||||
import type {Connection} from '../Connection'
|
||||
import {Emitter} from "@welshman/lib"
|
||||
import {ConnectionEvent} from "../ConnectionEvent.js"
|
||||
import type {Message} from "../Socket.js"
|
||||
import type {Connection} from "../Connection.js"
|
||||
|
||||
export class Relay extends Emitter {
|
||||
constructor(readonly connection: Connection) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Emitter} from '@welshman/lib'
|
||||
import type {Message} from '../Socket'
|
||||
import type {Connection} from '../Connection'
|
||||
import {ConnectionEvent} from '../ConnectionEvent'
|
||||
import {Emitter} from "@welshman/lib"
|
||||
import type {Message} from "../Socket.js"
|
||||
import type {Connection} from "../Connection.js"
|
||||
import {ConnectionEvent} from "../ConnectionEvent.js"
|
||||
|
||||
export class Relays extends Emitter {
|
||||
constructor(readonly connections: Connection[]) {
|
||||
@@ -23,7 +23,7 @@ export class Relays extends Emitter {
|
||||
cleanup = () => {
|
||||
this.removeAllListeners()
|
||||
this.connections.forEach(connection => {
|
||||
connection.off('receive', this.onMessage)
|
||||
connection.off("receive", this.onMessage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-8
@@ -1,8 +0,0 @@
|
||||
import type {NetContext} from './Context'
|
||||
|
||||
|
||||
declare module "@welshman/lib" {
|
||||
interface Context {
|
||||
net: NetContext
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"targets": [
|
||||
{"extname": ".cjs", "module": "commonjs"},
|
||||
{"extname": ".mjs", "module": "esnext", "moduleResolution": "node"}
|
||||
],
|
||||
"projects": ["tsconfig.json"]
|
||||
}
|
||||
@@ -3,9 +3,12 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "build",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"lib": ["esnext", "dom"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,26 +11,26 @@
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.x"
|
||||
},
|
||||
"types": "./build/src/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/src/index.d.ts",
|
||||
"import": "./build/src/index.mjs",
|
||||
"require": "./build/src/index.cjs"
|
||||
"import": "./build/src/index.js",
|
||||
"require": "./build/src/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"pub": "npm run lint && npm run build && npm publish",
|
||||
"build": "gts clean && tsc-multi",
|
||||
"build": "gts clean && tsc",
|
||||
"lint": "gts lint",
|
||||
"fix": "gts fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gts": "^5.0.1",
|
||||
"tsc-multi": "^1.1.0",
|
||||
"typescript": "~5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@noble/curves": "^1.7.0",
|
||||
"@noble/hashes": "^1.6.1",
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@welshman/net": "~0.0.41",
|
||||
"@welshman/util": "~0.0.50",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from './util'
|
||||
export * from './nip59'
|
||||
export * from './signers/nip01'
|
||||
export * from './signers/nip07'
|
||||
export * from './signers/nip46'
|
||||
export * from './signers/nip55'
|
||||
export * from "./util.js"
|
||||
export * from "./nip59.js"
|
||||
export * from "./signers/nip01.js"
|
||||
export * from "./signers/nip07.js"
|
||||
export * from "./signers/nip46.js"
|
||||
export * from "./signers/nip55.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {UnwrappedEvent, SignedEvent, HashedEvent, StampedEvent, WRAP, SEAL} from '@welshman/util'
|
||||
import {own, hash, decrypt, ISigner} from './util'
|
||||
import {Nip01Signer} from './signers/nip01'
|
||||
import {UnwrappedEvent, SignedEvent, HashedEvent, StampedEvent, WRAP, SEAL} from "@welshman/util"
|
||||
import {own, hash, decrypt, ISigner} from "./util.js"
|
||||
import {Nip01Signer} from "./signers/nip01.js"
|
||||
|
||||
export const seen = new Map<string, UnwrappedEvent | Error>()
|
||||
|
||||
@@ -11,24 +11,39 @@ export const getRumor = async (signer: ISigner, template: StampedEvent) =>
|
||||
hash(own(template, await signer.getPubkey()))
|
||||
|
||||
export const getSeal = async (signer: ISigner, pubkey: string, rumor: HashedEvent) =>
|
||||
signer.sign(hash({
|
||||
kind: SEAL,
|
||||
pubkey: await signer.getPubkey(),
|
||||
content: await signer.nip44.encrypt(pubkey, JSON.stringify(rumor)),
|
||||
created_at: now(5),
|
||||
tags: [],
|
||||
}))
|
||||
signer.sign(
|
||||
hash({
|
||||
kind: SEAL,
|
||||
pubkey: await signer.getPubkey(),
|
||||
content: await signer.nip44.encrypt(pubkey, JSON.stringify(rumor)),
|
||||
created_at: now(5),
|
||||
tags: [],
|
||||
}),
|
||||
)
|
||||
|
||||
export const getWrap = async (wrapper: ISigner, pubkey: string, seal: SignedEvent, tags: string[][]) =>
|
||||
wrapper.sign(hash({
|
||||
kind: WRAP,
|
||||
pubkey: await wrapper.getPubkey(),
|
||||
content: await wrapper.nip44.encrypt(pubkey, JSON.stringify(seal)),
|
||||
created_at: now(5),
|
||||
tags: [...tags, ["p", pubkey]],
|
||||
}))
|
||||
export const getWrap = async (
|
||||
wrapper: ISigner,
|
||||
pubkey: string,
|
||||
seal: SignedEvent,
|
||||
tags: string[][],
|
||||
) =>
|
||||
wrapper.sign(
|
||||
hash({
|
||||
kind: WRAP,
|
||||
pubkey: await wrapper.getPubkey(),
|
||||
content: await wrapper.nip44.encrypt(pubkey, JSON.stringify(seal)),
|
||||
created_at: now(5),
|
||||
tags: [...tags, ["p", pubkey]],
|
||||
}),
|
||||
)
|
||||
|
||||
export const wrap = async (signer: ISigner, wrapper: ISigner, pubkey: string, template: StampedEvent, tags: string[][] = []) => {
|
||||
export const wrap = async (
|
||||
signer: ISigner,
|
||||
wrapper: ISigner,
|
||||
pubkey: string,
|
||||
template: StampedEvent,
|
||||
tags: string[][] = [],
|
||||
) => {
|
||||
const rumor = await getRumor(signer, template)
|
||||
const seal = await getSeal(signer, pubkey, rumor)
|
||||
const wrap = await getWrap(wrapper, pubkey, seal, tags)
|
||||
@@ -69,7 +84,10 @@ export const unwrap = async (signer: ISigner, wrap: SignedEvent) => {
|
||||
// wrapping a single user signer and omit the wrapper signer argument to wrap, while still
|
||||
// making it possible to pass a wrapper signer if desired.
|
||||
export class Nip59 {
|
||||
constructor(private signer: ISigner, private wrapper?: ISigner) {}
|
||||
constructor(
|
||||
private signer: ISigner,
|
||||
private wrapper?: ISigner,
|
||||
) {}
|
||||
|
||||
static fromSigner = (signer: ISigner) => new Nip59(signer)
|
||||
|
||||
@@ -80,6 +98,5 @@ export class Nip59 {
|
||||
wrap = (pubkey: string, template: StampedEvent, tags: string[][] = []) =>
|
||||
wrap(this.signer, this.wrapper || Nip01Signer.ephemeral(), pubkey, template, tags)
|
||||
|
||||
unwrap = (event: SignedEvent) =>
|
||||
unwrap(this.signer, event)
|
||||
unwrap = (event: SignedEvent) => unwrap(this.signer, event)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {StampedEvent} from '@welshman/util'
|
||||
import {nip04, nip44, own, hash, sign, getPubkey, ISigner, makeSecret} from "../util"
|
||||
import {StampedEvent} from "@welshman/util"
|
||||
import {nip04, nip44, own, hash, sign, getPubkey, ISigner, makeSecret} from "../util.js"
|
||||
|
||||
export class Nip01Signer implements ISigner {
|
||||
#pubkey: string
|
||||
@@ -17,16 +17,12 @@ export class Nip01Signer implements ISigner {
|
||||
sign = async (event: StampedEvent) => sign(hash(own(event, this.#pubkey)), this.secret)
|
||||
|
||||
nip04 = {
|
||||
encrypt: async (pubkey: string, message: string) =>
|
||||
nip04.encrypt(pubkey, this.secret, message),
|
||||
decrypt: async (pubkey: string, message: string) =>
|
||||
nip04.decrypt(pubkey, this.secret, message),
|
||||
encrypt: async (pubkey: string, message: string) => nip04.encrypt(pubkey, this.secret, message),
|
||||
decrypt: async (pubkey: string, message: string) => nip04.decrypt(pubkey, this.secret, message),
|
||||
}
|
||||
|
||||
nip44 = {
|
||||
encrypt: async (pubkey: string, message: string) =>
|
||||
nip44.encrypt(pubkey, this.secret, message),
|
||||
decrypt: async (pubkey: string, message: string) =>
|
||||
nip44.decrypt(pubkey, this.secret, message),
|
||||
encrypt: async (pubkey: string, message: string) => nip44.encrypt(pubkey, this.secret, message),
|
||||
decrypt: async (pubkey: string, message: string) => nip44.decrypt(pubkey, this.secret, message),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {StampedEvent} from '@welshman/util'
|
||||
import {hash, own, Sign, ISigner, EncryptionImplementation} from '../util'
|
||||
import {StampedEvent} from "@welshman/util"
|
||||
import {hash, own, Sign, ISigner, EncryptionImplementation} from "../util.js"
|
||||
|
||||
export type Nip07 = {
|
||||
signEvent: Sign
|
||||
@@ -23,7 +23,10 @@ export class Nip07Signer implements ISigner {
|
||||
})
|
||||
|
||||
// Recover from errors
|
||||
this.#lock = promise.then(() => undefined, () => undefined)
|
||||
this.#lock = promise.then(
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
)
|
||||
|
||||
return promise
|
||||
}
|
||||
@@ -50,4 +53,3 @@ export class Nip07Signer implements ISigner {
|
||||
this.#then(ext => ext.nip44.decrypt(pubkey, message)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import {Emitter, throttle, makePromise, defer, sleep, tryCatch, randomId, equals} from "@welshman/lib"
|
||||
import {createEvent, normalizeRelayUrl, TrustedEvent, StampedEvent, NOSTR_CONNECT} from "@welshman/util"
|
||||
import {
|
||||
Emitter,
|
||||
throttle,
|
||||
makePromise,
|
||||
defer,
|
||||
sleep,
|
||||
tryCatch,
|
||||
randomId,
|
||||
equals,
|
||||
} from "@welshman/lib"
|
||||
import {
|
||||
createEvent,
|
||||
normalizeRelayUrl,
|
||||
TrustedEvent,
|
||||
StampedEvent,
|
||||
NOSTR_CONNECT,
|
||||
} from "@welshman/util"
|
||||
import {subscribe, publish, Subscription, SubscriptionEvent} from "@welshman/net"
|
||||
import {ISigner, decrypt, hash, own} from '../util'
|
||||
import {Nip01Signer} from './nip01'
|
||||
import {ISigner, EncryptionImplementation, decrypt, hash, own} from "../util.js"
|
||||
import {Nip01Signer} from "./nip01.js"
|
||||
|
||||
export type Nip46Algorithm = "nip04" | "nip44"
|
||||
|
||||
export enum Nip46Event {
|
||||
Send = 'send',
|
||||
Receive = 'receive',
|
||||
Send = "send",
|
||||
Receive = "receive",
|
||||
}
|
||||
|
||||
export type Nip46BrokerParams = {
|
||||
@@ -82,7 +97,10 @@ const popupManager = (() => {
|
||||
export class Nip46Receiver extends Emitter {
|
||||
public sub?: Subscription
|
||||
|
||||
constructor(public signer: ISigner, public params: Nip46BrokerParams) {
|
||||
constructor(
|
||||
public signer: ISigner,
|
||||
public params: Nip46BrokerParams,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@@ -125,7 +143,10 @@ export class Nip46Sender extends Emitter {
|
||||
public processing = false
|
||||
public queue: Nip46Request[] = []
|
||||
|
||||
constructor(public signer: ISigner, public params: Nip46BrokerParams) {
|
||||
constructor(
|
||||
public signer: ISigner,
|
||||
public params: Nip46BrokerParams,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@@ -160,7 +181,7 @@ export class Nip46Sender extends Emitter {
|
||||
try {
|
||||
await this.send(request)
|
||||
} catch (error: any) {
|
||||
console.error(`nip46 error:`, error, request)
|
||||
console.error("nip46 error:", error, request)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -182,7 +203,10 @@ export class Nip46Request {
|
||||
id = randomId()
|
||||
promise = defer<Nip46ResponseWithResult, Nip46ResponseWithError>()
|
||||
|
||||
constructor(readonly method: string, readonly params: string[]) {}
|
||||
constructor(
|
||||
readonly method: string,
|
||||
readonly params: string[],
|
||||
) {}
|
||||
|
||||
listen = async (receiver: Nip46Receiver) => {
|
||||
await receiver.start()
|
||||
@@ -251,7 +275,7 @@ export class Nip46Broker extends Emitter {
|
||||
const sender = new Nip46Sender(this.signer, this.params)
|
||||
|
||||
sender.on(Nip46Event.Send, (data: any) => {
|
||||
console.log('nip46 send:', data)
|
||||
console.log("nip46 send:", data)
|
||||
})
|
||||
|
||||
return sender
|
||||
@@ -261,7 +285,7 @@ export class Nip46Broker extends Emitter {
|
||||
const receiver = new Nip46Receiver(this.signer, this.params)
|
||||
|
||||
receiver.on(Nip46Event.Receive, (data: any) => {
|
||||
console.log('nip46 receive:', data)
|
||||
console.log("nip46 receive:", data)
|
||||
})
|
||||
|
||||
return receiver
|
||||
@@ -314,7 +338,7 @@ export class Nip46Broker extends Emitter {
|
||||
const _url = new URL(url)
|
||||
|
||||
relays = _url.searchParams.getAll("relay") || []
|
||||
signerPubkey = _url.hostname || _url.pathname.replace(/\//g, '')
|
||||
signerPubkey = _url.hostname || _url.pathname.replace(/\//g, "")
|
||||
connectSecret = _url.searchParams.get("secret") || ""
|
||||
} catch {
|
||||
// pass
|
||||
@@ -329,22 +353,24 @@ export class Nip46Broker extends Emitter {
|
||||
const params = new URLSearchParams({...meta, secret})
|
||||
|
||||
for (const relay of this.params.relays) {
|
||||
params.append('relay', relay)
|
||||
params.append("relay", relay)
|
||||
}
|
||||
|
||||
return `nostrconnect://${clientPubkey}?${params.toString()}`
|
||||
}
|
||||
|
||||
waitForNostrconnect = (url: string, abort?: AbortController) => {
|
||||
const secret = new URL(url).searchParams.get('secret')
|
||||
const secret = new URL(url).searchParams.get("secret")
|
||||
|
||||
return makePromise<Nip46ResponseWithResult, Nip46Response | undefined>((resolve, reject) => {
|
||||
const onReceive = (response: Nip46Response) => {
|
||||
if (["ack", secret].includes(response.result!)) {
|
||||
this.setParams({signerPubkey: response.event.pubkey})
|
||||
|
||||
if (response.result === 'ack') {
|
||||
console.warn("Bunker responded to nostrconnect with 'ack', which can lead to session hijacking")
|
||||
if (response.result === "ack") {
|
||||
console.warn(
|
||||
"Bunker responded to nostrconnect with 'ack', which can lead to session hijacking",
|
||||
)
|
||||
}
|
||||
|
||||
resolve(response as Nip46ResponseWithResult)
|
||||
@@ -362,7 +388,7 @@ export class Nip46Broker extends Emitter {
|
||||
this.receiver.on(Nip46Event.Receive, onReceive)
|
||||
this.receiver.start()
|
||||
|
||||
abort?.signal.addEventListener('abort', () => {
|
||||
abort?.signal.addEventListener("abort", () => {
|
||||
reject(undefined)
|
||||
cleanup()
|
||||
})
|
||||
@@ -394,9 +420,21 @@ export class Nip46Broker extends Emitter {
|
||||
}
|
||||
|
||||
export class Nip46Signer implements ISigner {
|
||||
public pubkey?: string
|
||||
pubkey?: string
|
||||
nip04: EncryptionImplementation
|
||||
nip44: EncryptionImplementation
|
||||
|
||||
constructor(public broker: Nip46Broker) {}
|
||||
constructor(public broker: Nip46Broker) {
|
||||
this.nip04 = {
|
||||
encrypt: this.broker.nip04Encrypt,
|
||||
decrypt: this.broker.nip04Decrypt,
|
||||
}
|
||||
|
||||
this.nip44 = {
|
||||
encrypt: this.broker.nip44Encrypt,
|
||||
decrypt: this.broker.nip44Decrypt,
|
||||
}
|
||||
}
|
||||
|
||||
getPubkey = async () => {
|
||||
if (!this.pubkey) {
|
||||
@@ -408,14 +446,4 @@ export class Nip46Signer implements ISigner {
|
||||
|
||||
sign = async (template: StampedEvent) =>
|
||||
this.broker.signEvent(hash(own(template, await this.getPubkey())))
|
||||
|
||||
nip04 = {
|
||||
encrypt: this.broker.nip04Encrypt,
|
||||
decrypt: this.broker.nip04Decrypt,
|
||||
}
|
||||
|
||||
nip44 = {
|
||||
encrypt: this.broker.nip44Encrypt,
|
||||
decrypt: this.broker.nip44Decrypt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {SignedEvent, StampedEvent} from "@welshman/util"
|
||||
import {hash, own, ISigner} from "../util"
|
||||
import {NostrSignerPlugin, AppInfo} from "nostr-signer-capacitor-plugin"
|
||||
import {nip19} from "nostr-tools"
|
||||
import {decode} from "nostr-tools/nip19"
|
||||
import {SignedEvent, StampedEvent} from "@welshman/util"
|
||||
import {hash, own, ISigner} from "../util.js"
|
||||
|
||||
export const getNip55 = async (): Promise<AppInfo[]> => {
|
||||
const {apps} = await NostrSignerPlugin.getInstalledSignerApps()
|
||||
@@ -21,10 +21,11 @@ export class Nip55Signer implements ISigner {
|
||||
this.#initialize()
|
||||
}
|
||||
|
||||
async #initialize() {
|
||||
#initialize() {
|
||||
if (!this.#packageNameSet) {
|
||||
await this.#plugin.setPackageName({packageName: this.#packageName})
|
||||
this.#packageNameSet = true
|
||||
void this.#plugin.setPackageName({packageName: this.#packageName}).then(() => {
|
||||
this.#packageNameSet = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,10 +54,10 @@ export class Nip55Signer implements ISigner {
|
||||
try {
|
||||
const {npub} = await signer.getPublicKey()
|
||||
this.#npub = npub
|
||||
const {data} = nip19.decode(npub)
|
||||
const {data} = decode(npub)
|
||||
this.#publicKey = data as string
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get public key`)
|
||||
throw new Error("Failed to get public key")
|
||||
}
|
||||
}
|
||||
return this.#publicKey
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {schnorr} from '@noble/curves/secp256k1'
|
||||
import {bytesToHex, hexToBytes} from '@noble/hashes/utils'
|
||||
import {nip04 as nt04, nip44 as nt44, generateSecretKey, getPublicKey, getEventHash} from "nostr-tools"
|
||||
import {cached, now} from '@welshman/lib'
|
||||
import {SignedEvent, HashedEvent, EventTemplate, StampedEvent, OwnedEvent} from '@welshman/util'
|
||||
import {schnorr} from "@noble/curves/secp256k1"
|
||||
import {bytesToHex, hexToBytes} from "@noble/hashes/utils"
|
||||
import * as nt04 from "nostr-tools/nip04"
|
||||
import * as nt44 from "nostr-tools/nip44"
|
||||
import {generateSecretKey, getPublicKey, getEventHash} from "nostr-tools/pure"
|
||||
import {cached, now} from "@welshman/lib"
|
||||
import {SignedEvent, HashedEvent, EventTemplate, StampedEvent, OwnedEvent} from "@welshman/util"
|
||||
|
||||
export const makeSecret = () => bytesToHex(generateSecretKey())
|
||||
|
||||
@@ -24,17 +26,20 @@ export const sign = (event: HashedEvent, secret: string) => ({...event, sig: get
|
||||
export const nip04 = {
|
||||
detect: (m: string) => m.includes("?iv="),
|
||||
encrypt: (pubkey: string, secret: string, m: string) => nt04.encrypt(secret, pubkey, m),
|
||||
decrypt: (pubkey: string, secret: string, m: string) => nt04.decrypt(secret, pubkey, m),
|
||||
decrypt: (pubkey: string, secret: string, m: string) => nt04.decrypt(secret, pubkey, m),
|
||||
}
|
||||
|
||||
export const nip44 = {
|
||||
getSharedSecret: cached({
|
||||
maxSize: 10000,
|
||||
getKey: ([secret, pubkey]) => [secret, pubkey].join(":"),
|
||||
getValue: ([secret, pubkey]: string[]) => nt44.v2.utils.getConversationKey(hexToBytes(secret), pubkey),
|
||||
getValue: ([secret, pubkey]: string[]) =>
|
||||
nt44.v2.utils.getConversationKey(hexToBytes(secret), pubkey),
|
||||
}),
|
||||
encrypt: (pubkey: string, secret: string, m: string) => nt44.v2.encrypt(m, nip44.getSharedSecret(secret, pubkey)!),
|
||||
decrypt: (pubkey: string, secret: string, m: string) => nt44.v2.decrypt(m, nip44.getSharedSecret(secret, pubkey)!),
|
||||
encrypt: (pubkey: string, secret: string, m: string) =>
|
||||
nt44.v2.encrypt(m, nip44.getSharedSecret(secret, pubkey)!),
|
||||
decrypt: (pubkey: string, secret: string, m: string) =>
|
||||
nt44.v2.decrypt(m, nip44.getSharedSecret(secret, pubkey)!),
|
||||
}
|
||||
|
||||
export type Sign = (event: StampedEvent) => Promise<SignedEvent>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"targets": [
|
||||
{"extname": ".cjs", "module": "commonjs"},
|
||||
{"extname": ".mjs", "module": "esnext", "moduleResolution": "node"}
|
||||
],
|
||||
"projects": ["tsconfig.json"]
|
||||
}
|
||||
@@ -3,9 +3,12 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "build",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["esnext", "dom", "dom.iterable"]
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"lib": ["esnext", "dom"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,21 +15,16 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/src/index.d.ts",
|
||||
"import": "./build/src/index.mjs",
|
||||
"require": "./build/src/index.cjs"
|
||||
"import": "./build/src/index.js",
|
||||
"require": "./build/src/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"pub": "npm run lint && npm run build && npm publish",
|
||||
"build": "gts clean && tsc-multi",
|
||||
"build": "gts clean && tsc",
|
||||
"lint": "gts lint",
|
||||
"fix": "gts fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gts": "^5.0.1",
|
||||
"tsc-multi": "^1.1.0",
|
||||
"typescript": "~5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@welshman/util": "~0.0.50",
|
||||
|
||||
+109
-88
@@ -1,6 +1,15 @@
|
||||
import {derived, writable} from "svelte/store"
|
||||
import type {Readable, Writable, Subscriber, Unsubscriber} from "svelte/store"
|
||||
import {identity, throttle, ensurePlural, getJson, setJson, batch, partition, first} from "@welshman/lib"
|
||||
import {
|
||||
identity,
|
||||
throttle,
|
||||
ensurePlural,
|
||||
getJson,
|
||||
setJson,
|
||||
batch,
|
||||
partition,
|
||||
first,
|
||||
} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import type {Repository} from "@welshman/util"
|
||||
import {matchFilters, getIdAndAddress, getIdFilters} from "@welshman/util"
|
||||
@@ -59,7 +68,10 @@ export type CustomStoreOpts<T> = {
|
||||
set?: (x: T) => void
|
||||
}
|
||||
|
||||
export const custom = <T>(start: Start<T>, opts: CustomStoreOpts<T> = {}): WritableWithGetter<T> => {
|
||||
export const custom = <T>(
|
||||
start: Start<T>,
|
||||
opts: CustomStoreOpts<T> = {},
|
||||
): WritableWithGetter<T> => {
|
||||
const subs: Subscriber<T>[] = []
|
||||
|
||||
let value: T
|
||||
@@ -118,9 +130,9 @@ export const adapter = <Source, Target>({
|
||||
forward,
|
||||
backward,
|
||||
}: {
|
||||
store: Writable<Source>,
|
||||
forward: (x: Source) => Target,
|
||||
backward: (x: Target) => Source,
|
||||
store: Writable<Source>
|
||||
forward: (x: Source) => Target
|
||||
backward: (x: Target) => Source
|
||||
}) => ({
|
||||
...derived(store, forward),
|
||||
set: (x: Target) => store.set(backward(x)),
|
||||
@@ -137,109 +149,118 @@ export type DeriveEventsMappedOptions<T> = {
|
||||
includeDeleted?: boolean
|
||||
}
|
||||
|
||||
export const deriveEventsMapped = <T>(repository: Repository, {
|
||||
filters,
|
||||
eventToItem,
|
||||
itemToEvent,
|
||||
throttle = 0,
|
||||
includeDeleted = false,
|
||||
}: DeriveEventsMappedOptions<T>) =>
|
||||
custom<T[]>(setter => {
|
||||
let data: T[] = []
|
||||
const deferred = new Set()
|
||||
export const deriveEventsMapped = <T>(
|
||||
repository: Repository,
|
||||
{
|
||||
filters,
|
||||
eventToItem,
|
||||
itemToEvent,
|
||||
throttle = 0,
|
||||
includeDeleted = false,
|
||||
}: DeriveEventsMappedOptions<T>,
|
||||
) =>
|
||||
custom<T[]>(
|
||||
setter => {
|
||||
let data: T[] = []
|
||||
const deferred = new Set()
|
||||
|
||||
const defer = (event: TrustedEvent, promise: Promise<T | T[]>) => {
|
||||
deferred.add(event.id)
|
||||
const defer = (event: TrustedEvent, promise: Promise<T | T[]>) => {
|
||||
deferred.add(event.id)
|
||||
|
||||
promise.then(items => {
|
||||
if (deferred.has(event.id)) {
|
||||
deferred.delete(event.id)
|
||||
void promise.then(items => {
|
||||
if (deferred.has(event.id)) {
|
||||
deferred.delete(event.id)
|
||||
|
||||
for (const item of ensurePlural(items)) {
|
||||
data.push(item)
|
||||
}
|
||||
|
||||
setter(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const event of repository.query(filters, {includeDeleted})) {
|
||||
const items = eventToItem(event)
|
||||
|
||||
if (!items) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (items instanceof Promise) {
|
||||
defer(event, items)
|
||||
} else {
|
||||
for (const item of ensurePlural(items)) {
|
||||
data.push(item)
|
||||
}
|
||||
|
||||
setter(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const event of repository.query(filters, {includeDeleted})) {
|
||||
const items = eventToItem(event)
|
||||
|
||||
if (!items) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (items instanceof Promise) {
|
||||
defer(event, items)
|
||||
} else {
|
||||
for (const item of ensurePlural(items)) {
|
||||
data.push(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setter(data)
|
||||
|
||||
const onUpdate = batch(300, (updates: {added: TrustedEvent[]; removed: Set<string>}[]) => {
|
||||
const removed = new Set()
|
||||
const added = new Map()
|
||||
|
||||
// Apply updates in order
|
||||
for (const update of updates) {
|
||||
for (const event of update.added.values()) {
|
||||
added.set(event.id, event)
|
||||
removed.delete(event.id)
|
||||
}
|
||||
|
||||
for (const id of update.removed) {
|
||||
removed.add(id)
|
||||
added.delete(id)
|
||||
deferred.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
let dirty = false
|
||||
for (const event of added.values()) {
|
||||
if (matchFilters(filters, event)) {
|
||||
const items = eventToItem(event)
|
||||
setter(data)
|
||||
|
||||
if (items instanceof Promise) {
|
||||
defer(event, items)
|
||||
} else if (items) {
|
||||
dirty = true
|
||||
const onUpdate = batch(300, (updates: {added: TrustedEvent[]; removed: Set<string>}[]) => {
|
||||
const removed = new Set()
|
||||
const added = new Map()
|
||||
|
||||
for (const item of ensurePlural(items)) {
|
||||
data.push(item as T)
|
||||
// Apply updates in order
|
||||
for (const update of updates) {
|
||||
for (const event of update.added.values()) {
|
||||
added.set(event.id, event)
|
||||
removed.delete(event.id)
|
||||
}
|
||||
|
||||
for (const id of update.removed) {
|
||||
removed.add(id)
|
||||
added.delete(id)
|
||||
deferred.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
let dirty = false
|
||||
for (const event of added.values()) {
|
||||
if (matchFilters(filters, event)) {
|
||||
const items = eventToItem(event)
|
||||
|
||||
if (items instanceof Promise) {
|
||||
defer(event, items)
|
||||
} else if (items) {
|
||||
dirty = true
|
||||
|
||||
for (const item of ensurePlural(items)) {
|
||||
data.push(item as T)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!includeDeleted && removed.size > 0) {
|
||||
const [deleted, ok] = partition(
|
||||
(item: T) => getIdAndAddress(itemToEvent(item)).some((id: string) => removed.has(id)),
|
||||
data,
|
||||
)
|
||||
if (!includeDeleted && removed.size > 0) {
|
||||
const [deleted, ok] = partition(
|
||||
(item: T) => getIdAndAddress(itemToEvent(item)).some((id: string) => removed.has(id)),
|
||||
data,
|
||||
)
|
||||
|
||||
if (deleted.length > 0) {
|
||||
dirty = true
|
||||
data = ok
|
||||
if (deleted.length > 0) {
|
||||
dirty = true
|
||||
data = ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty) {
|
||||
setter(data)
|
||||
}
|
||||
})
|
||||
if (dirty) {
|
||||
setter(data)
|
||||
}
|
||||
})
|
||||
|
||||
repository.on("update", onUpdate)
|
||||
repository.on("update", onUpdate)
|
||||
|
||||
return () => repository.off("update", onUpdate)
|
||||
}, {throttle})
|
||||
return () => repository.off("update", onUpdate)
|
||||
},
|
||||
{throttle},
|
||||
)
|
||||
|
||||
export type DeriveEventsOptions<T> = Omit<DeriveEventsMappedOptions<T>, "itemToEvent" | "eventToItem">
|
||||
export type DeriveEventsOptions<T> = Omit<
|
||||
DeriveEventsMappedOptions<T>,
|
||||
"itemToEvent" | "eventToItem"
|
||||
>
|
||||
|
||||
export const deriveEvents = <T>(repository: Repository, opts: DeriveEventsOptions<T>) =>
|
||||
deriveEventsMapped<TrustedEvent>(repository, {
|
||||
@@ -254,7 +275,7 @@ export const deriveEvent = (repository: Repository, idOrAddress: string) =>
|
||||
filters: getIdFilters([idOrAddress]),
|
||||
includeDeleted: true,
|
||||
}),
|
||||
first
|
||||
first,
|
||||
)
|
||||
|
||||
export const deriveIsDeleted = (repository: Repository, event: TrustedEvent) =>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"targets": [
|
||||
{"extname": ".cjs", "module": "commonjs"},
|
||||
{"extname": ".mjs", "module": "esnext", "moduleResolution": "node"}
|
||||
],
|
||||
"projects": ["tsconfig.json"]
|
||||
}
|
||||
@@ -3,9 +3,12 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "build",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["esnext", "dom", "dom.iterable"]
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"lib": ["esnext", "dom"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,26 +11,25 @@
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.4.0"
|
||||
},
|
||||
"types": "./build/src/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/src/index.d.ts",
|
||||
"import": "./build/src/index.mjs",
|
||||
"require": "./build/src/index.cjs"
|
||||
"import": "./build/src/index.js",
|
||||
"require": "./build/src/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"pub": "npm run lint && npm run build && npm publish",
|
||||
"build": "gts clean && tsc-multi",
|
||||
"build": "gts clean && tsc",
|
||||
"lint": "gts lint",
|
||||
"fix": "gts fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gts": "^5.0.1",
|
||||
"tsc-multi": "^1.1.0",
|
||||
"typescript": "~5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/ws": "^8.5.13",
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"nostr-tools": "^2.7.2"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {nip19} from "nostr-tools"
|
||||
import {decode, naddrEncode} from "nostr-tools/nip19"
|
||||
|
||||
// Define this locally to avoid circular dependencies
|
||||
type AddressableEvent = {
|
||||
@@ -12,7 +12,7 @@ export class Address {
|
||||
readonly kind: number,
|
||||
readonly pubkey: string,
|
||||
readonly identifier: string,
|
||||
readonly relays: string[] = []
|
||||
readonly relays: string[] = [],
|
||||
) {}
|
||||
|
||||
static isAddress(address: string) {
|
||||
@@ -26,22 +26,21 @@ export class Address {
|
||||
}
|
||||
|
||||
static fromNaddr(naddr: string) {
|
||||
let type
|
||||
let data = {} as any
|
||||
let decoded: any
|
||||
|
||||
try {
|
||||
({type, data} = nip19.decode(naddr) as {
|
||||
type: "naddr"
|
||||
data: any
|
||||
})
|
||||
decoded = decode(naddr)
|
||||
} catch (e) {
|
||||
// pass
|
||||
}
|
||||
|
||||
if (type !== "naddr") {
|
||||
if (decoded?.type !== "naddr") {
|
||||
throw new Error(`Invalid naddr ${naddr}`)
|
||||
}
|
||||
|
||||
return new Address(data.kind, data.pubkey, data.identifier, data.relays)
|
||||
const {kind, pubkey, identifier, relays} = decoded.data
|
||||
|
||||
return new Address(kind, pubkey, identifier, relays)
|
||||
}
|
||||
|
||||
static fromEvent(event: AddressableEvent, relays: string[] = []) {
|
||||
@@ -52,7 +51,7 @@ export class Address {
|
||||
|
||||
toString = () => [this.kind, this.pubkey, this.identifier].join(":")
|
||||
|
||||
toNaddr = () => nip19.naddrEncode(this)
|
||||
toNaddr = () => naddrEncode(this)
|
||||
}
|
||||
|
||||
// Utils
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user