diff --git a/packages/domain/.eslintignore b/packages/domain/.eslintignore deleted file mode 100644 index 43e824a..0000000 --- a/packages/domain/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -build -normalize-url diff --git a/packages/domain/README.md b/packages/domain/README.md deleted file mode 100644 index 2281f55..0000000 --- a/packages/domain/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @welshman/domain [![version](https://badgen.net/npm/v/@welshman/domain)](https://npmjs.com/package/@welshman/domain) - -A collection of utilities for mapping nostr events to useful data structures. diff --git a/packages/domain/package.json b/packages/domain/package.json deleted file mode 100644 index 7674463..0000000 --- a/packages/domain/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "@welshman/domain", - "version": "0.0.1", - "author": "hodlbod", - "license": "MIT", - "description": "A collection of utilities for mapping nostr events to useful data structures.", - "publishConfig": { - "access": "public" - }, - "type": "module", - "files": [ - "build" - ], - "types": "./build/src/index.d.ts", - "exports": { - ".": { - "types": "./build/src/index.d.ts", - "import": "./build/src/index.mjs", - "require": "./build/src/index.cjs" - } - }, - "scripts": { - "pub": "npm run lint && npm run build && npm publish", - "build": "gts clean && tsc-multi", - "lint": "gts lint", - "fix": "gts fix" - }, - "devDependencies": { - "gts": "^5.0.1", - "tsc-multi": "^1.1.0", - "typescript": "~5.1.6" - }, - "dependencies": { - "nostr-tools": "^2.7.2", - "@welshman/lib": "0.0.14", - "@welshman/util": "0.0.25" - } -} diff --git a/packages/domain/src/handle.ts b/packages/domain/src/handle.ts deleted file mode 100644 index 72debd3..0000000 --- a/packages/domain/src/handle.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {last} from '@welshman/lib' - -export type Handle = { - pubkey: string - nip05: string - nip46: string[] - relays: string[] -} - -export const displayHandle = (handle: Handle) => - handle.nip05.startsWith("_@") ? last(handle.nip05.split("@")) : handle.nip05 diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts deleted file mode 100644 index 204f533..0000000 --- a/packages/domain/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./handle" -export * from "./handler" -export * from "./list" -export * from "./profile" -export * from "./relay" -export * from "./util" diff --git a/packages/domain/src/relay.ts b/packages/domain/src/relay.ts deleted file mode 100644 index e89f38e..0000000 --- a/packages/domain/src/relay.ts +++ /dev/null @@ -1,63 +0,0 @@ -import {last} from "@welshman/lib" -import {LOCAL_RELAY_URL, normalizeRelayUrl as _normalizeRelayUrl} from "@welshman/util" - -// Utils related to bare urls - -export function normalizeRelayUrl(url: string, opts = {}) { - if (url === LOCAL_RELAY_URL) { - return url - } - - try { - return _normalizeRelayUrl(url, opts) - } catch (e) { - return url - } -} - -export const displayRelayUrl = (url: string) => last(url.split("://")).replace(/\/$/, "") - -// Relay profiles - -export type RelayProfile = { - url: string - name?: string - contact?: string - description?: string - supported_nips?: number[] - limitation?: { - payment_required?: boolean - auth_required?: boolean - } -} - -export const makeRelayProfile = (relayProfile: RelayProfile) => relayProfile - -export const filterRelaysByNip = (nip: number, relays: RelayProfile[]) => - relays.filter(r => r.supported_nips?.includes(nip)) - -// Relay policies - -export enum RelayMode { - Read = "read", - Write = "write", - Inbox = "inbox", -} - -export type RelayPolicy = { - url: string - read: boolean - write: boolean - inbox: boolean -} - -export const makeRelayPolicy = ({ - url, - ...relayPolicy -}: Partial & {url: string}): RelayPolicy => ({ - url: normalizeRelayUrl(url), - read: false, - write: false, - inbox: false, - ...relayPolicy, -}) diff --git a/packages/domain/tsc-multi.json b/packages/domain/tsc-multi.json deleted file mode 100644 index 6c37019..0000000 --- a/packages/domain/tsc-multi.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "targets": [ - {"extname": ".cjs", "module": "commonjs"}, - {"extname": ".mjs", "module": "esnext", "moduleResolution": "node"} - ], - "projects": ["tsconfig.json"] -} diff --git a/packages/domain/tsconfig.json b/packages/domain/tsconfig.json deleted file mode 100644 index 15d351a..0000000 --- a/packages/domain/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../node_modules/gts/tsconfig-google.json", - "compilerOptions": { - "rootDir": ".", - "outDir": "build", - "esModuleInterop": true, - "skipLibCheck": true, - "lib": ["esnext", "dom", "dom.iterable"] - }, - "include": ["**/*.ts"] -} diff --git a/packages/dvm/src/handler.ts b/packages/dvm/src/handler.ts index 709f3ad..2808962 100644 --- a/packages/dvm/src/handler.ts +++ b/packages/dvm/src/handler.ts @@ -1,12 +1,12 @@ import {hexToBytes} from '@noble/hashes/utils' import {getPublicKey, finalizeEvent} from 'nostr-tools' import {now} from '@welshman/lib' -import type {ExtensibleTrustedEvent, EventTemplate, Filter} from '@welshman/util' +import type {CustomEvent, EventTemplate, Filter} from '@welshman/util' import {subscribe, publish} from '@welshman/net' export type DVMHandler = { stop?: () => void - handleEvent: (e: ExtensibleTrustedEvent) => AsyncGenerator + handleEvent: (e: CustomEvent) => AsyncGenerator } export type CreateDVMHandler = (dvm: DVM) => DVMHandler @@ -49,7 +49,7 @@ export class DVM { const filters = [filter] const sub = subscribe({relays, filters}) - sub.emitter.on('event', (url: string, e: ExtensibleTrustedEvent) => this.onEvent(e)) + sub.emitter.on('event', (url: string, e: CustomEvent) => this.onEvent(e)) sub.emitter.on('complete', () => resolve()) }) } @@ -63,8 +63,7 @@ export class DVM { this.active = false } - async onEvent(request: ExtensibleTrustedEvent) { - console.log(request) + async onEvent(request: CustomEvent) { const {expireAfter = 60 * 60} = this.opts if (this.seen.has(request.id)) { diff --git a/packages/dvm/src/request.ts b/packages/dvm/src/request.ts index af27f8a..486c67c 100644 --- a/packages/dvm/src/request.ts +++ b/packages/dvm/src/request.ts @@ -1,5 +1,5 @@ import {Emitter, now} from '@welshman/lib' -import type {TrustedEvent, SignedEvent} from '@welshman/util' +import type {CustomEvent, SignedEvent} from '@welshman/util' import {subscribe, publish} from '@welshman/net' import type {Subscription, Publish} from '@welshman/net' @@ -32,7 +32,7 @@ export const makeDvmRequest = (request: DVMRequestOptions) => { const sub = subscribe({relays, timeout, filters}) const pub = publish({event, relays, timeout}) - sub.emitter.on('event', (url: string, event: TrustedEvent) => { + sub.emitter.on('event', (url: string, event: CustomEvent) => { if (event.kind === 7000) { emitter.emit(DVMEvent.Progress, url, event) } else { diff --git a/packages/feeds/src/compiler.ts b/packages/feeds/src/compiler.ts index 3650e19..cf76da5 100644 --- a/packages/feeds/src/compiler.ts +++ b/packages/feeds/src/compiler.ts @@ -1,5 +1,5 @@ import {uniq, identity, flatten, pushToMapKey, intersection, tryCatch, now} from '@welshman/lib' -import type {ExtensibleTrustedEvent, Filter} from '@welshman/util' +import type {CustomEvent, Filter} from '@welshman/util' import {Tags, intersectFilters, matchFilter, getAddress, getIdFilters, unionFilters} from '@welshman/util' import type {CreatedAtItem, RequestItem, ListItem, LabelItem, WOTItem, DVMItem, Scope, Feed, FeedOptions} from './core' import {getFeedArgs, feedsFromTags} from './utils' @@ -109,7 +109,7 @@ export class FeedCompiler { items.map(({mappings, ...request}) => this.options.requestDVM({ ...request, - onEvent: async (e: ExtensibleTrustedEvent) => { + onEvent: async (e: CustomEvent) => { const tags = Tags.wrap(await tryCatch(() => JSON.parse(e.content)) || []) for (const feed of feedsFromTags(tags, mappings)) { @@ -215,11 +215,11 @@ export class FeedCompiler { async _compileLists(listItems: ListItem[]): Promise { const addresses = uniq(listItems.flatMap(({addresses}) => addresses)) - const eventsByAddress = new Map() + const eventsByAddress = new Map() await this.options.request({ filters: getIdFilters(addresses), - onEvent: (e: ExtensibleTrustedEvent) => eventsByAddress.set(getAddress(e), e), + onEvent: (e: CustomEvent) => eventsByAddress.set(getAddress(e), e), }) const feeds = flatten( @@ -246,14 +246,14 @@ export class FeedCompiler { } async _compileLabels(labelItems: LabelItem[]): Promise { - const events: ExtensibleTrustedEvent[] = [] + const events: CustomEvent[] = [] await Promise.all( labelItems.map(({mappings, relays, ...filter}) => this.options.request({ relays, filters: [{kinds: [1985], ...filter}], - onEvent: (e: ExtensibleTrustedEvent) => events.push(e), + onEvent: (e: CustomEvent) => events.push(e), }) ) ) diff --git a/packages/feeds/src/core.ts b/packages/feeds/src/core.ts index 3558b72..717c64b 100644 --- a/packages/feeds/src/core.ts +++ b/packages/feeds/src/core.ts @@ -1,4 +1,4 @@ -import type {ExtensibleTrustedEvent, Filter} from '@welshman/util' +import type {CustomEvent, Filter} from '@welshman/util' export enum FeedType { Address = "address", @@ -110,7 +110,7 @@ export type RequestItem = { } export type RequestOpts = RequestItem & { - onEvent: (event: ExtensibleTrustedEvent) => void + onEvent: (event: CustomEvent) => void } export type DVMRequest = { @@ -120,7 +120,7 @@ export type DVMRequest = { } export type DVMOpts = DVMRequest & { - onEvent: (event: ExtensibleTrustedEvent) => void + onEvent: (event: CustomEvent) => void } export type FeedOptions = { diff --git a/packages/feeds/src/loader.ts b/packages/feeds/src/loader.ts index 5e8a19e..f44feb8 100644 --- a/packages/feeds/src/loader.ts +++ b/packages/feeds/src/loader.ts @@ -1,12 +1,12 @@ import {inc, max, min, now} from '@welshman/lib' -import type {ExtensibleTrustedEvent, Filter} from '@welshman/util' +import type {CustomEvent, Filter} from '@welshman/util' import {EPOCH, trimFilters, guessFilterDelta} from '@welshman/util' import type {Feed, RequestItem, FeedOptions} from './core' import {FeedType} from './core' import {FeedCompiler} from './compiler' export type LoadOpts = { - onEvent?: (event: ExtensibleTrustedEvent) => void + onEvent?: (event: CustomEvent) => void onExhausted?: () => void useWindowing?: boolean } @@ -101,7 +101,7 @@ export class FeedLoader { await this.options.request({ relays, filters: trimFilters(requestFilters), - onEvent: (event: ExtensibleTrustedEvent) => { + onEvent: (event: CustomEvent) => { count += 1 until = Math.min(until, event.created_at - 1) onEvent?.(event) @@ -130,14 +130,14 @@ export class FeedLoader { async _getDifferenceLoader(feeds: Feed[], {onEvent, onExhausted}: LoadOpts) { const exhausted = new Set() const skip = new Set() - const events: ExtensibleTrustedEvent[] = [] + const events: CustomEvent[] = [] const seen = new Set() const loaders = await Promise.all( feeds.map((feed: Feed, i: number) => this.getLoader(feed, { onExhausted: () => exhausted.add(i), - onEvent: (event: ExtensibleTrustedEvent) => { + onEvent: (event: CustomEvent) => { if (i === 0) { events.push(event) } else { @@ -175,14 +175,14 @@ export class FeedLoader { async _getIntersectionLoader(feeds: Feed[], {onEvent, onExhausted}: LoadOpts) { const exhausted = new Set() const counts = new Map() - const events: ExtensibleTrustedEvent[] = [] + const events: CustomEvent[] = [] const seen = new Set() const loaders = await Promise.all( feeds.map((feed: Feed, i: number) => this.getLoader(feed, { onExhausted: () => exhausted.add(i), - onEvent: (event: ExtensibleTrustedEvent) => { + onEvent: (event: CustomEvent) => { events.push(event) counts.set(event.id, inc(counts.get(event.id))) }, @@ -222,7 +222,7 @@ export class FeedLoader { feeds.map((feed: Feed, i: number) => this.getLoader(feed, { onExhausted: () => exhausted.add(i), - onEvent: (event: ExtensibleTrustedEvent) => { + onEvent: (event: CustomEvent) => { if (!seen.has(event.id)) { onEvent?.(event) seen.add(event.id) diff --git a/packages/lib/src/Tools.ts b/packages/lib/src/Tools.ts index 67b960d..6d2e0bc 100644 --- a/packages/lib/src/Tools.ts +++ b/packages/lib/src/Tools.ts @@ -7,6 +7,8 @@ export type Nil = null | undefined export const isNil = (x: any) => [null, undefined].includes(x) +export type Maybe = T | undefined + // Regular old utils export const now = () => Math.round(Date.now() / 1000) @@ -95,9 +97,9 @@ export const stripProtocol = (url: string) => url.replace(/.*:\/\//, "") export const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t)) -export const concat = (...xs: (T | Nil)[][]) => xs.flatMap(x => x || []) +export const concat = (...xs: T[][]) => xs.flatMap(x => x || []) -export const append = (x: T, xs: (T | Nil)[]) => concat(xs, [x]) +export const append = (x: T, xs: T[]) => concat(xs, [x]) export const union = (a: T[], b: T[]) => uniq([...a, ...b]) diff --git a/packages/net/src/ConnectionMeta.ts b/packages/net/src/ConnectionMeta.ts index 8326a35..6c3a0e3 100644 --- a/packages/net/src/ConnectionMeta.ts +++ b/packages/net/src/ConnectionMeta.ts @@ -1,10 +1,10 @@ -import type {Event, Filter} from 'nostr-tools' +import type {SignedEvent, Filter} from '@welshman/util' import type {Message} from './Socket' import type {Connection} from './Connection' export type PublishMeta = { sent: number - event: Event + event: SignedEvent } export type RequestMeta = { diff --git a/packages/net/src/Context.ts b/packages/net/src/Context.ts index 02115f4..a4124a3 100644 --- a/packages/net/src/Context.ts +++ b/packages/net/src/Context.ts @@ -1,6 +1,5 @@ -import type {Event} from 'nostr-tools' import {matchFilters, hasValidSignature} from '@welshman/util' -import type {Filter} from '@welshman/util' +import type {Filter, SignedEvent} from '@welshman/util' import {Pool} from "./Pool" import {Executor} from "./Executor" import {Relays} from "./target/Relays" @@ -10,17 +9,17 @@ export const defaultPool = new Pool() export const defaultGetExecutor = (relays: string[]) => new Executor(new Relays(relays.map((relay: string) => NetworkContext.pool.get(relay)))) -const defaultOnEvent = (url: string, event: Event) => null +const defaultOnEvent = (url: string, event: SignedEvent) => null const defaultOnAuth = (url: string, challenge: string) => null const defaultOnOk = (url: string, id: string, ok: boolean, message: string) => null -const defaultIsDeleted = (url: string, event: Event) => false +const defaultIsDeleted = (url: string, event: SignedEvent) => false -const defaultHasValidSignature = (url: string, event: Event) => hasValidSignature(event) +const defaultHasValidSignature = (url: string, event: SignedEvent) => hasValidSignature(event) -const defaultMatchFilters = (url: string, filters: Filter[], event: Event) => matchFilters(filters, event) +const defaultMatchFilters = (url: string, filters: Filter[], event: SignedEvent) => matchFilters(filters, event) export const NetworkContext = { pool: defaultPool, diff --git a/packages/net/src/Executor.ts b/packages/net/src/Executor.ts index ccf0398..df36d77 100644 --- a/packages/net/src/Executor.ts +++ b/packages/net/src/Executor.ts @@ -1,5 +1,5 @@ -import type {Event, Filter} from 'nostr-tools' import type {Emitter} from '@welshman/lib' +import type {SignedEvent, Filter} from '@welshman/util' import type {Message} from './Socket' import type {Connection} from './Connection' import {NetworkContext} from './Context' @@ -10,7 +10,7 @@ export type Target = Emitter & { cleanup: () => void } -type EventCallback = (url: string, event: Event) => void +type EventCallback = (url: string, event: SignedEvent) => void type EoseCallback = (url: string) => void type OkCallback = (url: string, id: string, ...extra: any[]) => void type ErrorCallback = (url: string, id: string, ...extra: any[]) => void @@ -31,7 +31,7 @@ export class Executor { const id = createSubId('REQ') - const eventListener = (url: string, subid: string, e: Event) => { + const eventListener = (url: string, subid: string, e: SignedEvent) => { if (subid === id) { NetworkContext.onEvent(url, e) onEvent?.(url, e) @@ -61,7 +61,7 @@ export class Executor { } } - publish(event: Event, {verb = 'EVENT', onOk, onError}: PublishOpts = {}) { + publish(event: SignedEvent, {verb = 'EVENT', onOk, onError}: PublishOpts = {}) { const okListener = (url: string, id: string, ...payload: any[]) => { if (id === event.id) { NetworkContext.onEvent(url, event) diff --git a/packages/net/src/Publish.ts b/packages/net/src/Publish.ts index d1d2528..12848c7 100644 --- a/packages/net/src/Publish.ts +++ b/packages/net/src/Publish.ts @@ -1,7 +1,7 @@ -import type {Event} from 'nostr-tools' import {Emitter, now, randomId, defer} from '@welshman/lib' import type {Deferred} from '@welshman/lib' import {asSignedEvent} from '@welshman/util' +import type {SignedEvent} from '@welshman/util' import {NetworkContext} from './Context' export enum PublishStatus { @@ -15,7 +15,7 @@ export enum PublishStatus { export type PublishStatusMap = Map export type PublishRequest = { - event: Event + event: SignedEvent relays: string[] signal?: AbortSignal timeout?: number diff --git a/packages/net/src/Subscribe.ts b/packages/net/src/Subscribe.ts index c68f894..141f900 100644 --- a/packages/net/src/Subscribe.ts +++ b/packages/net/src/Subscribe.ts @@ -1,6 +1,5 @@ -import type {Event} from 'nostr-tools' import {Emitter, chunk, randomId, once, groupBy, batch, uniq} from '@welshman/lib' -import {matchFilters, unionFilters} from '@welshman/util' +import {matchFilters, unionFilters, SignedEvent} from '@welshman/util' import type {Filter} from '@welshman/util' import {Tracker} from "./Tracker" import {Connection} from './Connection' @@ -101,7 +100,7 @@ export const mergeSubscriptions = (subs: Subscription[]) => { controller.signal.addEventListener('abort', onAbort) } - mergedSub.emitter.on(SubscriptionEvent.Event, (url: string, event: Event) => { + mergedSub.emitter.on(SubscriptionEvent.Event, (url: string, event: SignedEvent) => { for (const sub of callerSubs) { if (sub.tracker.track(event.id, url)) { continue @@ -117,7 +116,7 @@ export const mergeSubscriptions = (subs: Subscription[]) => { // Pass events back to caller const propagateEvent = (type: SubscriptionEvent, checkFilter: boolean) => - mergedSub.emitter.on(type, (url: string, event: Event) => { + mergedSub.emitter.on(type, (url: string, event: SignedEvent) => { for (const sub of callerSubs) { if (!checkFilter || matchFilters(sub.request.filters, event)) { sub.emitter.emit(type, url, event) @@ -172,11 +171,11 @@ export const executeSubscription = (sub: Subscription) => { const executor = NetworkContext.getExecutor(relays) const subs: {unsubscribe: () => void}[] = [] const completedRelays = new Set() - const events: Event[] = [] + const events: SignedEvent[] = [] // Hook up our events - emitter.on(SubscriptionEvent.Event, (url: string, event: Event) => { + emitter.on(SubscriptionEvent.Event, (url: string, event: SignedEvent) => { events.push(event) }) @@ -205,7 +204,7 @@ export const executeSubscription = (sub: Subscription) => { // Functions for emitting events - const onEvent = (url: string, event: Event) => { + const onEvent = (url: string, event: SignedEvent) => { if (tracker.track(event.id, url)) { emitter.emit(SubscriptionEvent.Duplicate, url, event) } else if (NetworkContext.isDeleted(url, event)) { diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 161209f..ecc1fff 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -4,7 +4,7 @@ import type {Readable, Writable} from "svelte/store" import {identity, batch, partition, first} from "@welshman/lib" import type {Repository} from "@welshman/util" import {matchFilters, getIdAndAddress, getIdFilters} from "@welshman/util" -import type {Filter, ExtensibleTrustedEvent} from "@welshman/util" +import type {Filter, CustomEvent} from "@welshman/util" export const getter = (store: Readable) => { let value: T @@ -26,20 +26,23 @@ export const custom = (start: Start, opts: {throttle?: number} = {}) => { let value: T let stop: () => void + const set = (newValue: T) => { + for (const sub of subs) { + sub(newValue) + } + + value = newValue + } + return { + set, subscribe: (sub: Sub) => { if (opts.throttle) { sub = throttle(opts.throttle, sub) } if (subs.length === 0) { - stop = start((newValue: T) => { - for (const sub of subs) { - sub(newValue) - } - - value = newValue - }) + stop = start(set) } subs.push(sub) @@ -69,7 +72,7 @@ export const throttled = (delay: number, store: Readable) => custom(set => store.subscribe(throttle(delay, set))) export const createEventStore = (repository: Repository) => { - let subs: Sub[] = [] + let subs: Sub[] = [] const onUpdate = throttle(300, () => { const $events = repository.dump() @@ -81,8 +84,8 @@ export const createEventStore = (repository: Repository) => { return { get: () => repository.dump(), - set: (events: ExtensibleTrustedEvent[]) => repository.load(events), - subscribe: (f: Sub) => { + set: (events: CustomEvent[]) => repository.load(events), + subscribe: (f: Sub) => { f(repository.dump()) subs.push(f) @@ -111,16 +114,17 @@ export const deriveEventsMapped = ({ }: { filters: Filter[] repository: Repository, - eventToItem: (event: ExtensibleTrustedEvent) => T - itemToEvent: (item: T) => ExtensibleTrustedEvent + eventToItem: (event: CustomEvent) => T | Promise + itemToEvent: (item: T) => CustomEvent includeDeleted?: boolean }) => custom(setter => { - let data = repository.query(filters, {includeDeleted}).map(eventToItem).filter(identity) + let data = repository.query(filters, {includeDeleted}).map(eventToItem).filter(identity) as T[] + const deferred = new Set() setter(data) - const onUpdate = batch(300, (updates: {added: ExtensibleTrustedEvent[]; removed: Set}[]) => { + const onUpdate = batch(300, (updates: {added: CustomEvent[]; removed: Set}[]) => { const removed = new Set() const added = new Map() @@ -133,6 +137,7 @@ export const deriveEventsMapped = ({ for (const id of update.removed) { removed.add(id) added.delete(id) + deferred.delete(id) } } @@ -141,9 +146,22 @@ export const deriveEventsMapped = ({ if (matchFilters(filters, event)) { const item = eventToItem(event) - if (item) { + if (item instanceof Promise) { + // If it's a promise, resolve it before adding it to data + deferred.add(event.id) + + // Wait for the promise to resolve. If it hasn't since been removed, + // we're clear to add it + item.then($item => { + if (deferred.has(event.id)) { + deferred.delete(event.id) + data.push($item) + setter(data) + } + }) + } else if (item) { dirty = true - data.push(item) + data.push(item as T) } } } @@ -168,26 +186,28 @@ export const deriveEventsMapped = ({ repository.on("update", onUpdate) return () => repository.off("update", onUpdate) + }, { + throttle: 300, }) -export const deriveEvents = (repository: Repository, opts: {filters: Filter[]; includeDeleted?: boolean}) => - deriveEventsMapped({ +export const deriveEvents = (opts: {repository: Repository, filters: Filter[], includeDeleted?: boolean}) => + deriveEventsMapped({ ...opts, - repository, eventToItem: identity, itemToEvent: identity, }) -export const deriveEvent = (repository: Repository, idOrAddress: string) => +export const deriveEvent = ({repository, idOrAddress}: {repository: Repository, idOrAddress: string}) => derived( - deriveEvents(repository, { + deriveEvents({ + repository, filters: getIdFilters([idOrAddress]), includeDeleted: true, }), first ) -export const deriveIsDeletedByAddress = (repository: Repository, event: ExtensibleTrustedEvent) => +export const deriveIsDeletedByAddress = ({repository, event}: {repository: Repository, event: CustomEvent}) => custom(setter => { setter(repository.isDeletedByAddress(event)) diff --git a/packages/domain/src/util.ts b/packages/util/src/Encryptable.ts similarity index 80% rename from packages/domain/src/util.ts rename to packages/util/src/Encryptable.ts index a8e8a6e..cf6c21f 100644 --- a/packages/domain/src/util.ts +++ b/packages/util/src/Encryptable.ts @@ -1,12 +1,12 @@ -import type {EventContent, ExtensibleTrustedEvent} from "@welshman/util" +import type {EventContent, CustomEvent} from './Events' export type Encrypt = (x: string) => Promise -export type DecryptedEvent = ExtensibleTrustedEvent & { +export type DecryptedEvent = CustomEvent & { plaintext: Partial } -export const asDecryptedEvent = (event: ExtensibleTrustedEvent, plaintext: Partial) => +export const asDecryptedEvent = (event: CustomEvent, plaintext: Partial) => ({...event, plaintext}) as DecryptedEvent export class Encryptable> { diff --git a/packages/util/src/Events.ts b/packages/util/src/Events.ts index cce8f02..7d82531 100644 --- a/packages/util/src/Events.ts +++ b/packages/util/src/Events.ts @@ -38,7 +38,8 @@ export type TrustedEvent = HashedEvent & { [verifiedSymbol]?: boolean } -export type ExtensibleTrustedEvent = TrustedEvent & Record +/* eslint @typescript-eslint/no-empty-interface: 0 */ +export interface CustomEvent extends TrustedEvent {} export type CreateEventOpts = { content?: string diff --git a/packages/util/src/Filters.ts b/packages/util/src/Filters.ts index 23e2673..5aa9646 100644 --- a/packages/util/src/Filters.ts +++ b/packages/util/src/Filters.ts @@ -1,7 +1,6 @@ -import {Event} from 'nostr-tools' import {matchFilter as nostrToolsMatchFilter} from 'nostr-tools' import {uniqBy, prop, mapVals, shuffle, avg, hash, groupBy, randomId, uniq} from '@welshman/lib' -import type {HashedEvent, TrustedEvent} from './Events' +import type {HashedEvent, CustomEvent, SignedEvent} from './Events' import {isReplaceableKind} from './Kinds' import {Address, getAddress} from './Address' @@ -19,7 +18,7 @@ export type Filter = { } export const matchFilter = (filter: Filter, event: E) => { - if (!nostrToolsMatchFilter(filter, event as unknown as Event)) { + if (!nostrToolsMatchFilter(filter, event as unknown as SignedEvent)) { return false } @@ -156,7 +155,7 @@ export const getIdFilters = (idsOrAddresses: string[]) => { return filters } -export const getReplyFilters = (events: TrustedEvent[], filter: Filter) => { +export const getReplyFilters = (events: CustomEvent[], filter: Filter) => { const a = [] const e = [] diff --git a/packages/domain/src/handler.ts b/packages/util/src/Handler.ts similarity index 63% rename from packages/domain/src/handler.ts rename to packages/util/src/Handler.ts index 4f11a41..0b38767 100644 --- a/packages/domain/src/handler.ts +++ b/packages/util/src/Handler.ts @@ -1,6 +1,7 @@ -import {fromPairs, parseJson} from "@welshman/lib" -import {getAddress, Tags} from "@welshman/util" -import type {ExtensibleTrustedEvent} from "@welshman/util" +import {fromPairs, last, first, parseJson} from "@welshman/lib" +import {getAddress} from "./Address" +import {getAddressTags, getKindTagValues} from "./Tags" +import type {CustomEvent} from "./Events" export type Handler = { kind: number @@ -8,13 +9,13 @@ export type Handler = { about: string image: string identifier: string - event: ExtensibleTrustedEvent + event: CustomEvent website?: string lud16?: string nip05?: string } -export const readHandlers = (event: ExtensibleTrustedEvent) => { +export const readHandlers = (event: CustomEvent) => { const {d: identifier} = fromPairs(event.tags) const meta = parseJson(event.content) const normalizedMeta = { @@ -31,10 +32,7 @@ export const readHandlers = (event: ExtensibleTrustedEvent) => { return [] } - return Tags.fromEvent(event) - .whereKey("k") - .values() - .valueOf() + return getKindTagValues(event.tags) .map(kind => ({...normalizedMeta, kind: parseInt(kind), identifier, event})) as Handler[] } @@ -42,9 +40,9 @@ export const getHandlerKey = (handler: Handler) => `${handler.kind}:${getAddress export const displayHandler = (handler?: Handler, fallback = "") => handler?.name || fallback -export const getHandlerAddress = (event: ExtensibleTrustedEvent) => { - const tags = Tags.fromEvent(event).whereKey("a") - const tag = tags.filter(t => t.last() === "web").first() || tags.first() +export const getHandlerAddress = (event: CustomEvent) => { + const tags = getAddressTags(event.tags) + const tag = tags.find(t => last(t) === "web") || first(tags) - return tag?.value() + return tag?.[1] } diff --git a/packages/domain/src/list.ts b/packages/util/src/List.ts similarity index 79% rename from packages/domain/src/list.ts rename to packages/util/src/List.ts index 20ddd46..a31b30b 100644 --- a/packages/domain/src/list.ts +++ b/packages/util/src/List.ts @@ -1,6 +1,7 @@ -import {parseJson} from "@welshman/lib" -import {Address, isShareableRelayUrl} from "@welshman/util" -import {Encryptable, DecryptedEvent} from "./util" +import {parseJson, nth, nthEq} from "@welshman/lib" +import {Address} from "./Address" +import {isShareableRelayUrl} from "./Relay" +import {Encryptable, DecryptedEvent} from "./Encryptable" export type ListParams = { kind: number @@ -43,3 +44,6 @@ export const createList = ({kind, publicTags = [], privateTags = []}: List) => export const editList = ({kind, publicTags = [], privateTags = []}: PublishedList) => new Encryptable({kind, tags: publicTags}, {content: JSON.stringify(privateTags)}) + +export const getListValues = (tagName: string, list: List | undefined) => + [...list?.publicTags || [], ...list?.privateTags || []].filter(nthEq(0, tagName)).map(nth(1)) diff --git a/packages/domain/src/profile.ts b/packages/util/src/Profile.ts similarity index 89% rename from packages/domain/src/profile.ts rename to packages/util/src/Profile.ts index cebd28a..4acad77 100644 --- a/packages/domain/src/profile.ts +++ b/packages/util/src/Profile.ts @@ -1,6 +1,7 @@ import {nip19} from "nostr-tools" import {ellipsize, parseJson} from "@welshman/lib" -import {PROFILE, ExtensibleTrustedEvent} from "@welshman/util" +import {CustomEvent} from "./Events" +import {PROFILE} from "./Kinds" export type Profile = { name?: string @@ -12,11 +13,11 @@ export type Profile = { picture?: string website?: string display_name?: string - event?: ExtensibleTrustedEvent + event?: CustomEvent } export type PublishedProfile = Omit & { - event: ExtensibleTrustedEvent + event: CustomEvent } export const isPublishedProfile = (profile: Profile): profile is PublishedProfile => @@ -35,7 +36,7 @@ export const makeProfile = (profile: Partial = {}): Profile => ({ ...profile, }) -export const readProfile = (event: ExtensibleTrustedEvent) => { +export const readProfile = (event: CustomEvent) => { const profile = parseJson(event.content) || {} return {...profile, event} as PublishedProfile diff --git a/packages/util/src/Relay.ts b/packages/util/src/Relay.ts index 1f7de30..fb77a46 100644 --- a/packages/util/src/Relay.ts +++ b/packages/util/src/Relay.ts @@ -1,22 +1,45 @@ -import {Emitter, normalizeUrl, sleep, stripProtocol} from '@welshman/lib' +import {last, Emitter, normalizeUrl, sleep, stripProtocol} from '@welshman/lib' import {matchFilters} from './Filters' import type {Repository} from './Repository' import type {Filter} from './Filters' -import type {ExtensibleTrustedEvent} from './Events' +import type {CustomEvent} from './Events' + +// Constants and types export const LOCAL_RELAY_URL = "local://welshman.relay" export const BOGUS_RELAY_URL = "bogus://welshman.relay" +export type RelayProfile = { + url: string + name?: string + pubkey?: string + contact?: string + description?: string + supported_nips?: number[] + limitation?: { + payment_required?: boolean + auth_required?: boolean + } +} + +// Utils related to bare urls + +export const isRelayUrl = (url: string) => { + try { + new URL(url) + } catch (e) { + return false + } + + return true +} + export const isShareableRelayUrl = (url: string) => Boolean( - typeof url === 'string' && + isRelayUrl(url) && // Is it actually a websocket url and has a dot url.match(/^wss:\/\/.+\..+/) && - // Sometimes bugs cause multiple relays to get concatenated - url.match(/:\/\//g)?.length === 1 && - // It shouldn't have any whitespace, url-encoded or otherwise - !url.match(/\s|%/) && // Don't match stuff with a port number !url.slice(6).match(/:\d+/) && // Don't match stuff with a numeric tld @@ -48,6 +71,10 @@ export const normalizeRelayUrl = (url: string, {allowInsecure = false}: Normaliz return prefix + url } +export const displayRelayUrl = (url: string) => last(url.split("://")).replace(/\/$/, "") + +// In-memory relay implementation backed by Repository + export class Relay extends Emitter { subs = new Map() @@ -57,13 +84,13 @@ export class Relay extends Emitter { send(type: string, ...message: any[]) { switch(type) { - case 'EVENT': return this.handleEVENT(message as [ExtensibleTrustedEvent]) + case 'EVENT': return this.handleEVENT(message as [CustomEvent]) case 'CLOSE': return this.handleCLOSE(message as [string]) case 'REQ': return this.handleREQ(message as [string, ...Filter[]]) } } - handleEVENT([event]: [ExtensibleTrustedEvent]) { + handleEVENT([event]: [CustomEvent]) { this.repository.publish(event) // Callers generally expect async relays diff --git a/packages/util/src/Repository.ts b/packages/util/src/Repository.ts index 4869a0a..7269cf6 100644 --- a/packages/util/src/Repository.ts +++ b/packages/util/src/Repository.ts @@ -4,19 +4,19 @@ import {EPOCH, matchFilter} from './Filters' import {isReplaceable, isTrustedEvent} from './Events' import {getAddress} from './Address' import type {Filter} from './Filters' -import type {ExtensibleTrustedEvent} from './Events' +import type {CustomEvent} from './Events' export const DAY = 86400 const getDay = (ts: number) => Math.floor(ts / DAY) export class Repository extends Emitter { - eventsById = new Map() - eventsByWrap = new Map() - eventsByAddress = new Map() - eventsByTag = new Map() - eventsByDay = new Map() - eventsByAuthor = new Map() + eventsById = new Map() + eventsByWrap = new Map() + eventsByAddress = new Map() + eventsByTag = new Map() + eventsByDay = new Map() + eventsByAuthor = new Map() deletes = new Map() // Dump/load/clear @@ -25,7 +25,7 @@ export class Repository extends Emitter { return Array.from(this.eventsById.values()) } - load = async (events: ExtensibleTrustedEvent[], chunkSize = 1000) => { + load = async (events: CustomEvent[], chunkSize = 1000) => { this.clear() const added = [] @@ -69,7 +69,7 @@ export class Repository extends Emitter { : this.eventsById.get(idOrAddress) } - hasEvent = (event: ExtensibleTrustedEvent) => { + hasEvent = (event: CustomEvent) => { const duplicate = ( this.eventsById.get(event.id) || this.eventsByAddress.get(getAddress(event)) @@ -79,12 +79,12 @@ export class Repository extends Emitter { } query = (filters: Filter[], {includeDeleted = false} = {}) => { - const result: ExtensibleTrustedEvent[][] = [] + const result: CustomEvent[][] = [] for (let filter of filters) { - let events: ExtensibleTrustedEvent[] = Array.from(this.eventsById.values()) + let events: CustomEvent[] = Array.from(this.eventsById.values()) if (filter.ids) { - events = filter.ids!.map(id => this.eventsById.get(id)).filter(identity) as ExtensibleTrustedEvent[] + events = filter.ids!.map(id => this.eventsById.get(id)).filter(identity) as CustomEvent[] filter = omit(['ids'], filter) } else if (filter.authors) { events = uniq(filter.authors!.flatMap(pubkey => this.eventsByAuthor.get(pubkey) || [])) @@ -112,8 +112,8 @@ export class Repository extends Emitter { } } - const chunk: ExtensibleTrustedEvent[] = [] - for (const event of sortBy((e: ExtensibleTrustedEvent) => -e.created_at, events)) { + const chunk: CustomEvent[] = [] + for (const event of sortBy((e: CustomEvent) => -e.created_at, events)) { if (filter.limit && chunk.length >= filter.limit) { break } @@ -133,7 +133,7 @@ export class Repository extends Emitter { return uniq(flatten(result)) } - publish = (event: ExtensibleTrustedEvent, {shouldNotify = true} = {}): boolean => { + publish = (event: CustomEvent, {shouldNotify = true} = {}): boolean => { if (!isTrustedEvent(event)) { throw new Error("Invalid event published to Repository", event) } @@ -203,19 +203,19 @@ export class Repository extends Emitter { return true } - isDeletedByAddress = (event: ExtensibleTrustedEvent) => (this.deletes.get(getAddress(event)) || 0) > event.created_at + isDeletedByAddress = (event: CustomEvent) => (this.deletes.get(getAddress(event)) || 0) > event.created_at - isDeletedById = (event: ExtensibleTrustedEvent) => (this.deletes.get(event.id) || 0) > event.created_at + isDeletedById = (event: CustomEvent) => (this.deletes.get(event.id) || 0) > event.created_at - isDeleted = (event: ExtensibleTrustedEvent) => this.isDeletedByAddress(event) || this.isDeletedById(event) + isDeleted = (event: CustomEvent) => this.isDeletedByAddress(event) || this.isDeletedById(event) // Utilities - _updateIndex(m: Map, k: K, e: ExtensibleTrustedEvent, duplicate?: ExtensibleTrustedEvent) { + _updateIndex(m: Map, k: K, e: CustomEvent, duplicate?: CustomEvent) { let a = m.get(k) || [] if (duplicate) { - a = a.filter((x: ExtensibleTrustedEvent) => x !== duplicate) + a = a.filter((x: CustomEvent) => x !== duplicate) } a.push(e) diff --git a/packages/util/src/Router.ts b/packages/util/src/Router.ts index 7b9e96c..0ad4cf6 100644 --- a/packages/util/src/Router.ts +++ b/packages/util/src/Router.ts @@ -1,6 +1,6 @@ import {first, splitAt, identity, sortBy, uniq, shuffle, pushToMapKey} from '@welshman/lib' import {Tags} from './Tags' -import type {TrustedEvent} from './Events' +import type {CustomEvent} from './Events' import {isShareableRelayUrl} from './Relay' import {isCommunityAddress, isGroupAddress} from './Address' @@ -172,19 +172,19 @@ export class Router { this.getPubkeySelection(pubkey, RelayMode.Inbox), ]).policy(this.addMinimalFallbacks) - Event = (event: TrustedEvent) => + Event = (event: CustomEvent) => this.scenario(this.forceValue(event.id, [ this.getPubkeySelection(event.pubkey, RelayMode.Write), ...this.getContextSelections(Tags.fromEvent(event).context()), ])) - EventChildren = (event: TrustedEvent) => + EventChildren = (event: CustomEvent) => this.scenario(this.forceValue(event.id, [ this.getPubkeySelection(event.pubkey, RelayMode.Read), ...this.getContextSelections(Tags.fromEvent(event).context()), ])) - EventAncestors = (event: TrustedEvent, type: "mentions" | "replies" | "roots") => { + EventAncestors = (event: CustomEvent, type: "mentions" | "replies" | "roots") => { const tags = Tags.fromEvent(event) const ancestors = tags.ancestors()[type] const pubkeys = tags.values("p").valueOf() @@ -201,13 +201,13 @@ export class Router { return this.product(ancestors.values().valueOf(), relays) } - EventMentions = (event: TrustedEvent) => this.EventAncestors(event, "mentions") + EventMentions = (event: CustomEvent) => this.EventAncestors(event, "mentions") - EventParents = (event: TrustedEvent) => this.EventAncestors(event, "replies") + EventParents = (event: CustomEvent) => this.EventAncestors(event, "replies") - EventRoots = (event: TrustedEvent) => this.EventAncestors(event, "roots") + EventRoots = (event: CustomEvent) => this.EventAncestors(event, "roots") - PublishEvent = (event: TrustedEvent) => { + PublishEvent = (event: CustomEvent) => { const tags = Tags.fromEvent(event) const mentions = tags.values("p").valueOf() diff --git a/packages/util/src/Tags.ts b/packages/util/src/Tags.ts index 226e94e..ed9f1aa 100644 --- a/packages/util/src/Tags.ts +++ b/packages/util/src/Tags.ts @@ -1,7 +1,7 @@ import {EventTemplate} from 'nostr-tools' import type {OmitStatics} from '@welshman/lib' import {Fluent, ensurePlural} from '@welshman/lib' -import {isShareableRelayUrl, normalizeRelayUrl} from './Relay' +import {isRelayUrl, normalizeRelayUrl} from './Relay' import {Address, isContextAddress} from './Address' import {GROUP, COMMUNITY} from './Kinds' @@ -71,7 +71,7 @@ export class Tags extends (Fluent as OmitStatics, 'from' entries = () => this.mapTo(t => t.entry()) - relays = () => this.flatMap((t: Tag) => t.valueOf().filter(isShareableRelayUrl).map(url => normalizeRelayUrl(url))).uniq() + relays = () => this.flatMap((t: Tag) => t.valueOf().filter(isRelayUrl).map(url => normalizeRelayUrl(url))).uniq() topics = () => this.whereKey("t").values().map((t: string) => t.replace(/^#/, "")) @@ -240,10 +240,14 @@ export const getPubkeyTags = getTags(["p"], pk => pk.length === 64) export const getPubkeyTagValues = getTagValues(["p"], pk => pk.length === 64) -export const getRelayTags = getTags(["r", "relay"], isShareableRelayUrl) +export const getRelayTags = getTags(["r", "relay"], isRelayUrl) -export const getRelayTagValues = getTagValues(["r", "relay"], isShareableRelayUrl) +export const getRelayTagValues = getTagValues(["r", "relay"], isRelayUrl) export const getGroupTags = getTags(["h", "group"], h => Boolean(h.match(/^(.+)'(.+)$/))) export const getGroupTagValues = getTagValues(["h", "group"], h => Boolean(h.match(/^(.+)'(.+)$/))) + +export const getKindTags = getTags(["k"], h => Boolean(h.match(/^\d+$/))) + +export const getKindTagValues = getTagValues(["k"], h => Boolean(h.match(/^\d+$/))) diff --git a/packages/util/src/Zaps.ts b/packages/util/src/Zaps.ts index 218d28b..2586aaa 100644 --- a/packages/util/src/Zaps.ts +++ b/packages/util/src/Zaps.ts @@ -1,5 +1,5 @@ import {hexToBech32} from '@welshman/lib' -import type {TrustedEvent} from './Events' +import type {CustomEvent} from './Events' import {Tags} from "./Tags" const DIVISORS = { @@ -81,12 +81,12 @@ export type Zapper = { } export type Zap = { - request: TrustedEvent - response: TrustedEvent, + request: CustomEvent + response: CustomEvent, invoiceAmount: number } -export const zapFromEvent = (response: TrustedEvent, zapper: Zapper) => { +export const zapFromEvent = (response: CustomEvent, zapper: Zapper) => { const responseMeta = Tags.fromEvent(response).asObject() let zap: Zap diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 232e42a..1f34fb9 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -1,8 +1,12 @@ export * from './Address' +export * from './Encryptable' export * from './Events' export * from './Filters' +export * from './Handler' export * from './Kinds' export * from './Links' +export * from './List' +export * from './Profile' export * from './Relay' export * from './Repository' export * from './Router' diff --git a/packages/util/tsconfig.json b/packages/util/tsconfig.json index a6bbc7e..13631ba 100644 --- a/packages/util/tsconfig.json +++ b/packages/util/tsconfig.json @@ -5,7 +5,7 @@ "outDir": "build", "esModuleInterop": true, "skipLibCheck": true, - "lib": ["esnext"] + "lib": ["esnext", "dom"] }, "include": ["**/*.ts"] }