Remove tsc-multi, re-install gts, apply autoformatting and linting

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

Some files were not shown because too many files have changed in this diff Show More