diff --git a/.gitignore b/.gitignore index 5bd99317..3fb207a9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ node_modules/ .idea .yarn yarn.lock +dist/ +*.tgz +*.log \ No newline at end of file diff --git a/package.json b/package.json index 8e2e17d5..d05a6602 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "packages/*" ], "scripts": { - "build": "yarn workspace @snort/nostr build && yarn workspace @snort/app build", - "start": "yarn workspace @snort/nostr build && yarn workspace @snort/app start" + "build": "yarn workspace @snort/system build && yarn workspace @snort/app build", + "start": "yarn workspace @snort/system build && yarn workspace @snort/app start", + "test": "yarn workspace @snort/app test" }, "devDependencies": { "@tauri-apps/cli": "^1.2.3", diff --git a/packages/app/.gitignore b/packages/app/.gitignore index 3a1bea68..3486a8a6 100644 --- a/packages/app/.gitignore +++ b/packages/app/.gitignore @@ -23,3 +23,5 @@ yarn-debug.log* yarn-error.log* .idea + +dist/ diff --git a/packages/app/jest.config.js b/packages/app/jest.config.js new file mode 100644 index 00000000..928edeee --- /dev/null +++ b/packages/app/jest.config.js @@ -0,0 +1,9 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + bail: true, + preset: "ts-jest", + testEnvironment: "jsdom", + roots: ["src"], + moduleDirectories: ["src", "node_modules"], + setupFiles: ["./src/setupTests.ts"], +}; diff --git a/packages/app/package.json b/packages/app/package.json index 6b487ed1..8bd40d54 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -12,11 +12,11 @@ "@reduxjs/toolkit": "^1.9.1", "@scure/bip32": "^1.3.0", "@scure/bip39": "^1.1.1", - "@snort/nostr": "^1.0.0", "@szhsin/react-menu": "^3.3.1", "@void-cat/api": "^1.0.4", "base32-decode": "^1.0.0", "bech32": "^2.0.0", + "debug": "^4.3.4", "dexie": "^3.2.2", "dexie-react-hooks": "^1.1.1", "dns-over-http-resolver": "^2.1.1", @@ -48,7 +48,7 @@ "scripts": { "start": "webpack serve", "build": "webpack --node-env=production", - "test": "", + "test": "jest --runInBand", "intl-extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file src/lang.json --flatten true", "intl-compile": "formatjs compile src/lang.json --out-file src/translations/en.json", "format": "prettier --write .", @@ -80,7 +80,8 @@ "@babel/preset-react": "^7.18.6", "@formatjs/cli": "^6.0.1", "@formatjs/ts-transformer": "^3.13.1", - "@types/jest": "^29.2.5", + "@types/debug": "^4.1.8", + "@types/jest": "^29.5.1", "@types/node": "^18.11.18", "@types/react": "^18.0.26", "@types/react-dom": "^18.0.10", @@ -99,9 +100,12 @@ "eslint-webpack-plugin": "^4.0.1", "html-webpack-plugin": "^5.5.1", "husky": ">=6", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", "lint-staged": ">=10", "mini-css-extract-plugin": "^2.7.5", "prettier": "2.8.3", + "ts-jest": "^29.1.0", "ts-loader": "^9.4.2", "typescript": "^5.0.4", "webpack": "^5.82.1", diff --git a/packages/app/src/Cache/DMCache.ts b/packages/app/src/Cache/DMCache.ts index a147559f..4041a0fc 100644 --- a/packages/app/src/Cache/DMCache.ts +++ b/packages/app/src/Cache/DMCache.ts @@ -1,13 +1,13 @@ -import { RawEvent } from "@snort/nostr"; +import { NostrEvent } from "@snort/system"; import { db } from "Db"; import FeedCache from "./FeedCache"; -class DMCache extends FeedCache { +class DMCache extends FeedCache { constructor() { super("DMCache", db.dms); } - key(of: RawEvent): string { + key(of: NostrEvent): string { return of.id; } @@ -23,11 +23,11 @@ class DMCache extends FeedCache { return ret; } - allDms(): Array { + allDms(): Array { return [...this.cache.values()]; } - takeSnapshot(): Array { + takeSnapshot(): Array { return this.allDms(); } } diff --git a/packages/app/src/Cache/EventInteractionCache.ts b/packages/app/src/Cache/EventInteractionCache.ts index 2ea3ac80..1a727c06 100644 --- a/packages/app/src/Cache/EventInteractionCache.ts +++ b/packages/app/src/Cache/EventInteractionCache.ts @@ -1,6 +1,6 @@ import { db, EventInteraction } from "Db"; import { LoginStore } from "Login"; -import { sha256 } from "Util"; +import { sha256 } from "SnortUtils"; import FeedCache from "./FeedCache"; class EventInteractionCache extends FeedCache { diff --git a/packages/app/src/Cache/FeedCache.ts b/packages/app/src/Cache/FeedCache.ts index 6300ebeb..1e62fb2d 100644 --- a/packages/app/src/Cache/FeedCache.ts +++ b/packages/app/src/Cache/FeedCache.ts @@ -1,6 +1,7 @@ import { db } from "Db"; +import debug from "debug"; import { Table } from "dexie"; -import { unixNowMs, unwrap } from "Util"; +import { unixNowMs, unwrap } from "SnortUtils"; type HookFn = () => void; @@ -11,31 +12,32 @@ interface HookFilter { export default abstract class FeedCache { #name: string; - #table: Table; #hooks: Array = []; #snapshot: Readonly> = []; #changed = true; #hits = 0; #miss = 0; + protected table: Table; protected onTable: Set = new Set(); protected cache: Map = new Map(); constructor(name: string, table: Table) { this.#name = name; - this.#table = table; + this.table = table; setInterval(() => { - console.debug( - `[${this.#name}] ${this.cache.size} loaded, ${this.onTable.size} on-disk, ${this.#hooks.length} hooks, ${( - (this.#hits / (this.#hits + this.#miss)) * - 100 - ).toFixed(1)} % hit` + debug(this.#name)( + "%d loaded, %d on-disk, %d hooks, %d% hit", + this.cache.size, + this.onTable.size, + this.#hooks.length, + ((this.#hits / (this.#hits + this.#miss)) * 100).toFixed(1) ); - }, 5_000); + }, 30_000); } async preload() { if (db.ready) { - const keys = await this.#table.toCollection().primaryKeys(); + const keys = await this.table.toCollection().primaryKeys(); this.onTable = new Set(keys.map(a => a as string)); } } @@ -73,7 +75,7 @@ export default abstract class FeedCache { async get(key?: string) { if (key && !this.cache.has(key) && db.ready) { - const cached = await this.#table.get(key); + const cached = await this.table.get(key); if (cached) { this.cache.set(this.key(cached), cached); this.notifyChange([key]); @@ -86,7 +88,7 @@ export default abstract class FeedCache { async bulkGet(keys: Array) { const missing = keys.filter(a => !this.cache.has(a)); if (missing.length > 0 && db.ready) { - const cached = await this.#table.bulkGet(missing); + const cached = await this.table.bulkGet(missing); cached.forEach(a => { if (a) { this.cache.set(this.key(a), a); @@ -103,7 +105,7 @@ export default abstract class FeedCache { const k = this.key(obj); this.cache.set(k, obj); if (db.ready) { - await this.#table.put(obj); + await this.table.put(obj); this.onTable.add(k); } this.notifyChange([k]); @@ -111,13 +113,44 @@ export default abstract class FeedCache { async bulkSet(obj: Array) { if (db.ready) { - await this.#table.bulkPut(obj); + await this.table.bulkPut(obj); obj.forEach(a => this.onTable.add(this.key(a))); } obj.forEach(v => this.cache.set(this.key(v), v)); this.notifyChange(obj.map(a => this.key(a))); } + /** + * Try to update an entry where created values exists + * @param m Profile metadata + * @returns + */ + async update(m: TCachedWithCreated) { + const k = this.key(m); + const existing = this.getFromCache(k) as TCachedWithCreated; + const updateType = (() => { + if (!existing) { + return "new"; + } + if (existing.created < m.created) { + return "updated"; + } + if (existing && existing.loaded < m.loaded) { + return "refresh"; + } + return "no_change"; + })(); + debug(this.#name)("Updating %s %s %o", k, updateType, m); + if (updateType !== "no_change") { + const updated = { + ...existing, + ...m, + }; + await this.set(updated); + } + return updateType; + } + /** * Loads a list of rows from disk cache * @param keys List of ids to load @@ -131,16 +164,17 @@ export default abstract class FeedCache { key: a, })); const start = unixNowMs(); - const fromCache = await this.#table.bulkGet(mapped.filter(a => a.has).map(a => a.key)); + const fromCache = await this.table.bulkGet(mapped.filter(a => a.has).map(a => a.key)); const fromCacheFiltered = fromCache.filter(a => a !== undefined).map(a => unwrap(a)); fromCacheFiltered.forEach(a => { this.cache.set(this.key(a), a); }); this.notifyChange(fromCacheFiltered.map(a => this.key(a))); - console.debug( - `[${this.#name}] Loaded ${fromCacheFiltered.length}/${keys.length} in ${( - unixNowMs() - start - ).toLocaleString()} ms` + debug(this.#name)( + `Loaded %d/%d in %d ms`, + fromCacheFiltered.length, + keys.length, + (unixNowMs() - start).toLocaleString() ); return mapped.filter(a => !a.has).map(a => a.key); } @@ -150,7 +184,7 @@ export default abstract class FeedCache { } async clear() { - await this.#table.clear(); + await this.table.clear(); this.cache.clear(); this.onTable.clear(); } diff --git a/packages/app/src/Cache/UserCache.ts b/packages/app/src/Cache/UserCache.ts index ab6feade..1b9ce04b 100644 --- a/packages/app/src/Cache/UserCache.ts +++ b/packages/app/src/Cache/UserCache.ts @@ -1,6 +1,6 @@ import FeedCache from "Cache/FeedCache"; import { db } from "Db"; -import { MetadataCache } from "Cache"; +import { MetadataCache } from "@snort/system"; import { LNURL } from "LNURL"; import { fetchNip05Pubkey } from "Nip05/Verifier"; @@ -18,6 +18,14 @@ class UserProfileCache extends FeedCache { return of.pubkey; } + override async preload(follows?: Array): Promise { + await super.preload(); + // load follows profiles + if (follows) { + await this.buffer(follows); + } + } + async search(q: string): Promise> { if (db.ready) { // on-disk cache will always have more data @@ -53,35 +61,15 @@ class UserProfileCache extends FeedCache { * @param m Profile metadata * @returns */ - async update(m: MetadataCache) { - const existing = this.getFromCache(m.pubkey); - const updateType = (() => { - if (!existing) { - return "new_profile"; - } - if (existing.created < m.created) { - return "updated_profile"; - } - if (existing && existing.loaded < m.loaded) { - return "refresh_profile"; - } - return "no_change"; - })(); - console.debug(`Updating ${m.pubkey} ${updateType}`, m); - if (updateType !== "no_change") { - const writeProfile = { - ...existing, - ...m, - }; - await this.#setItem(writeProfile); - if (updateType !== "refresh_profile") { - const lnurl = m.lud16 ?? m.lud06; - if (lnurl) { - this.#zapperQueue.push({ - pubkey: m.pubkey, - lnurl, - }); - } + override async update(m: MetadataCache) { + const updateType = await super.update(m); + if (updateType !== "refresh") { + const lnurl = m.lud16 ?? m.lud06; + if (lnurl) { + this.#zapperQueue.push({ + pubkey: m.pubkey, + lnurl, + }); } if (m.nip05) { this.#nip5Queue.push({ @@ -97,15 +85,6 @@ class UserProfileCache extends FeedCache { return []; } - async #setItem(m: MetadataCache) { - this.cache.set(m.pubkey, m); - if (db.ready) { - await db.users.put(m); - this.onTable.add(m.pubkey); - } - this.notifyChange([m.pubkey]); - } - async #processZapperQueue() { await this.#batchQueue( this.#zapperQueue, @@ -114,7 +93,7 @@ class UserProfileCache extends FeedCache { await svc.load(); const p = this.getFromCache(i.pubkey); if (p) { - this.#setItem({ + await this.set({ ...p, zapService: svc.zapperPubkey, }); @@ -134,7 +113,7 @@ class UserProfileCache extends FeedCache { const nip5pk = await fetchNip05Pubkey(name, domain); const p = this.getFromCache(i.pubkey); if (p) { - this.#setItem({ + await this.set({ ...p, isNostrAddressValid: i.pubkey === nip5pk, }); diff --git a/packages/app/src/Cache/UserRelayCache.ts b/packages/app/src/Cache/UserRelayCache.ts new file mode 100644 index 00000000..c0c9fb5a --- /dev/null +++ b/packages/app/src/Cache/UserRelayCache.ts @@ -0,0 +1,31 @@ +import { db, UsersRelays } from "Db"; +import FeedCache from "./FeedCache"; + +export class UsersRelaysCache extends FeedCache { + constructor() { + super("UserRelays", db.userRelays); + } + + key(of: UsersRelays): string { + return of.pubkey; + } + + override async preload(follows?: Array): Promise { + await super.preload(); + if (follows) { + await this.buffer(follows); + } + } + + newest(): number { + let ret = 0; + this.cache.forEach(v => (ret = v.created_at > ret ? v.created_at : ret)); + return ret; + } + + takeSnapshot(): Array { + return [...this.cache.values()]; + } +} + +export const UserRelays = new UsersRelaysCache(); diff --git a/packages/app/src/Cache/index.ts b/packages/app/src/Cache/index.ts index b8ac6bc4..ec3f6c0f 100644 --- a/packages/app/src/Cache/index.ts +++ b/packages/app/src/Cache/index.ts @@ -1,60 +1,16 @@ -import { HexKey, RawEvent, UserMetadata } from "@snort/nostr"; -import { hexToBech32, unixNowMs } from "Util"; import { DmCache } from "./DMCache"; import { InteractionCache } from "./EventInteractionCache"; import { UserCache } from "./UserCache"; +import { UserRelays } from "./UserRelayCache"; -export interface MetadataCache extends UserMetadata { - /** - * When the object was saved in cache - */ - loaded: number; - - /** - * When the source metadata event was created - */ - created: number; - - /** - * The pubkey of the owner of this metadata - */ - pubkey: HexKey; - - /** - * The bech32 encoded pubkey - */ - npub: string; - - /** - * Pubkey of zapper service - */ - zapService?: HexKey; - - /** - * If the nip05 is valid for this user - */ - isNostrAddressValid: boolean; -} - -export function mapEventToProfile(ev: RawEvent) { - try { - const data: UserMetadata = JSON.parse(ev.content); - return { - pubkey: ev.pubkey, - npub: hexToBech32("npub", ev.pubkey), - created: ev.created_at, - ...data, - loaded: unixNowMs(), - } as MetadataCache; - } catch (e) { - console.error("Failed to parse JSON", ev, e); - } -} - -export async function preload() { - await UserCache.preload(); - await DmCache.preload(); - await InteractionCache.preload(); +export async function preload(follows?: Array) { + const preloads = [ + UserCache.preload(follows), + DmCache.preload(), + InteractionCache.preload(), + UserRelays.preload(follows), + ]; + await Promise.all(preloads); } export { UserCache, DmCache }; diff --git a/packages/app/src/Const.ts b/packages/app/src/Const.ts index 7dd90867..db961bc6 100644 --- a/packages/app/src/Const.ts +++ b/packages/app/src/Const.ts @@ -1,4 +1,4 @@ -import { RelaySettings } from "@snort/nostr"; +import { RelaySettings } from "@snort/system"; /** * Add-on api for snort features @@ -33,7 +33,7 @@ export const DefaultConnectTimeout = 2000; /** * How long profile cache should be considered valid for */ -export const ProfileCacheExpire = 1_000 * 60 * 30; +export const ProfileCacheExpire = 1_000 * 60 * 60 * 6; /** * Default bootstrap relays diff --git a/packages/app/src/Db/index.ts b/packages/app/src/Db/index.ts index 30be5c7c..e8d1724c 100644 --- a/packages/app/src/Db/index.ts +++ b/packages/app/src/Db/index.ts @@ -1,6 +1,5 @@ import Dexie, { Table } from "dexie"; -import { FullRelaySettings, HexKey, RawEvent, u256 } from "@snort/nostr"; -import { MetadataCache } from "Cache"; +import { FullRelaySettings, HexKey, NostrEvent, u256, MetadataCache } from "@snort/system"; export const NAME = "snortDB"; export const VERSION = 9; @@ -21,6 +20,7 @@ export interface RelayMetrics { export interface UsersRelays { pubkey: HexKey; + created_at: number; relays: FullRelaySettings[]; } @@ -55,8 +55,8 @@ export class SnortDB extends Dexie { users!: Table; relayMetrics!: Table; userRelays!: Table; - events!: Table; - dms!: Table; + events!: Table; + dms!: Table; eventInteraction!: Table; payments!: Table; diff --git a/packages/app/src/Element/Avatar.tsx b/packages/app/src/Element/Avatar.tsx index 04a0fa0d..89fef4aa 100644 --- a/packages/app/src/Element/Avatar.tsx +++ b/packages/app/src/Element/Avatar.tsx @@ -2,7 +2,7 @@ import "./Avatar.css"; import Nostrich from "nostrich.webp"; import { CSSProperties, useEffect, useState } from "react"; -import type { UserMetadata } from "@snort/nostr"; +import type { UserMetadata } from "@snort/system"; import useImgProxy from "Hooks/useImgProxy"; diff --git a/packages/app/src/Element/AvatarEditor.tsx b/packages/app/src/Element/AvatarEditor.tsx index 690fc189..b7b5776a 100644 --- a/packages/app/src/Element/AvatarEditor.tsx +++ b/packages/app/src/Element/AvatarEditor.tsx @@ -1,7 +1,7 @@ import Icon from "Icons/Icon"; import { useState } from "react"; import useFileUpload from "Upload"; -import { openFile, unwrap } from "Util"; +import { openFile, unwrap } from "SnortUtils"; interface AvatarEditorProps { picture?: string; diff --git a/packages/app/src/Element/BadgeList.tsx b/packages/app/src/Element/BadgeList.tsx index 9eec2784..36088902 100644 --- a/packages/app/src/Element/BadgeList.tsx +++ b/packages/app/src/Element/BadgeList.tsx @@ -3,13 +3,13 @@ import "./BadgeList.css"; import { useState } from "react"; import { FormattedMessage } from "react-intl"; -import { TaggedRawEvent } from "@snort/nostr"; +import { TaggedRawEvent } from "@snort/system"; import { ProxyImg } from "Element/ProxyImg"; import Icon from "Icons/Icon"; import Modal from "Element/Modal"; import Username from "Element/Username"; -import { findTag } from "Util"; +import { findTag } from "SnortUtils"; export default function BadgeList({ badges }: { badges: TaggedRawEvent[] }) { const [showModal, setShowModal] = useState(false); diff --git a/packages/app/src/Element/BlockButton.tsx b/packages/app/src/Element/BlockButton.tsx index e908192d..3245d76d 100644 --- a/packages/app/src/Element/BlockButton.tsx +++ b/packages/app/src/Element/BlockButton.tsx @@ -1,5 +1,5 @@ import { FormattedMessage } from "react-intl"; -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; import useModeration from "Hooks/useModeration"; import messages from "./messages"; diff --git a/packages/app/src/Element/Bookmarks.tsx b/packages/app/src/Element/Bookmarks.tsx index dda75eec..118cf917 100644 --- a/packages/app/src/Element/Bookmarks.tsx +++ b/packages/app/src/Element/Bookmarks.tsx @@ -1,6 +1,6 @@ import { useState, useMemo, ChangeEvent } from "react"; import { FormattedMessage } from "react-intl"; -import { HexKey, TaggedRawEvent } from "@snort/nostr"; +import { HexKey, TaggedRawEvent } from "@snort/system"; import Note from "Element/Note"; import useLogin from "Hooks/useLogin"; diff --git a/packages/app/src/Element/DM.tsx b/packages/app/src/Element/DM.tsx index dfc8911b..2a8e9efa 100644 --- a/packages/app/src/Element/DM.tsx +++ b/packages/app/src/Element/DM.tsx @@ -2,13 +2,13 @@ import "./DM.css"; import { useEffect, useState } from "react"; import { useIntl } from "react-intl"; import { useInView } from "react-intersection-observer"; -import { TaggedRawEvent } from "@snort/nostr"; +import { TaggedRawEvent } from "@snort/system"; import useEventPublisher from "Feed/EventPublisher"; import NoteTime from "Element/NoteTime"; import Text from "Element/Text"; import { setLastReadDm } from "Pages/MessagesPage"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; import useLogin from "Hooks/useLogin"; import messages from "./messages"; diff --git a/packages/app/src/Element/DmWindow.tsx b/packages/app/src/Element/DmWindow.tsx index 79244a4a..b56e95c0 100644 --- a/packages/app/src/Element/DmWindow.tsx +++ b/packages/app/src/Element/DmWindow.tsx @@ -1,6 +1,6 @@ import "./DmWindow.css"; import { useEffect, useMemo, useRef } from "react"; -import { TaggedRawEvent } from "@snort/nostr"; +import { TaggedRawEvent } from "@snort/system"; import ProfileImage from "Element/ProfileImage"; import DM from "Element/DM"; @@ -9,7 +9,7 @@ import NoteToSelf from "Element/NoteToSelf"; import { useDmCache } from "Hooks/useDmsCache"; import useLogin from "Hooks/useLogin"; import WriteDm from "Element/WriteDm"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; export default function DmWindow({ id }: { id: string }) { const pubKey = useLogin().publicKey; diff --git a/packages/app/src/Element/FollowButton.tsx b/packages/app/src/Element/FollowButton.tsx index 044f20e4..72e3fb33 100644 --- a/packages/app/src/Element/FollowButton.tsx +++ b/packages/app/src/Element/FollowButton.tsx @@ -1,9 +1,9 @@ import "./FollowButton.css"; import { FormattedMessage } from "react-intl"; -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; import useEventPublisher from "Feed/EventPublisher"; -import { parseId } from "Util"; +import { parseId } from "SnortUtils"; import useLogin from "Hooks/useLogin"; import AsyncButton from "Element/AsyncButton"; diff --git a/packages/app/src/Element/FollowListBase.tsx b/packages/app/src/Element/FollowListBase.tsx index 63de99a7..dfc7fdc5 100644 --- a/packages/app/src/Element/FollowListBase.tsx +++ b/packages/app/src/Element/FollowListBase.tsx @@ -1,6 +1,6 @@ import { ReactNode } from "react"; import { FormattedMessage } from "react-intl"; -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; import useEventPublisher from "Feed/EventPublisher"; import ProfilePreview from "Element/ProfilePreview"; diff --git a/packages/app/src/Element/HyperText.tsx b/packages/app/src/Element/HyperText.tsx index f7439a7c..83de733f 100644 --- a/packages/app/src/Element/HyperText.tsx +++ b/packages/app/src/Element/HyperText.tsx @@ -13,7 +13,7 @@ import { NostrNestsRegex, WavlakeRegex, } from "Const"; -import { magnetURIDecode } from "Util"; +import { magnetURIDecode } from "SnortUtils"; import SoundCloudEmbed from "Element/SoundCloudEmded"; import MixCloudEmbed from "Element/MixCloudEmbed"; import SpotifyEmbed from "Element/SpotifyEmbed"; diff --git a/packages/app/src/Element/Invoice.tsx b/packages/app/src/Element/Invoice.tsx index 2d6d1251..f8704c17 100644 --- a/packages/app/src/Element/Invoice.tsx +++ b/packages/app/src/Element/Invoice.tsx @@ -6,7 +6,7 @@ import { useMemo } from "react"; import SendSats from "Element/SendSats"; import Icon from "Icons/Icon"; import { useWallet } from "Wallet"; -import { decodeInvoice } from "Util"; +import { decodeInvoice } from "SnortUtils"; import messages from "./messages"; diff --git a/packages/app/src/Element/MagnetLink.tsx b/packages/app/src/Element/MagnetLink.tsx index 169a45cf..4dca7d5e 100644 --- a/packages/app/src/Element/MagnetLink.tsx +++ b/packages/app/src/Element/MagnetLink.tsx @@ -1,6 +1,6 @@ import { FormattedMessage } from "react-intl"; -import { Magnet } from "Util"; +import { Magnet } from "SnortUtils"; interface MagnetLinkProps { magnet: Magnet; diff --git a/packages/app/src/Element/Mention.tsx b/packages/app/src/Element/Mention.tsx index add8c869..9fe4723e 100644 --- a/packages/app/src/Element/Mention.tsx +++ b/packages/app/src/Element/Mention.tsx @@ -1,9 +1,9 @@ import { useMemo } from "react"; import { Link } from "react-router-dom"; -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; import { useUserProfile } from "Hooks/useUserProfile"; -import { profileLink } from "Util"; +import { profileLink } from "SnortUtils"; import { getDisplayName } from "Element/ProfileImage"; export default function Mention({ pubkey, relays }: { pubkey: HexKey; relays?: Array | string }) { diff --git a/packages/app/src/Element/MuteButton.tsx b/packages/app/src/Element/MuteButton.tsx index 7a8d05eb..b95d1e21 100644 --- a/packages/app/src/Element/MuteButton.tsx +++ b/packages/app/src/Element/MuteButton.tsx @@ -1,5 +1,5 @@ import { FormattedMessage } from "react-intl"; -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; import useModeration from "Hooks/useModeration"; import messages from "./messages"; diff --git a/packages/app/src/Element/MutedList.tsx b/packages/app/src/Element/MutedList.tsx index 4616182a..b3ecb946 100644 --- a/packages/app/src/Element/MutedList.tsx +++ b/packages/app/src/Element/MutedList.tsx @@ -1,5 +1,5 @@ import { FormattedMessage } from "react-intl"; -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; import MuteButton from "Element/MuteButton"; import ProfilePreview from "Element/ProfilePreview"; import useModeration from "Hooks/useModeration"; diff --git a/packages/app/src/Element/Nip05.tsx b/packages/app/src/Element/Nip05.tsx index 4d1fb286..5e1de7ab 100644 --- a/packages/app/src/Element/Nip05.tsx +++ b/packages/app/src/Element/Nip05.tsx @@ -1,5 +1,5 @@ import "./Nip05.css"; -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; import Icon from "Icons/Icon"; import { useUserProfile } from "Hooks/useUserProfile"; diff --git a/packages/app/src/Element/Nip5Service.tsx b/packages/app/src/Element/Nip5Service.tsx index 8feb5428..3eb16982 100644 --- a/packages/app/src/Element/Nip5Service.tsx +++ b/packages/app/src/Element/Nip5Service.tsx @@ -1,9 +1,9 @@ import { useEffect, useMemo, useState, ChangeEvent } from "react"; import { useIntl, FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; -import { UserMetadata } from "@snort/nostr"; +import { UserMetadata, mapEventToProfile } from "@snort/system"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; import { formatShort } from "Number"; import { ServiceProvider, @@ -19,10 +19,10 @@ import SendSats from "Element/SendSats"; import Copy from "Element/Copy"; import { useUserProfile } from "Hooks/useUserProfile"; import useEventPublisher from "Feed/EventPublisher"; -import { debounce } from "Util"; +import { debounce } from "SnortUtils"; import useLogin from "Hooks/useLogin"; import SnortServiceProvider from "Nip05/SnortServiceProvider"; -import { mapEventToProfile, UserCache } from "Cache"; +import { UserCache } from "Cache"; import messages from "./messages"; diff --git a/packages/app/src/Element/NostrFileHeader.tsx b/packages/app/src/Element/NostrFileHeader.tsx index 91bb015b..b4efdc0d 100644 --- a/packages/app/src/Element/NostrFileHeader.tsx +++ b/packages/app/src/Element/NostrFileHeader.tsx @@ -1,7 +1,7 @@ import { FormattedMessage } from "react-intl"; -import { RawEvent } from "@snort/nostr"; +import { NostrEvent, NostrLink } from "@snort/system"; -import { findTag, NostrLink } from "Util"; +import { findTag } from "SnortUtils"; import useEventFeed from "Feed/EventFeed"; import PageSpinner from "Element/PageSpinner"; import Reveal from "Element/Reveal"; @@ -14,7 +14,7 @@ export default function NostrFileHeader({ link }: { link: NostrLink }) { return ; } -export function NostrFileElement({ ev }: { ev: RawEvent }) { +export function NostrFileElement({ ev }: { ev: NostrEvent }) { // assume image or embed which can be rendered by the hypertext kind // todo: make use of hash // todo: use magnet or other links if present diff --git a/packages/app/src/Element/NostrLink.tsx b/packages/app/src/Element/NostrLink.tsx index c4f2a077..28ea734a 100644 --- a/packages/app/src/Element/NostrLink.tsx +++ b/packages/app/src/Element/NostrLink.tsx @@ -1,8 +1,7 @@ import { Link } from "react-router-dom"; -import { NostrPrefix } from "@snort/nostr"; +import { NostrPrefix, parseNostrLink } from "@snort/system"; import Mention from "Element/Mention"; -import { parseNostrLink } from "Util"; import NoteQuote from "Element/NoteQuote"; export default function NostrLink({ link, depth }: { link: string; depth?: number }) { diff --git a/packages/app/src/Element/Note.tsx b/packages/app/src/Element/Note.tsx index 273de2ea..10115754 100644 --- a/packages/app/src/Element/Note.tsx +++ b/packages/app/src/Element/Note.tsx @@ -3,7 +3,7 @@ import React, { useMemo, useState, useLayoutEffect, ReactNode } from "react"; import { useNavigate, Link } from "react-router-dom"; import { useInView } from "react-intersection-observer"; import { useIntl, FormattedMessage } from "react-intl"; -import { TaggedRawEvent, HexKey, EventKind, NostrPrefix, Lists } from "@snort/nostr"; +import { TaggedRawEvent, HexKey, EventKind, NostrPrefix, Lists, EventExt } from "@snort/system"; import useEventPublisher from "Feed/EventPublisher"; import Icon from "Icons/Icon"; @@ -19,14 +19,13 @@ import { normalizeReaction, Reaction, profileLink, -} from "Util"; +} from "SnortUtils"; import NoteFooter, { Translation } from "Element/NoteFooter"; import NoteTime from "Element/NoteTime"; import Reveal from "Element/Reveal"; import useModeration from "Hooks/useModeration"; import { UserCache } from "Cache/UserCache"; import Poll from "Element/Poll"; -import { EventExt } from "System/EventExt"; import useLogin from "Hooks/useLogin"; import { setBookmarked, setPinned } from "Login"; import { NostrFileElement } from "Element/NostrFileHeader"; diff --git a/packages/app/src/Element/NoteCreator.tsx b/packages/app/src/Element/NoteCreator.tsx index b1e5f59d..958cc179 100644 --- a/packages/app/src/Element/NoteCreator.tsx +++ b/packages/app/src/Element/NoteCreator.tsx @@ -1,11 +1,11 @@ import "./NoteCreator.css"; import { FormattedMessage, useIntl } from "react-intl"; import { useDispatch, useSelector } from "react-redux"; -import { encodeTLV, EventKind, NostrPrefix, TaggedRawEvent } from "@snort/nostr"; +import { encodeTLV, EventKind, NostrPrefix, TaggedRawEvent, EventBuilder } from "@snort/system"; import Icon from "Icons/Icon"; import useEventPublisher from "Feed/EventPublisher"; -import { openFile } from "Util"; +import { openFile } from "SnortUtils"; import Textarea from "Element/Textarea"; import Modal from "Element/Modal"; import ProfileImage from "Element/ProfileImage"; @@ -31,7 +31,6 @@ import { LNURL } from "LNURL"; import messages from "./messages"; import { ClipboardEventHandler, useState } from "react"; import Spinner from "Icons/Spinner"; -import { EventBuilder } from "System"; import { Menu, MenuItem } from "@szhsin/react-menu"; import { LoginStore } from "Login"; import { getCurrentSubscription } from "Subscription"; diff --git a/packages/app/src/Element/NoteFooter.tsx b/packages/app/src/Element/NoteFooter.tsx index 219738bb..800c4d31 100644 --- a/packages/app/src/Element/NoteFooter.tsx +++ b/packages/app/src/Element/NoteFooter.tsx @@ -3,14 +3,14 @@ import { useSelector, useDispatch } from "react-redux"; import { useIntl, FormattedMessage } from "react-intl"; import { Menu, MenuItem } from "@szhsin/react-menu"; import { useLongPress } from "use-long-press"; -import { TaggedRawEvent, HexKey, u256, encodeTLV, NostrPrefix, Lists } from "@snort/nostr"; +import { TaggedRawEvent, HexKey, u256, encodeTLV, NostrPrefix, Lists } from "@snort/system"; import Icon from "Icons/Icon"; import Spinner from "Icons/Spinner"; import { formatShort } from "Number"; import useEventPublisher from "Feed/EventPublisher"; -import { delay, normalizeReaction, unwrap } from "Util"; +import { delay, normalizeReaction, unwrap } from "SnortUtils"; import { NoteCreator } from "Element/NoteCreator"; import { ReBroadcaster } from "Element/ReBroadcaster"; import Reactions from "Element/Reactions"; diff --git a/packages/app/src/Element/NoteQuote.tsx b/packages/app/src/Element/NoteQuote.tsx index 75cdc3b9..262b1ca6 100644 --- a/packages/app/src/Element/NoteQuote.tsx +++ b/packages/app/src/Element/NoteQuote.tsx @@ -1,5 +1,5 @@ import useEventFeed from "Feed/EventFeed"; -import { NostrLink } from "Util"; +import { NostrLink } from "@snort/system"; import Note from "Element/Note"; import PageSpinner from "Element/PageSpinner"; diff --git a/packages/app/src/Element/NoteReaction.tsx b/packages/app/src/Element/NoteReaction.tsx index a9a7132d..00301dac 100644 --- a/packages/app/src/Element/NoteReaction.tsx +++ b/packages/app/src/Element/NoteReaction.tsx @@ -1,14 +1,13 @@ import "./NoteReaction.css"; import { Link } from "react-router-dom"; import { useMemo } from "react"; -import { EventKind, RawEvent, TaggedRawEvent, NostrPrefix } from "@snort/nostr"; +import { EventKind, NostrEvent, TaggedRawEvent, NostrPrefix, EventExt } from "@snort/system"; import Note from "Element/Note"; import ProfileImage from "Element/ProfileImage"; -import { eventLink, hexToBech32 } from "Util"; +import { eventLink, hexToBech32 } from "SnortUtils"; import NoteTime from "Element/NoteTime"; import useModeration from "Hooks/useModeration"; -import { EventExt } from "System/EventExt"; export interface NoteReactionProps { data: TaggedRawEvent; @@ -43,7 +42,7 @@ export default function NoteReaction(props: NoteReactionProps) { function extractRoot() { if (ev?.kind === EventKind.Repost && ev.content.length > 0 && ev.content !== "#[0]") { try { - const r: RawEvent = JSON.parse(ev.content); + const r: NostrEvent = JSON.parse(ev.content); return r as TaggedRawEvent; } catch (e) { console.error("Could not load reposted content", e); diff --git a/packages/app/src/Element/NoteToSelf.tsx b/packages/app/src/Element/NoteToSelf.tsx index 348372dd..b134762a 100644 --- a/packages/app/src/Element/NoteToSelf.tsx +++ b/packages/app/src/Element/NoteToSelf.tsx @@ -1,7 +1,7 @@ import "./NoteToSelf.css"; import { Link, useNavigate } from "react-router-dom"; import { FormattedMessage } from "react-intl"; -import { profileLink } from "Util"; +import { profileLink } from "SnortUtils"; import messages from "./messages"; import Icon from "Icons/Icon"; diff --git a/packages/app/src/Element/Poll.tsx b/packages/app/src/Element/Poll.tsx index 0316c3f6..4ef7dcf9 100644 --- a/packages/app/src/Element/Poll.tsx +++ b/packages/app/src/Element/Poll.tsx @@ -1,4 +1,4 @@ -import { TaggedRawEvent } from "@snort/nostr"; +import { TaggedRawEvent } from "@snort/system"; import { useState } from "react"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; @@ -8,7 +8,7 @@ import useEventPublisher from "Feed/EventPublisher"; import { useWallet } from "Wallet"; import { useUserProfile } from "Hooks/useUserProfile"; import { LNURL } from "LNURL"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; import { formatShort } from "Number"; import Spinner from "Icons/Spinner"; import SendSats from "Element/SendSats"; diff --git a/packages/app/src/Element/ProfileImage.tsx b/packages/app/src/Element/ProfileImage.tsx index ec3e017c..5745b9c7 100644 --- a/packages/app/src/Element/ProfileImage.tsx +++ b/packages/app/src/Element/ProfileImage.tsx @@ -1,13 +1,12 @@ import "./ProfileImage.css"; import React, { useMemo } from "react"; -import { HexKey, NostrPrefix } from "@snort/nostr"; +import { HexKey, NostrPrefix, MetadataCache } from "@snort/system"; import { useUserProfile } from "Hooks/useUserProfile"; -import { hexToBech32, profileLink } from "Util"; +import { hexToBech32, profileLink } from "SnortUtils"; import Avatar from "Element/Avatar"; import Nip05 from "Element/Nip05"; -import { MetadataCache } from "Cache"; import { Link } from "react-router-dom"; export interface ProfileImageProps { diff --git a/packages/app/src/Element/ProfilePreview.tsx b/packages/app/src/Element/ProfilePreview.tsx index 0d927bb2..6397e778 100644 --- a/packages/app/src/Element/ProfilePreview.tsx +++ b/packages/app/src/Element/ProfilePreview.tsx @@ -4,7 +4,7 @@ import { ReactNode } from "react"; import ProfileImage from "Element/ProfileImage"; import FollowButton from "Element/FollowButton"; import { useUserProfile } from "Hooks/useUserProfile"; -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; import { useInView } from "react-intersection-observer"; export interface ProfilePreviewProps { diff --git a/packages/app/src/Element/ProxyImg.tsx b/packages/app/src/Element/ProxyImg.tsx index 44aeef6a..0fcefa25 100644 --- a/packages/app/src/Element/ProxyImg.tsx +++ b/packages/app/src/Element/ProxyImg.tsx @@ -1,7 +1,7 @@ import useImgProxy from "Hooks/useImgProxy"; import { useEffect, useState } from "react"; import { FormattedMessage } from "react-intl"; -import { getUrlHostname } from "Util"; +import { getUrlHostname } from "SnortUtils"; interface ProxyImgProps extends React.DetailedHTMLProps, HTMLImageElement> { size?: number; diff --git a/packages/app/src/Element/PubkeyList.tsx b/packages/app/src/Element/PubkeyList.tsx index 6c43fe0f..f9bf86a4 100644 --- a/packages/app/src/Element/PubkeyList.tsx +++ b/packages/app/src/Element/PubkeyList.tsx @@ -1,7 +1,7 @@ -import { RawEvent } from "@snort/nostr"; +import { NostrEvent } from "@snort/system"; import { FormattedMessage, FormattedNumber } from "react-intl"; -import { dedupe, hexToBech32, unixNow } from "Util"; +import { dedupe, hexToBech32, unixNow } from "SnortUtils"; import FollowListBase from "Element/FollowListBase"; import AsyncButton from "Element/AsyncButton"; import { useWallet } from "Wallet"; @@ -13,7 +13,7 @@ import { LNURL } from "LNURL"; import useEventPublisher from "Feed/EventPublisher"; import { WalletInvoiceState } from "Wallet"; -export default function PubkeyList({ ev, className }: { ev: RawEvent; className?: string }) { +export default function PubkeyList({ ev, className }: { ev: NostrEvent; className?: string }) { const wallet = useWallet(); const login = useLogin(); const publisher = useEventPublisher(); diff --git a/packages/app/src/Element/Reactions.tsx b/packages/app/src/Element/Reactions.tsx index ab8340f7..8e723277 100644 --- a/packages/app/src/Element/Reactions.tsx +++ b/packages/app/src/Element/Reactions.tsx @@ -2,7 +2,7 @@ import "./Reactions.css"; import { useState, useMemo, useEffect } from "react"; import { useIntl, FormattedMessage } from "react-intl"; -import { TaggedRawEvent } from "@snort/nostr"; +import { TaggedRawEvent } from "@snort/system"; import { formatShort } from "Number"; import Icon from "Icons/Icon"; diff --git a/packages/app/src/Element/Relay.tsx b/packages/app/src/Element/Relay.tsx index 5d9e6afc..ab3d3de9 100644 --- a/packages/app/src/Element/Relay.tsx +++ b/packages/app/src/Element/Relay.tsx @@ -2,11 +2,11 @@ import "./Relay.css"; import { useMemo } from "react"; import { FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; -import { RelaySettings } from "@snort/nostr"; +import { RelaySettings } from "@snort/system"; import useRelayState from "Feed/RelayState"; -import { System } from "System"; -import { getRelayName, unixNowMs, unwrap } from "Util"; +import { System } from "index"; +import { getRelayName, unixNowMs, unwrap } from "SnortUtils"; import useLogin from "Hooks/useLogin"; import { setRelays } from "Login"; import Icon from "Icons/Icon"; @@ -20,7 +20,9 @@ export interface RelayProps { export default function Relay(props: RelayProps) { const navigate = useNavigate(); const login = useLogin(); - const relaySettings = unwrap(login.relays.item[props.addr] ?? System.Sockets.get(props.addr)?.Settings ?? {}); + const relaySettings = unwrap( + login.relays.item[props.addr] ?? System.Sockets.find(a => a.address === props.addr)?.settings ?? {} + ); const state = useRelayState(props.addr); const name = useMemo(() => getRelayName(props.addr), [props.addr]); @@ -35,7 +37,6 @@ export default function Relay(props: RelayProps) { ); } - const latency = Math.floor(state?.avgLatency ?? 0); return ( <>
diff --git a/packages/app/src/Element/RelaysMetadata.tsx b/packages/app/src/Element/RelaysMetadata.tsx index 4152c677..5df1631e 100644 --- a/packages/app/src/Element/RelaysMetadata.tsx +++ b/packages/app/src/Element/RelaysMetadata.tsx @@ -2,7 +2,7 @@ import "./RelaysMetadata.css"; import Nostrich from "nostrich.webp"; import { useState } from "react"; -import { FullRelaySettings } from "@snort/nostr"; +import { FullRelaySettings } from "@snort/system"; import Icon from "Icons/Icon"; const RelayFavicon = ({ url }: { url: string }) => { diff --git a/packages/app/src/Element/SendSats.tsx b/packages/app/src/Element/SendSats.tsx index 60ac0949..1c55269d 100644 --- a/packages/app/src/Element/SendSats.tsx +++ b/packages/app/src/Element/SendSats.tsx @@ -1,8 +1,9 @@ import "./SendSats.css"; import React, { useEffect, useMemo, useState } from "react"; import { useIntl, FormattedMessage } from "react-intl"; -import { HexKey, RawEvent } from "@snort/nostr"; +import { HexKey, NostrEvent, EventPublisher } from "@snort/system"; +import { System } from "index"; import { formatShort } from "Number"; import Icon from "Icons/Icon"; import useEventPublisher from "Feed/EventPublisher"; @@ -11,11 +12,10 @@ import Modal from "Element/Modal"; import QrCode from "Element/QrCode"; import Copy from "Element/Copy"; import { LNURL, LNURLError, LNURLErrorCode, LNURLInvoice, LNURLSuccessAction } from "LNURL"; -import { chunks, debounce } from "Util"; +import { chunks, debounce } from "SnortUtils"; import { useWallet } from "Wallet"; import useLogin from "Hooks/useLogin"; import { generateRandomKey } from "Login"; -import { EventPublisher } from "System/EventPublisher"; import { ZapPoolController } from "ZapPoolController"; import messages from "./messages"; @@ -124,7 +124,7 @@ export default function SendSats(props: SendSatsProps) { async function loadInvoice() { if (!amount || !handler || !publisher) return null; - let zap: RawEvent | undefined; + let zap: NostrEvent | undefined; if (author && zapType !== ZapType.NonZap) { const relays = Object.keys(login.relays.item); @@ -133,7 +133,7 @@ export default function SendSats(props: SendSatsProps) { const randomKey = generateRandomKey(); console.debug("Generated new key for zap: ", randomKey); - const publisher = new EventPublisher(randomKey.publicKey, randomKey.privateKey); + const publisher = new EventPublisher(System, randomKey.publicKey, randomKey.privateKey); zap = await publisher.zap(amount * 1000, author, relays, note, comment, eb => eb.tag(["anon", ""])); } else { zap = await publisher.zap(amount * 1000, author, relays, note, comment); diff --git a/packages/app/src/Element/SubDebug.tsx b/packages/app/src/Element/SubDebug.tsx index 0f1de9ce..bf1707fe 100644 --- a/packages/app/src/Element/SubDebug.tsx +++ b/packages/app/src/Element/SubDebug.tsx @@ -3,11 +3,11 @@ import { useState } from "react"; import useRelayState from "Feed/RelayState"; import Tabs, { Tab } from "Element/Tabs"; -import { System } from "System"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; import useSystemState from "Hooks/useSystemState"; -import { RawReqFilter } from "@snort/nostr"; +import { ReqFilter } from "@snort/system"; import { useCopy } from "useCopy"; +import { System } from "index"; function RelayInfo({ id }: { id: string }) { const state = useRelayState(id); @@ -18,7 +18,7 @@ function Queries() { const qs = useSystemState(); const { copy } = useCopy(); - function countElements(filters: Array) { + function countElements(filters: Array) { let total = 0; for (const f of filters) { for (const v of Object.values(f)) { @@ -30,15 +30,10 @@ function Queries() { return total; } - function queryInfo(q: { - id: string; - filters: Array; - closing: boolean; - subFilters: Array; - }) { + function queryInfo(q: { id: string; filters: Array; subFilters: Array }) { return (
- {q.closing ? {q.id} : <>{q.id}} + {q.id}
copy(JSON.stringify(q.filters))} className="pointer">   Filters: {q.filters.length} ({countElements(q.filters)} elements) @@ -66,8 +61,8 @@ const SubDebug = () => { return ( <> Connections: - {[...System.Sockets.keys()].map(k => ( - + {System.Sockets.map(k => ( + ))} ); diff --git a/packages/app/src/Element/SuggestedProfiles.tsx b/packages/app/src/Element/SuggestedProfiles.tsx index 319befe7..7d86420c 100644 --- a/packages/app/src/Element/SuggestedProfiles.tsx +++ b/packages/app/src/Element/SuggestedProfiles.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { HexKey, NostrPrefix } from "@snort/nostr"; +import { HexKey, NostrPrefix } from "@snort/system"; import { FormattedMessage } from "react-intl"; import FollowListBase from "Element/FollowListBase"; @@ -7,7 +7,7 @@ import PageSpinner from "Element/PageSpinner"; import NostrBandApi from "External/NostrBand"; import SemisolDevApi from "External/SemisolDev"; import useLogin from "Hooks/useLogin"; -import { hexToBech32 } from "Util"; +import { hexToBech32 } from "SnortUtils"; enum Provider { NostrBand = 1, diff --git a/packages/app/src/Element/Text.tsx b/packages/app/src/Element/Text.tsx index 68017aad..779c35b8 100644 --- a/packages/app/src/Element/Text.tsx +++ b/packages/app/src/Element/Text.tsx @@ -1,10 +1,10 @@ import "./Text.css"; import { useMemo } from "react"; import { Link, useLocation } from "react-router-dom"; -import { HexKey, NostrPrefix } from "@snort/nostr"; +import { HexKey, NostrPrefix, validateNostrLink } from "@snort/system"; import { MentionRegex, InvoiceRegex, HashtagRegex, CashuRegex } from "Const"; -import { eventLink, hexToBech32, splitByUrl, validateNostrLink } from "Util"; +import { eventLink, hexToBech32, splitByUrl } from "SnortUtils"; import Invoice from "Element/Invoice"; import Hashtag from "Element/Hashtag"; import Mention from "Element/Mention"; diff --git a/packages/app/src/Element/Textarea.tsx b/packages/app/src/Element/Textarea.tsx index fff173bb..352ade10 100644 --- a/packages/app/src/Element/Textarea.tsx +++ b/packages/app/src/Element/Textarea.tsx @@ -4,12 +4,11 @@ import "./Textarea.css"; import { useIntl } from "react-intl"; import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete"; import TextareaAutosize from "react-textarea-autosize"; -import { NostrPrefix } from "@snort/nostr"; +import { NostrPrefix, MetadataCache } from "@snort/system"; import Avatar from "Element/Avatar"; import Nip05 from "Element/Nip05"; -import { hexToBech32 } from "Util"; -import { MetadataCache } from "Cache"; +import { hexToBech32 } from "SnortUtils"; import { UserCache } from "Cache/UserCache"; import messages from "./messages"; diff --git a/packages/app/src/Element/Thread.tsx b/packages/app/src/Element/Thread.tsx index 8e650e31..f30f8966 100644 --- a/packages/app/src/Element/Thread.tsx +++ b/packages/app/src/Element/Thread.tsx @@ -2,10 +2,17 @@ import "./Thread.css"; import { useMemo, useState, ReactNode } from "react"; import { useIntl } from "react-intl"; import { useNavigate, useLocation, Link, useParams } from "react-router-dom"; -import { TaggedRawEvent, u256, EventKind, NostrPrefix } from "@snort/nostr"; -import { EventExt, Thread as ThreadInfo } from "System/EventExt"; +import { + TaggedRawEvent, + u256, + EventKind, + NostrPrefix, + EventExt, + Thread as ThreadInfo, + parseNostrLink, +} from "@snort/system"; -import { eventLink, unwrap, getReactions, parseNostrLink, getAllReactions, findTag } from "Util"; +import { eventLink, unwrap, getReactions, getAllReactions, findTag } from "SnortUtils"; import BackButton from "Element/BackButton"; import Note from "Element/Note"; import NoteGhost from "Element/NoteGhost"; diff --git a/packages/app/src/Element/Timeline.tsx b/packages/app/src/Element/Timeline.tsx index 8fb9644f..6aa8401e 100644 --- a/packages/app/src/Element/Timeline.tsx +++ b/packages/app/src/Element/Timeline.tsx @@ -2,10 +2,10 @@ import "./Timeline.css"; import { FormattedMessage } from "react-intl"; import { useCallback, useMemo } from "react"; import { useInView } from "react-intersection-observer"; -import { TaggedRawEvent, EventKind, u256 } from "@snort/nostr"; +import { TaggedRawEvent, EventKind, u256 } from "@snort/system"; import Icon from "Icons/Icon"; -import { dedupeByPubkey, findTag, tagFilterOfTextRepost } from "Util"; +import { dedupeByPubkey, findTag, tagFilterOfTextRepost } from "SnortUtils"; import ProfileImage from "Element/ProfileImage"; import useTimelineFeed, { TimelineFeed, TimelineSubject } from "Feed/TimelineFeed"; import LoadMore from "Element/LoadMore"; @@ -143,7 +143,7 @@ const Timeline = (props: TimelineProps) => { )} {mainFeed.map(eventElement)} {(props.loadMore === undefined || props.loadMore === true) && ( - + feed.loadMore()} shouldLoadMore={!feed.loading}> diff --git a/packages/app/src/Element/TrendingPosts.tsx b/packages/app/src/Element/TrendingPosts.tsx index b2434498..863174e6 100644 --- a/packages/app/src/Element/TrendingPosts.tsx +++ b/packages/app/src/Element/TrendingPosts.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { RawEvent, TaggedRawEvent } from "@snort/nostr"; +import { NostrEvent, TaggedRawEvent } from "@snort/system"; import { FormattedMessage } from "react-intl"; import PageSpinner from "Element/PageSpinner"; @@ -7,7 +7,7 @@ import Note from "Element/Note"; import NostrBandApi from "External/NostrBand"; export default function TrendingNotes() { - const [posts, setPosts] = useState>(); + const [posts, setPosts] = useState>(); async function loadTrendingNotes() { const api = new NostrBandApi(); diff --git a/packages/app/src/Element/TrendingUsers.tsx b/packages/app/src/Element/TrendingUsers.tsx index 477cca1f..665acc89 100644 --- a/packages/app/src/Element/TrendingUsers.tsx +++ b/packages/app/src/Element/TrendingUsers.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; import { FormattedMessage } from "react-intl"; import FollowListBase from "Element/FollowListBase"; diff --git a/packages/app/src/Element/Username.tsx b/packages/app/src/Element/Username.tsx index 989890f9..3235cd7d 100644 --- a/packages/app/src/Element/Username.tsx +++ b/packages/app/src/Element/Username.tsx @@ -1,10 +1,10 @@ import { MouseEvent } from "react"; import { useNavigate, Link } from "react-router-dom"; -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; import { useUserProfile } from "Hooks/useUserProfile"; -import { profileLink } from "Util"; +import { profileLink } from "SnortUtils"; export default function Username({ pubkey, onLinkVisit }: { pubkey: HexKey; onLinkVisit(): void }) { const user = useUserProfile(pubkey); diff --git a/packages/app/src/Element/WriteDm.tsx b/packages/app/src/Element/WriteDm.tsx index eb9a25ea..e060c1f7 100644 --- a/packages/app/src/Element/WriteDm.tsx +++ b/packages/app/src/Element/WriteDm.tsx @@ -1,17 +1,17 @@ -import { encodeTLV, NostrPrefix, RawEvent } from "@snort/nostr"; +import { encodeTLV, NostrPrefix, NostrEvent } from "@snort/system"; import useEventPublisher from "Feed/EventPublisher"; import Icon from "Icons/Icon"; import Spinner from "Icons/Spinner"; import { useState } from "react"; import useFileUpload from "Upload"; -import { openFile } from "Util"; +import { openFile } from "SnortUtils"; import Textarea from "./Textarea"; export default function WriteDm({ chatPubKey }: { chatPubKey: string }) { const [msg, setMsg] = useState(""); const [sending, setSending] = useState(false); const [uploading, setUploading] = useState(false); - const [otherEvents, setOtherEvents] = useState>([]); + const [otherEvents, setOtherEvents] = useState>([]); const [error, setError] = useState(""); const publisher = useEventPublisher(); const uploader = useFileUpload(); diff --git a/packages/app/src/Element/Zap.tsx b/packages/app/src/Element/Zap.tsx index 106e7727..cb59eee1 100644 --- a/packages/app/src/Element/Zap.tsx +++ b/packages/app/src/Element/Zap.tsx @@ -1,13 +1,13 @@ import "./Zap.css"; import { useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { HexKey, TaggedRawEvent } from "@snort/nostr"; +import { HexKey, TaggedRawEvent } from "@snort/system"; -import { decodeInvoice, InvoiceDetails, sha256, unwrap } from "Util"; +import { decodeInvoice, InvoiceDetails, sha256, unwrap } from "SnortUtils"; import { formatShort } from "Number"; import Text from "Element/Text"; import ProfileImage from "Element/ProfileImage"; -import { findTag } from "Util"; +import { findTag } from "SnortUtils"; import { UserCache } from "Cache/UserCache"; import useLogin from "Hooks/useLogin"; diff --git a/packages/app/src/Element/ZapButton.tsx b/packages/app/src/Element/ZapButton.tsx index 4d7cdd1a..1910fda1 100644 --- a/packages/app/src/Element/ZapButton.tsx +++ b/packages/app/src/Element/ZapButton.tsx @@ -1,6 +1,6 @@ import "./ZapButton.css"; import { useState } from "react"; -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; import { useUserProfile } from "Hooks/useUserProfile"; import SendSats from "Element/SendSats"; diff --git a/packages/app/src/Element/ZapstrEmbed.tsx b/packages/app/src/Element/ZapstrEmbed.tsx index b8cb0e0c..844b5b67 100644 --- a/packages/app/src/Element/ZapstrEmbed.tsx +++ b/packages/app/src/Element/ZapstrEmbed.tsx @@ -1,12 +1,12 @@ import "./ZapstrEmbed.css"; import { Link } from "react-router-dom"; -import { encodeTLV, NostrPrefix, RawEvent } from "@snort/nostr"; +import { encodeTLV, NostrPrefix, NostrEvent } from "@snort/system"; import { ProxyImg } from "Element/ProxyImg"; import ProfileImage from "Element/ProfileImage"; import { FormattedMessage } from "react-intl"; -export default function ZapstrEmbed({ ev }: { ev: RawEvent }) { +export default function ZapstrEmbed({ ev }: { ev: NostrEvent }) { const media = ev.tags.find(a => a[0] === "media"); const cover = ev.tags.find(a => a[0] === "cover"); const subject = ev.tags.find(a => a[0] === "subject"); diff --git a/packages/app/src/External/NostrBand.ts b/packages/app/src/External/NostrBand.ts index 930edeb0..8553a460 100644 --- a/packages/app/src/External/NostrBand.ts +++ b/packages/app/src/External/NostrBand.ts @@ -1,4 +1,4 @@ -import { RawEvent } from "@snort/nostr"; +import { NostrEvent } from "@snort/system"; export interface TrendingUser { pubkey: string; @@ -9,8 +9,8 @@ export interface TrendingUserResponse { } export interface TrendingNote { - event: RawEvent; - author: RawEvent; // kind0 event + event: NostrEvent; + author: NostrEvent; // kind0 event } export interface TrendingNoteResponse { diff --git a/packages/app/src/ExternalStore.ts b/packages/app/src/ExternalStore.ts index f9b77a88..4b1dedea 100644 --- a/packages/app/src/ExternalStore.ts +++ b/packages/app/src/ExternalStore.ts @@ -1,18 +1,18 @@ -type HookFn = () => void; +type HookFn = (e?: TSnapshot) => void; -interface HookFilter { - fn: HookFn; +interface HookFilter { + fn: HookFn; } /** * Simple React hookable store with manual change notifications */ export default abstract class ExternalStore { - #hooks: Array = []; + #hooks: Array> = []; #snapshot: Readonly = {} as Readonly; #changed = true; - hook(fn: HookFn) { + hook(fn: HookFn) { this.#hooks.push({ fn, }); @@ -32,9 +32,9 @@ export default abstract class ExternalStore { return this.#snapshot; } - protected notifyChange() { + protected notifyChange(sn?: TSnapshot) { this.#changed = true; - this.#hooks.forEach(h => h.fn()); + this.#hooks.forEach(h => h.fn(sn)); } abstract takeSnapshot(): TSnapshot; diff --git a/packages/app/src/Feed/BadgesFeed.ts b/packages/app/src/Feed/BadgesFeed.ts index a9e55c7a..8397f8ce 100644 --- a/packages/app/src/Feed/BadgesFeed.ts +++ b/packages/app/src/Feed/BadgesFeed.ts @@ -1,9 +1,7 @@ import { useMemo } from "react"; -import { EventKind, HexKey, Lists } from "@snort/nostr"; +import { EventKind, HexKey, Lists, RequestBuilder, FlatNoteStore, ReplaceableNoteStore } from "@snort/system"; -import { unwrap, findTag, chunks } from "Util"; -import { RequestBuilder } from "System"; -import { FlatNoteStore, ReplaceableNoteStore } from "System/NoteCollection"; +import { unwrap, findTag, chunks } from "SnortUtils"; import useRequestBuilder from "Hooks/useRequestBuilder"; type BadgeAwards = { diff --git a/packages/app/src/Feed/BookmarkFeed.tsx b/packages/app/src/Feed/BookmarkFeed.tsx index 3412a75a..2c7da28e 100644 --- a/packages/app/src/Feed/BookmarkFeed.tsx +++ b/packages/app/src/Feed/BookmarkFeed.tsx @@ -1,4 +1,4 @@ -import { HexKey, Lists } from "@snort/nostr"; +import { HexKey, Lists } from "@snort/system"; import useNotelistSubscription from "Hooks/useNotelistSubscription"; import useLogin from "Hooks/useLogin"; diff --git a/packages/app/src/Feed/EventFeed.ts b/packages/app/src/Feed/EventFeed.ts index 359dbee8..2c295271 100644 --- a/packages/app/src/Feed/EventFeed.ts +++ b/packages/app/src/Feed/EventFeed.ts @@ -1,9 +1,8 @@ import { useMemo } from "react"; -import { NostrPrefix } from "@snort/nostr"; +import { NostrPrefix, RequestBuilder, ReplaceableNoteStore, NostrLink } from "@snort/system"; import useRequestBuilder from "Hooks/useRequestBuilder"; -import { RequestBuilder, ReplaceableNoteStore } from "System"; -import { NostrLink, unwrap } from "Util"; +import { unwrap } from "SnortUtils"; export default function useEventFeed(link: NostrLink) { const sub = useMemo(() => { diff --git a/packages/app/src/Feed/EventPublisher.ts b/packages/app/src/Feed/EventPublisher.ts index 475ae29d..ff238977 100644 --- a/packages/app/src/Feed/EventPublisher.ts +++ b/packages/app/src/Feed/EventPublisher.ts @@ -1,12 +1,13 @@ import { useMemo } from "react"; import useLogin from "Hooks/useLogin"; -import { EventPublisher } from "System/EventPublisher"; +import { EventPublisher } from "@snort/system"; +import { System } from "index"; export default function useEventPublisher() { const { publicKey, privateKey } = useLogin(); return useMemo(() => { if (publicKey) { - return new EventPublisher(publicKey, privateKey); + return new EventPublisher(System, publicKey, privateKey); } }, [publicKey, privateKey]); } diff --git a/packages/app/src/Feed/FollowersFeed.ts b/packages/app/src/Feed/FollowersFeed.ts index 6acc1ec4..dc2f79b8 100644 --- a/packages/app/src/Feed/FollowersFeed.ts +++ b/packages/app/src/Feed/FollowersFeed.ts @@ -1,7 +1,6 @@ import { useMemo } from "react"; -import { HexKey, EventKind } from "@snort/nostr"; +import { HexKey, EventKind, PubkeyReplaceableNoteStore, RequestBuilder } from "@snort/system"; -import { PubkeyReplaceableNoteStore, RequestBuilder } from "System"; import useRequestBuilder from "Hooks/useRequestBuilder"; export default function useFollowersFeed(pubkey?: HexKey) { diff --git a/packages/app/src/Feed/FollowsFeed.ts b/packages/app/src/Feed/FollowsFeed.ts index 499af525..5f7b260b 100644 --- a/packages/app/src/Feed/FollowsFeed.ts +++ b/packages/app/src/Feed/FollowsFeed.ts @@ -1,7 +1,6 @@ import { useMemo } from "react"; -import { HexKey, TaggedRawEvent, EventKind } from "@snort/nostr"; +import { HexKey, TaggedRawEvent, EventKind, PubkeyReplaceableNoteStore, RequestBuilder } from "@snort/system"; -import { PubkeyReplaceableNoteStore, RequestBuilder } from "System"; import useRequestBuilder from "Hooks/useRequestBuilder"; import useLogin from "Hooks/useLogin"; diff --git a/packages/app/src/Feed/LoginFeed.ts b/packages/app/src/Feed/LoginFeed.ts index 84066370..bf4a71b6 100644 --- a/packages/app/src/Feed/LoginFeed.ts +++ b/packages/app/src/Feed/LoginFeed.ts @@ -1,25 +1,27 @@ import { useEffect, useMemo } from "react"; -import { TaggedRawEvent, Lists, EventKind } from "@snort/nostr"; +import { TaggedRawEvent, Lists, EventKind, FlatNoteStore, RequestBuilder } from "@snort/system"; +import debug from "debug"; -import { bech32ToHex, getNewest, getNewestEventTagsByKey, unwrap } from "Util"; +import { bech32ToHex, getNewest, getNewestEventTagsByKey, unwrap } from "SnortUtils"; import { makeNotification, sendNotification } from "Notifications"; import useEventPublisher from "Feed/EventPublisher"; import { getMutedKeys } from "Feed/MuteList"; import useModeration from "Hooks/useModeration"; -import { FlatNoteStore, RequestBuilder } from "System"; import useRequestBuilder from "Hooks/useRequestBuilder"; import { DmCache } from "Cache"; import useLogin from "Hooks/useLogin"; import { addSubscription, setBlocked, setBookmarked, setFollows, setMuted, setPinned, setRelays, setTags } from "Login"; import { SnortPubKey } from "Const"; import { SubscriptionEvent } from "Subscription"; +import useRelaysFeedFollows from "./RelaysFeedFollows"; +import { UserRelays } from "Cache/UserRelayCache"; /** * Managed loading data for the current logged in user */ export default function useLoginFeed() { const login = useLogin(); - const { publicKey: pubKey, readNotifications } = login; + const { publicKey: pubKey, readNotifications, follows } = login; const { isMuted } = useModeration(); const publisher = useEventPublisher(); @@ -39,6 +41,7 @@ export default function useLoginFeed() { .limit(1); const dmSince = DmCache.newest(); + debug("LoginFeed")("Loading dms since %s", new Date(dmSince * 1000).toISOString()); b.withFilter().authors([pubKey]).kinds([EventKind.DirectMessage]).since(dmSince); b.withFilter().kinds([EventKind.DirectMessage]).tag("p", [pubKey]).since(dmSince); return b; @@ -171,8 +174,12 @@ export default function useLoginFeed() { } }, [listsFeed]); - /*const fRelays = useRelaysFeedFollows(follows); useEffect(() => { - FollowsRelays.bulkSet(fRelays).catch(console.error); - }, [dispatch, fRelays]);*/ + UserRelays.buffer(follows.item).catch(console.error); + }, [follows.item]); + + const fRelays = useRelaysFeedFollows(follows.item); + useEffect(() => { + UserRelays.bulkSet(fRelays).catch(console.error); + }, [fRelays]); } diff --git a/packages/app/src/Feed/MuteList.ts b/packages/app/src/Feed/MuteList.ts index 4d551448..5e7c75d5 100644 --- a/packages/app/src/Feed/MuteList.ts +++ b/packages/app/src/Feed/MuteList.ts @@ -1,8 +1,14 @@ import { useMemo } from "react"; -import { HexKey, TaggedRawEvent, Lists, EventKind } from "@snort/nostr"; +import { + HexKey, + TaggedRawEvent, + Lists, + EventKind, + ParameterizedReplaceableNoteStore, + RequestBuilder, +} from "@snort/system"; -import { getNewest } from "Util"; -import { ParameterizedReplaceableNoteStore, RequestBuilder } from "System"; +import { getNewest } from "SnortUtils"; import useRequestBuilder from "Hooks/useRequestBuilder"; import useLogin from "Hooks/useLogin"; diff --git a/packages/app/src/Feed/PinnedFeed.tsx b/packages/app/src/Feed/PinnedFeed.tsx index f8c6b5af..bd714903 100644 --- a/packages/app/src/Feed/PinnedFeed.tsx +++ b/packages/app/src/Feed/PinnedFeed.tsx @@ -1,4 +1,4 @@ -import { HexKey, Lists } from "@snort/nostr"; +import { HexKey, Lists } from "@snort/system"; import useNotelistSubscription from "Hooks/useNotelistSubscription"; import useLogin from "Hooks/useLogin"; diff --git a/packages/app/src/Feed/RelayState.ts b/packages/app/src/Feed/RelayState.ts index beef8218..c5c5c5a1 100644 --- a/packages/app/src/Feed/RelayState.ts +++ b/packages/app/src/Feed/RelayState.ts @@ -1,18 +1,6 @@ -import { useSyncExternalStore } from "react"; -import { StateSnapshot } from "@snort/nostr"; -import { System } from "System"; - -const noop = () => { - return () => undefined; -}; -const noopState = (): StateSnapshot | undefined => { - return undefined; -}; +import { System } from "index"; export default function useRelayState(addr: string) { - const c = System.Sockets.get(addr); - return useSyncExternalStore( - c?.StatusHook.bind(c) ?? noop, - c?.GetState.bind(c) ?? noopState - ); + const c = System.Sockets.find(a => a.address === addr); + return c; } diff --git a/packages/app/src/Feed/RelaysFeed.tsx b/packages/app/src/Feed/RelaysFeed.tsx index 4d6d30cb..e6fe274e 100644 --- a/packages/app/src/Feed/RelaysFeed.tsx +++ b/packages/app/src/Feed/RelaysFeed.tsx @@ -1,8 +1,6 @@ import { useMemo } from "react"; -import { HexKey, FullRelaySettings, EventKind } from "@snort/nostr"; +import { HexKey, FullRelaySettings, EventKind, RequestBuilder, ReplaceableNoteStore } from "@snort/system"; -import { RequestBuilder } from "System"; -import { ReplaceableNoteStore } from "System/NoteCollection"; import useRequestBuilder from "Hooks/useRequestBuilder"; export default function useRelaysFeed(pubkey?: HexKey) { diff --git a/packages/app/src/Feed/RelaysFeedFollows.tsx b/packages/app/src/Feed/RelaysFeedFollows.tsx index 839fbd48..77367f4d 100644 --- a/packages/app/src/Feed/RelaysFeedFollows.tsx +++ b/packages/app/src/Feed/RelaysFeedFollows.tsx @@ -1,73 +1,88 @@ import { useMemo } from "react"; -import { HexKey, FullRelaySettings, TaggedRawEvent, RelaySettings, EventKind } from "@snort/nostr"; +import { + HexKey, + FullRelaySettings, + TaggedRawEvent, + RelaySettings, + EventKind, + PubkeyReplaceableNoteStore, + RequestBuilder, +} from "@snort/system"; +import debug from "debug"; -import { sanitizeRelayUrl } from "Util"; -import { PubkeyReplaceableNoteStore, RequestBuilder } from "System"; +import { sanitizeRelayUrl } from "SnortUtils"; import useRequestBuilder from "Hooks/useRequestBuilder"; +import { UserRelays } from "Cache/UserRelayCache"; -type UserRelayMap = Record>; +interface RelayList { + pubkey: string; + created_at: number; + relays: FullRelaySettings[]; +} -export default function useRelaysFeedFollows(pubkeys: HexKey[]): UserRelayMap { +export default function useRelaysFeedFollows(pubkeys: HexKey[]): Array { const sub = useMemo(() => { const b = new RequestBuilder(`relays:follows`); - b.withFilter().authors(pubkeys).kinds([EventKind.Relays, EventKind.ContactList]); + const since = UserRelays.newest(); + debug("LoginFeed")("Loading relay lists since %s", new Date(since * 1000).toISOString()); + b.withFilter().authors(pubkeys).kinds([EventKind.Relays, EventKind.ContactList]).since(since); return b; }, [pubkeys]); - function mapFromRelays(notes: Array): UserRelayMap { - return Object.fromEntries( - notes.map(ev => { - return [ - ev.pubkey, - ev.tags - .map(a => { - return { - url: sanitizeRelayUrl(a[1]), - settings: { - read: a[2] === "read" || a[2] === undefined, - write: a[2] === "write" || a[2] === undefined, - }, - } as FullRelaySettings; - }) - .filter(a => a.url !== undefined), - ]; - }) - ); + function mapFromRelays(notes: Array): Array { + return notes.map(ev => { + return { + pubkey: ev.pubkey, + created_at: ev.created_at, + relays: ev.tags + .map(a => { + return { + url: sanitizeRelayUrl(a[1]), + settings: { + read: a[2] === "read" || a[2] === undefined, + write: a[2] === "write" || a[2] === undefined, + }, + } as FullRelaySettings; + }) + .filter(a => a.url !== undefined), + }; + }); } - function mapFromContactList(notes: Array): UserRelayMap { - return Object.fromEntries( - notes.map(ev => { - if (ev.content !== "" && ev.content !== "{}" && ev.content.startsWith("{") && ev.content.endsWith("}")) { - try { - const relays: Record = JSON.parse(ev.content); - return [ - ev.pubkey, - Object.entries(relays) - .map(([k, v]) => { - return { - url: sanitizeRelayUrl(k), - settings: v, - } as FullRelaySettings; - }) - .filter(a => a.url !== undefined), - ]; - } catch { - // ignored - } + // instead of discarding the follow list we should also use it for follow graph + function mapFromContactList(notes: Array): Array { + return notes.map(ev => { + if (ev.content !== "" && ev.content !== "{}" && ev.content.startsWith("{") && ev.content.endsWith("}")) { + try { + const relays: Record = JSON.parse(ev.content); + return { + pubkey: ev.pubkey, + created_at: ev.created_at, + relays: Object.entries(relays) + .map(([k, v]) => { + return { + url: sanitizeRelayUrl(k), + settings: v, + } as FullRelaySettings; + }) + .filter(a => a.url !== undefined), + }; + } catch { + // ignored } - return [ev.pubkey, []]; - }) - ); + } + return { + pubkey: ev.pubkey, + created_at: 0, + relays: [], + }; + }); } const relays = useRequestBuilder(PubkeyReplaceableNoteStore, sub); const notesRelays = relays.data?.filter(a => a.kind === EventKind.Relays) ?? []; const notesContactLists = relays.data?.filter(a => a.kind === EventKind.ContactList) ?? []; return useMemo(() => { - return { - ...mapFromContactList(notesContactLists), - ...mapFromRelays(notesRelays), - } as UserRelayMap; + return [...mapFromContactList(notesContactLists), ...mapFromRelays(notesRelays)]; }, [relays]); } diff --git a/packages/app/src/Feed/ThreadFeed.ts b/packages/app/src/Feed/ThreadFeed.ts index 84df5db7..1b0af776 100644 --- a/packages/app/src/Feed/ThreadFeed.ts +++ b/packages/app/src/Feed/ThreadFeed.ts @@ -1,15 +1,22 @@ import { useEffect, useMemo, useState } from "react"; -import { u256, EventKind } from "@snort/nostr"; +import { u256, EventKind, NostrLink, FlatNoteStore, RequestBuilder } from "@snort/system"; -import { appendDedupe, NostrLink } from "Util"; -import { FlatNoteStore, RequestBuilder } from "System"; +import { appendDedupe } from "SnortUtils"; import useRequestBuilder from "Hooks/useRequestBuilder"; import useLogin from "Hooks/useLogin"; +interface RelayTaggedEventId { + id: u256; + relay?: string; +} export default function useThreadFeed(link: NostrLink) { - const [trackingEvents, setTrackingEvent] = useState([link.id]); + const linkTagged = { + id: link.id, + relay: link.relays?.[0], + }; + const [trackingEvents, setTrackingEvent] = useState>([linkTagged]); const [trackingATags, setTrackingATags] = useState([]); - const [allEvents, setAllEvents] = useState([link.id]); + const [allEvents, setAllEvents] = useState>([linkTagged]); const pref = useLogin().preferences; const sub = useMemo(() => { @@ -17,7 +24,10 @@ export default function useThreadFeed(link: NostrLink) { sub.withOptions({ leaveOpen: true, }); - sub.withFilter().ids(trackingEvents); + const fTracking = sub.withFilter(); + for (const te of trackingEvents) { + fTracking.id(te.id, te.relay); + } sub .withFilter() .kinds( @@ -25,7 +35,10 @@ export default function useThreadFeed(link: NostrLink) { ? [EventKind.Reaction, EventKind.TextNote, EventKind.Repost, EventKind.ZapReceipt] : [EventKind.TextNote, EventKind.ZapReceipt, EventKind.Repost] ) - .tag("e", allEvents); + .tag( + "e", + allEvents.map(a => a.id) + ); if (trackingATags.length > 0) { const parsed = trackingATags.map(a => a.split(":")); @@ -45,16 +58,27 @@ export default function useThreadFeed(link: NostrLink) { useEffect(() => { setTrackingATags([]); - setTrackingEvent([link.id]); - setAllEvents([link.id]); + setTrackingEvent([linkTagged]); + setAllEvents([linkTagged]); }, [link.id]); useEffect(() => { if (store.data) { const mainNotes = store.data?.filter(a => a.kind === EventKind.TextNote || a.kind === EventKind.Polls) ?? []; - const eTags = mainNotes.map(a => a.tags.filter(b => b[0] === "e").map(b => b[1])).flat(); - const eTagsMissing = eTags.filter(a => !mainNotes.some(b => b.id === a)); + const eTags = mainNotes + .map(a => + a.tags + .filter(b => b[0] === "e") + .map(b => { + return { + id: b[1], + relay: b[2], + }; + }) + ) + .flat(); + const eTagsMissing = eTags.filter(a => !mainNotes.some(b => b.id === a.id)); setTrackingEvent(s => appendDedupe(s, eTagsMissing)); setAllEvents(s => appendDedupe(s, eTags)); diff --git a/packages/app/src/Feed/TimelineFeed.ts b/packages/app/src/Feed/TimelineFeed.ts index da814655..c08badba 100644 --- a/packages/app/src/Feed/TimelineFeed.ts +++ b/packages/app/src/Feed/TimelineFeed.ts @@ -1,8 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { EventKind, u256 } from "@snort/nostr"; +import { EventKind, u256, FlatNoteStore, RequestBuilder } from "@snort/system"; -import { unixNow, unwrap, tagFilterOfTextRepost } from "Util"; -import { FlatNoteStore, RequestBuilder } from "System"; +import { unixNow, unwrap, tagFilterOfTextRepost } from "SnortUtils"; import useRequestBuilder from "Hooks/useRequestBuilder"; import useTimelineWindow from "Hooks/useTimelineWindow"; import useLogin from "Hooks/useLogin"; @@ -134,7 +133,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel }, [options.relay]); const subNext = useMemo(() => { - const rb = new RequestBuilder(`timeline-related:${subject.type}`); + const rb = new RequestBuilder(`timeline-related:${subject.type}:${subject.discriminator}`); if (trackingEvents.length > 0) { rb.withFilter() .kinds( diff --git a/packages/app/src/Feed/ZapsFeed.ts b/packages/app/src/Feed/ZapsFeed.ts index 38819589..6fd52ba4 100644 --- a/packages/app/src/Feed/ZapsFeed.ts +++ b/packages/app/src/Feed/ZapsFeed.ts @@ -1,8 +1,7 @@ import { useMemo } from "react"; -import { HexKey, EventKind } from "@snort/nostr"; +import { HexKey, EventKind, FlatNoteStore, RequestBuilder } from "@snort/system"; import { parseZap } from "Element/Zap"; -import { FlatNoteStore, RequestBuilder } from "System"; import useRequestBuilder from "Hooks/useRequestBuilder"; export default function useZapsFeed(pubkey?: HexKey) { diff --git a/packages/app/src/Hooks/useImgProxy.ts b/packages/app/src/Hooks/useImgProxy.ts index 5844f9b7..76bb8747 100644 --- a/packages/app/src/Hooks/useImgProxy.ts +++ b/packages/app/src/Hooks/useImgProxy.ts @@ -1,6 +1,6 @@ import * as utils from "@noble/curves/abstract/utils"; import * as base64 from "@protobufjs/base64"; -import { hmacSha256, unwrap } from "Util"; +import { hmacSha256, unwrap } from "SnortUtils"; import useLogin from "Hooks/useLogin"; export interface ImgProxySettings { diff --git a/packages/app/src/Hooks/useInteractionCache.tsx b/packages/app/src/Hooks/useInteractionCache.tsx index d2aa74fe..46a7521d 100644 --- a/packages/app/src/Hooks/useInteractionCache.tsx +++ b/packages/app/src/Hooks/useInteractionCache.tsx @@ -1,9 +1,9 @@ import { useSyncExternalStore } from "react"; -import { HexKey, u256 } from "@snort/nostr"; +import { HexKey, u256 } from "@snort/system"; import { InteractionCache } from "Cache/EventInteractionCache"; import { EventInteraction } from "Db"; -import { sha256, unwrap } from "Util"; +import { sha256, unwrap } from "SnortUtils"; export function useInteractionCache(pubkey?: HexKey, event?: u256) { const id = event && pubkey ? sha256(event + pubkey) : undefined; diff --git a/packages/app/src/Hooks/useLoginHandler.tsx b/packages/app/src/Hooks/useLoginHandler.tsx index 94865c5c..aa7a3b8f 100644 --- a/packages/app/src/Hooks/useLoginHandler.tsx +++ b/packages/app/src/Hooks/useLoginHandler.tsx @@ -4,7 +4,7 @@ import { EmailRegex, MnemonicRegex } from "Const"; import { LoginStore } from "Login"; import { generateBip39Entropy, entropyToPrivateKey } from "nip6"; import { getNip05PubKey } from "Pages/LoginPage"; -import { bech32ToHex } from "Util"; +import { bech32ToHex } from "SnortUtils"; export default function useLoginHandler() { const { formatMessage } = useIntl(); diff --git a/packages/app/src/Hooks/useModeration.tsx b/packages/app/src/Hooks/useModeration.tsx index ce20ad48..3a530c5a 100644 --- a/packages/app/src/Hooks/useModeration.tsx +++ b/packages/app/src/Hooks/useModeration.tsx @@ -1,8 +1,8 @@ -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; import useEventPublisher from "Feed/EventPublisher"; import useLogin from "Hooks/useLogin"; import { setBlocked, setMuted } from "Login"; -import { appendDedupe } from "Util"; +import { appendDedupe } from "SnortUtils"; export default function useModeration() { const login = useLogin(); diff --git a/packages/app/src/Hooks/useNotelistSubscription.ts b/packages/app/src/Hooks/useNotelistSubscription.ts index 5e82d6a6..f9e1eee2 100644 --- a/packages/app/src/Hooks/useNotelistSubscription.ts +++ b/packages/app/src/Hooks/useNotelistSubscription.ts @@ -1,7 +1,13 @@ import { useMemo } from "react"; -import { HexKey, Lists, EventKind } from "@snort/nostr"; +import { + HexKey, + Lists, + EventKind, + FlatNoteStore, + ParameterizedReplaceableNoteStore, + RequestBuilder, +} from "@snort/system"; -import { FlatNoteStore, ParameterizedReplaceableNoteStore, RequestBuilder } from "System"; import useRequestBuilder from "Hooks/useRequestBuilder"; import useLogin from "Hooks/useLogin"; diff --git a/packages/app/src/Hooks/useRelaysForFollows.tsx b/packages/app/src/Hooks/useRelaysForFollows.tsx deleted file mode 100644 index bb14fce7..00000000 --- a/packages/app/src/Hooks/useRelaysForFollows.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { HexKey } from "@snort/nostr"; -import { useMemo } from "react"; -import { FollowsRelays } from "State/Relays"; -import { unwrap } from "Util"; - -export type RelayPicker = ReturnType; - -/** - * Number of relays to pick per pubkey - */ -const PickNRelays = 2; - -export default function useRelaysForFollows(keys: Array) { - return useMemo(() => { - if (keys.length === 0) { - return {}; - } - - const allRelays = keys.map(a => { - return { - key: a, - relays: FollowsRelays.snapshot.get(a), - }; - }); - - const missing = allRelays.filter(a => a.relays === undefined); - const hasRelays = allRelays.filter(a => a.relays !== undefined); - const relayUserMap = hasRelays.reduce((acc, v) => { - for (const r of unwrap(v.relays)) { - if (!acc.has(r.url)) { - acc.set(r.url, new Set([v.key])); - } else { - unwrap(acc.get(r.url)).add(v.key); - } - } - return acc; - }, new Map>()); - const topRelays = [...relayUserMap.entries()].sort(([, v], [, v1]) => v1.size - v.size); - - // - count keys per relay - // - pick n top relays - // - map keys per relay (for subscription filter) - - const userPickedRelays = keys.map(k => { - // pick top 3 relays for this key - const relaysForKey = topRelays - .filter(([, v]) => v.has(k)) - .slice(0, PickNRelays) - .map(([k]) => k); - return { k, relaysForKey }; - }); - - const pickedRelays = new Set(userPickedRelays.map(a => a.relaysForKey).flat()); - - const picked = Object.fromEntries( - [...pickedRelays].map(a => { - const keysOnPickedRelay = new Set(userPickedRelays.filter(b => b.relaysForKey.includes(a)).map(b => b.k)); - return [a, [...keysOnPickedRelay]]; - }) - ); - picked[""] = missing.map(a => a.key); - console.debug(picked); - return picked; - }, [keys]); -} diff --git a/packages/app/src/Hooks/useRequestBuilder.tsx b/packages/app/src/Hooks/useRequestBuilder.tsx index a1bba65a..d982ed9c 100644 --- a/packages/app/src/Hooks/useRequestBuilder.tsx +++ b/packages/app/src/Hooks/useRequestBuilder.tsx @@ -1,39 +1,30 @@ import { useSyncExternalStore } from "react"; -import { RequestBuilder, System } from "System"; -import { EmptySnapshot, NoteStore, StoreSnapshot } from "System/NoteCollection"; -import { unwrap } from "Util"; +import { RequestBuilder, EmptySnapshot, NoteStore, StoreSnapshot } from "@snort/system"; +import { unwrap } from "SnortUtils"; +import { System } from "index"; const useRequestBuilder = >( type: { new (): TStore }, - rb: RequestBuilder | null, - debounced?: number + rb: RequestBuilder | null ) => { const subscribe = (onChanged: () => void) => { - const store = System.Query(type, rb); - let t: ReturnType | undefined; - const release = store.hook(() => { - if (!t) { - t = setTimeout(() => { - clearTimeout(t); - t = undefined; - onChanged(); - }, debounced ?? 500); - } - }); - + if (rb) { + const q = System.Query(type, rb); + const release = q.feed.hook(onChanged); + q.uncancel(); + return () => { + q.cancel(); + release(); + }; + } return () => { - if (rb?.id) { - System.CancelQuery(rb.id); - } - release(); + // noop }; }; const getState = (): StoreSnapshot => { - if (rb?.id) { - const q = System.GetQuery(rb.id); - if (q) { - return unwrap(q).feed?.snapshot as StoreSnapshot; - } + const q = System.GetQuery(rb?.id ?? ""); + if (q) { + return unwrap(q).feed?.snapshot as StoreSnapshot; } return EmptySnapshot as StoreSnapshot; }; diff --git a/packages/app/src/Hooks/useSystemState.tsx b/packages/app/src/Hooks/useSystemState.tsx index c128a242..78932907 100644 --- a/packages/app/src/Hooks/useSystemState.tsx +++ b/packages/app/src/Hooks/useSystemState.tsx @@ -1,9 +1,10 @@ import { useSyncExternalStore } from "react"; -import { System, SystemSnapshot } from "System"; +import { SystemSnapshot } from "@snort/system"; +import { System } from "index"; export default function useSystemState() { return useSyncExternalStore( cb => System.hook(cb), - () => System.getSnapshot() + () => System.snapshot() ); } diff --git a/packages/app/src/Hooks/useUserProfile.ts b/packages/app/src/Hooks/useUserProfile.ts index f04245a6..3909774a 100644 --- a/packages/app/src/Hooks/useUserProfile.ts +++ b/packages/app/src/Hooks/useUserProfile.ts @@ -1,9 +1,8 @@ import { useEffect, useSyncExternalStore } from "react"; -import { HexKey } from "@snort/nostr"; -import { MetadataCache } from "Cache"; +import { HexKey, MetadataCache } from "@snort/system"; import { UserCache } from "Cache/UserCache"; -import { ProfileLoader } from "System/ProfileCache"; +import { ProfileLoader } from "index"; export function useUserProfile(pubKey?: HexKey): MetadataCache | undefined { const user = useSyncExternalStore( diff --git a/packages/app/src/LNURL.ts b/packages/app/src/LNURL.ts index 2f80f692..42978610 100644 --- a/packages/app/src/LNURL.ts +++ b/packages/app/src/LNURL.ts @@ -1,6 +1,6 @@ -import { HexKey, RawEvent } from "@snort/nostr"; +import { HexKey, NostrEvent } from "@snort/system"; import { EmailRegex } from "Const"; -import { bech32ToText, unwrap } from "Util"; +import { bech32ToText, unwrap } from "SnortUtils"; const PayServiceTag = "payRequest"; @@ -119,7 +119,7 @@ export class LNURL { * @param zap * @returns */ - async getInvoice(amount: number, comment?: string, zap?: RawEvent) { + async getInvoice(amount: number, comment?: string, zap?: NostrEvent) { const callback = new URL(unwrap(this.#service?.callback)); const query = new Map(); diff --git a/packages/app/src/Login/Functions.ts b/packages/app/src/Login/Functions.ts index 9be5e0a7..1ba6ffcf 100644 --- a/packages/app/src/Login/Functions.ts +++ b/packages/app/src/Login/Functions.ts @@ -1,13 +1,13 @@ -import { HexKey, RelaySettings } from "@snort/nostr"; +import { HexKey, RelaySettings, EventPublisher } from "@snort/system"; import * as secp from "@noble/curves/secp256k1"; import * as utils from "@noble/curves/abstract/utils"; import { DefaultRelays, SnortPubKey } from "Const"; import { LoginStore, UserPreferences, LoginSession } from "Login"; import { generateBip39Entropy, entropyToPrivateKey } from "nip6"; -import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unixNowMs, unwrap } from "Util"; +import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unixNowMs, unwrap } from "SnortUtils"; import { SubscriptionEvent } from "Subscription"; -import { EventPublisher } from "System/EventPublisher"; +import { System } from "index"; export function setRelays(state: LoginSession, relays: Record, createdAt: number) { if (state.relays.timestamp >= createdAt) { @@ -78,7 +78,7 @@ export async function generateNewLogin() { } const publicKey = utils.bytesToHex(secp.schnorr.getPublicKey(privateKey)); - const publisher = new EventPublisher(publicKey, privateKey); + const publisher = new EventPublisher(System, publicKey, privateKey); const ev = await publisher.contactList([bech32ToHex(SnortPubKey), publicKey], newRelays); publisher.broadcast(ev); diff --git a/packages/app/src/Login/LoginSession.ts b/packages/app/src/Login/LoginSession.ts index ab7dd421..3a7fb288 100644 --- a/packages/app/src/Login/LoginSession.ts +++ b/packages/app/src/Login/LoginSession.ts @@ -1,4 +1,4 @@ -import { HexKey, RelaySettings, u256 } from "@snort/nostr"; +import { HexKey, RelaySettings, u256 } from "@snort/system"; import { UserPreferences } from "Login"; import { SubscriptionEvent } from "Subscription"; diff --git a/packages/app/src/Login/MultiAccountStore.ts b/packages/app/src/Login/MultiAccountStore.ts index 0900aecf..a723879c 100644 --- a/packages/app/src/Login/MultiAccountStore.ts +++ b/packages/app/src/Login/MultiAccountStore.ts @@ -1,12 +1,12 @@ import * as secp from "@noble/curves/secp256k1"; import * as utils from "@noble/curves/abstract/utils"; -import { HexKey, RelaySettings } from "@snort/nostr"; +import { HexKey, RelaySettings } from "@snort/system"; import { DefaultRelays } from "Const"; import ExternalStore from "ExternalStore"; import { LoginSession } from "Login"; -import { deepClone, sanitizeRelayUrl, unwrap } from "Util"; +import { deepClone, sanitizeRelayUrl, unwrap } from "SnortUtils"; import { DefaultPreferences, UserPreferences } from "./Preferences"; const AccountStoreKey = "sessions"; diff --git a/packages/app/src/Nip05/SnortServiceProvider.ts b/packages/app/src/Nip05/SnortServiceProvider.ts index c4d58a1d..d6efa314 100644 --- a/packages/app/src/Nip05/SnortServiceProvider.ts +++ b/packages/app/src/Nip05/SnortServiceProvider.ts @@ -1,5 +1,4 @@ -import { EventKind } from "@snort/nostr"; -import { EventPublisher } from "System/EventPublisher"; +import { EventKind, EventPublisher } from "@snort/system"; import { ServiceError, ServiceProvider } from "./ServiceProvider"; export interface ManageHandle { diff --git a/packages/app/src/Nip05/Verifier.ts b/packages/app/src/Nip05/Verifier.ts index 5d02204f..51b23920 100644 --- a/packages/app/src/Nip05/Verifier.ts +++ b/packages/app/src/Nip05/Verifier.ts @@ -1,5 +1,5 @@ import DnsOverHttpResolver from "dns-over-http-resolver"; -import { bech32ToHex } from "Util"; +import { bech32ToHex } from "SnortUtils"; const resolver = new DnsOverHttpResolver(); interface NostrJson { diff --git a/packages/app/src/Notifications.ts b/packages/app/src/Notifications.ts index 5dc28164..46a4fdef 100644 --- a/packages/app/src/Notifications.ts +++ b/packages/app/src/Notifications.ts @@ -1,11 +1,9 @@ import Nostrich from "nostrich.webp"; -import { TaggedRawEvent } from "@snort/nostr"; -import { EventKind } from "@snort/nostr"; -import { MetadataCache } from "Cache"; +import { TaggedRawEvent, EventKind, MetadataCache } from "@snort/system"; import { getDisplayName } from "Element/ProfileImage"; import { MentionRegex } from "Const"; -import { tagFilterOfTextRepost, unwrap } from "Util"; +import { tagFilterOfTextRepost, unwrap } from "SnortUtils"; import { UserCache } from "Cache/UserCache"; import { LoginSession } from "Login"; diff --git a/packages/app/src/Pages/ChatPage.tsx b/packages/app/src/Pages/ChatPage.tsx index 9c15b003..d1f3e691 100644 --- a/packages/app/src/Pages/ChatPage.tsx +++ b/packages/app/src/Pages/ChatPage.tsx @@ -1,6 +1,6 @@ import DmWindow from "Element/DmWindow"; import { useParams } from "react-router-dom"; -import { bech32ToHex } from "Util"; +import { bech32ToHex } from "SnortUtils"; import "./ChatPage.css"; diff --git a/packages/app/src/Pages/Debug.tsx b/packages/app/src/Pages/Debug.tsx new file mode 100644 index 00000000..37c650d6 --- /dev/null +++ b/packages/app/src/Pages/Debug.tsx @@ -0,0 +1,9 @@ +import SubDebug from "Element/SubDebug"; + +export default function DebugPage() { + return ( + <> + + + ); +} diff --git a/packages/app/src/Pages/DonatePage.tsx b/packages/app/src/Pages/DonatePage.tsx index 57be4a64..64517db8 100644 --- a/packages/app/src/Pages/DonatePage.tsx +++ b/packages/app/src/Pages/DonatePage.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from "react"; import { FormattedMessage } from "react-intl"; -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; import { ApiHost, KieranPubKey, SnortPubKey } from "Const"; import ProfilePreview from "Element/ProfilePreview"; import ZapButton from "Element/ZapButton"; -import { bech32ToHex } from "Util"; +import { bech32ToHex } from "SnortUtils"; import SnortApi, { RevenueSplit, RevenueToday } from "SnortApi"; const Developers = [ diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx index 415299a8..6c450958 100644 --- a/packages/app/src/Pages/Layout.tsx +++ b/packages/app/src/Pages/Layout.tsx @@ -9,21 +9,18 @@ import messages from "./messages"; import Icon from "Icons/Icon"; import { RootState } from "State/Store"; import { setShow, reset } from "State/NoteCreator"; -import { System } from "System"; +import { System } from "index"; import useLoginFeed from "Feed/LoginFeed"; import { totalUnread } from "Pages/MessagesPage"; import useModeration from "Hooks/useModeration"; import { NoteCreator } from "Element/NoteCreator"; -import { db } from "Db"; import useEventPublisher from "Feed/EventPublisher"; -import SubDebug from "Element/SubDebug"; -import { preload } from "Cache"; import { useDmCache } from "Hooks/useDmsCache"; import { mapPlanName } from "./subscribe"; import useLogin from "Hooks/useLogin"; import Avatar from "Element/Avatar"; import { useUserProfile } from "Hooks/useUserProfile"; -import { profileLink } from "Util"; +import { profileLink } from "SnortUtils"; import { getCurrentSubscription } from "Subscription"; import Toaster from "Toaster"; @@ -78,9 +75,9 @@ export default function Layout() { for (const [k, v] of Object.entries(relays.item)) { await System.ConnectToRelay(k, v); } - for (const [k, c] of System.Sockets) { - if (!relays.item[k] && !c.Ephemeral) { - System.DisconnectRelay(k); + for (const v of System.Sockets) { + if (!relays.item[v.address] && !v.ephemeral) { + System.DisconnectRelay(v.address); } } })(); @@ -112,29 +109,6 @@ export default function Layout() { }; }, [preferences.theme]); - useEffect(() => { - // check DB support then init - db.isAvailable().then(async a => { - db.ready = a; - if (a) { - await preload(); - } - console.debug(`Using db: ${a ? "IndexedDB" : "In-Memory"}`); - - try { - if ("registerProtocolHandler" in window.navigator) { - window.navigator.registerProtocolHandler( - "web+nostr", - `${window.location.protocol}//${window.location.host}/%s` - ); - console.info("Registered protocol handler for 'web+nostr'"); - } - } catch (e) { - console.error("Failed to register protocol handler", e); - } - }); - }, []); - return (
{!shouldHideHeader && ( @@ -170,7 +144,6 @@ export default function Layout() { )} - {window.localStorage.getItem("debug") && }
); diff --git a/packages/app/src/Pages/LoginPage.tsx b/packages/app/src/Pages/LoginPage.tsx index 89165021..be38fea4 100644 --- a/packages/app/src/Pages/LoginPage.tsx +++ b/packages/app/src/Pages/LoginPage.tsx @@ -3,9 +3,9 @@ import "./LoginPage.css"; import { CSSProperties, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useIntl, FormattedMessage } from "react-intl"; -import { HexKey } from "@snort/nostr"; +import { HexKey } from "@snort/system"; -import { bech32ToHex, unwrap } from "Util"; +import { bech32ToHex, unwrap } from "SnortUtils"; import ZapButton from "Element/ZapButton"; import useImgProxy from "Hooks/useImgProxy"; import Icon from "Icons/Icon"; diff --git a/packages/app/src/Pages/MessagesPage.tsx b/packages/app/src/Pages/MessagesPage.tsx index 30fed4e7..3aa0e6e4 100644 --- a/packages/app/src/Pages/MessagesPage.tsx +++ b/packages/app/src/Pages/MessagesPage.tsx @@ -1,11 +1,11 @@ import React, { useMemo, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useNavigate } from "react-router-dom"; -import { HexKey, RawEvent, NostrPrefix } from "@snort/nostr"; +import { HexKey, NostrEvent, NostrPrefix } from "@snort/system"; import UnreadCount from "Element/UnreadCount"; import ProfileImage, { getDisplayName } from "Element/ProfileImage"; -import { dedupe, hexToBech32, unwrap } from "Util"; +import { dedupe, hexToBech32, unwrap } from "SnortUtils"; import NoteToSelf from "Element/NoteToSelf"; import useModeration from "Hooks/useModeration"; import { useDmCache } from "Hooks/useDmsCache"; @@ -162,30 +162,30 @@ export function setLastReadDm(pk: HexKey) { window.localStorage.setItem(k, now.toString()); } -export function dmTo(e: RawEvent) { +export function dmTo(e: NostrEvent) { const firstP = e.tags.find(b => b[0] === "p"); return unwrap(firstP?.[1]); } -export function isToSelf(e: Readonly, pk: HexKey) { +export function isToSelf(e: Readonly, pk: HexKey) { return e.pubkey === pk && dmTo(e) === pk; } -export function dmsInChat(dms: readonly RawEvent[], pk: HexKey) { +export function dmsInChat(dms: readonly NostrEvent[], pk: HexKey) { return dms.filter(a => a.pubkey === pk || dmTo(a) === pk); } -export function totalUnread(dms: RawEvent[], myPubKey: HexKey) { +export function totalUnread(dms: NostrEvent[], myPubKey: HexKey) { return extractChats(dms, myPubKey).reduce((acc, v) => (acc += v.unreadMessages), 0); } -function unreadDms(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) { +function unreadDms(dms: NostrEvent[], myPubKey: HexKey, pk: HexKey) { if (pk === myPubKey) return 0; const lastRead = lastReadDm(pk); return dmsInChat(dms, pk).filter(a => a.created_at >= lastRead && a.pubkey !== myPubKey).length; } -function newestMessage(dms: readonly RawEvent[], myPubKey: HexKey, pk: HexKey) { +function newestMessage(dms: readonly NostrEvent[], myPubKey: HexKey, pk: HexKey) { if (pk === myPubKey) { return dmsInChat( dms.filter(d => isToSelf(d, myPubKey)), @@ -196,11 +196,11 @@ function newestMessage(dms: readonly RawEvent[], myPubKey: HexKey, pk: HexKey) { return dmsInChat(dms, pk).reduce((acc, v) => (acc = v.created_at > acc ? v.created_at : acc), 0); } -export function dmsForLogin(dms: readonly RawEvent[], myPubKey: HexKey) { +export function dmsForLogin(dms: readonly NostrEvent[], myPubKey: HexKey) { return dms.filter(a => a.pubkey === myPubKey || (a.pubkey !== myPubKey && dmTo(a) === myPubKey)); } -export function extractChats(dms: RawEvent[], myPubKey: HexKey) { +export function extractChats(dms: NostrEvent[], myPubKey: HexKey) { const myDms = dmsForLogin(dms, myPubKey); const keys = myDms.map(a => [a.pubkey, dmTo(a)]).flat(); const filteredKeys = dedupe(keys); diff --git a/packages/app/src/Pages/NostrLinkHandler.tsx b/packages/app/src/Pages/NostrLinkHandler.tsx index 195f0867..ea8f57c5 100644 --- a/packages/app/src/Pages/NostrLinkHandler.tsx +++ b/packages/app/src/Pages/NostrLinkHandler.tsx @@ -1,12 +1,11 @@ -import { NostrPrefix } from "@snort/nostr"; +import { NostrPrefix, parseNostrLink } from "@snort/system"; import { useEffect, useState } from "react"; import { FormattedMessage } from "react-intl"; import { useNavigate, useParams } from "react-router-dom"; import Spinner from "Icons/Spinner"; -import { parseNostrLink, profileLink } from "Util"; +import { profileLink } from "SnortUtils"; import { getNip05PubKey } from "Pages/LoginPage"; -import { System } from "System"; export default function NostrLinkHandler() { const params = useParams(); @@ -18,9 +17,6 @@ export default function NostrLinkHandler() { async function handleLink(link: string) { const nav = parseNostrLink(link); if (nav) { - if ((nav.relays?.length ?? 0) > 0) { - nav.relays?.map(a => System.ConnectEphemeralRelay(a)); - } if (nav.type === NostrPrefix.Event || nav.type === NostrPrefix.Note || nav.type === NostrPrefix.Address) { navigate(`/e/${nav.encode()}`); } else if (nav.type === NostrPrefix.PublicKey || nav.type === NostrPrefix.Profile) { diff --git a/packages/app/src/Pages/Notifications.tsx b/packages/app/src/Pages/Notifications.tsx index 9cf9907c..c5a83077 100644 --- a/packages/app/src/Pages/Notifications.tsx +++ b/packages/app/src/Pages/Notifications.tsx @@ -4,7 +4,7 @@ import Timeline from "Element/Timeline"; import { TaskList } from "Tasks/TaskList"; import useLogin from "Hooks/useLogin"; import { markNotificationsRead } from "Login"; -import { unixNow } from "Util"; +import { unixNow } from "SnortUtils"; export default function NotificationsPage() { const login = useLogin(); diff --git a/packages/app/src/Pages/ProfilePage.tsx b/packages/app/src/Pages/ProfilePage.tsx index 1db6305c..551b0c8f 100644 --- a/packages/app/src/Pages/ProfilePage.tsx +++ b/packages/app/src/Pages/ProfilePage.tsx @@ -2,9 +2,9 @@ import "./ProfilePage.css"; import { useEffect, useState } from "react"; import { useIntl, FormattedMessage } from "react-intl"; import { useNavigate, useParams } from "react-router-dom"; -import { encodeTLV, EventKind, HexKey, NostrPrefix } from "@snort/nostr"; +import { encodeTLV, EventKind, HexKey, NostrPrefix, parseNostrLink } from "@snort/system"; -import { parseNostrLink, getReactions, unwrap } from "Util"; +import { getReactions, unwrap } from "SnortUtils"; import { formatShort } from "Number"; import Note from "Element/Note"; import Bookmarks from "Element/Bookmarks"; @@ -23,7 +23,7 @@ import useModeration from "Hooks/useModeration"; import useZapsFeed from "Feed/ZapsFeed"; import { default as ZapElement } from "Element/Zap"; import FollowButton from "Element/FollowButton"; -import { parseId, hexToBech32 } from "Util"; +import { parseId, hexToBech32 } from "SnortUtils"; import Avatar from "Element/Avatar"; import Timeline from "Element/Timeline"; import Text from "Element/Text"; diff --git a/packages/app/src/Pages/Root.tsx b/packages/app/src/Pages/Root.tsx index 8d738c51..7c854650 100644 --- a/packages/app/src/Pages/Root.tsx +++ b/packages/app/src/Pages/Root.tsx @@ -5,9 +5,9 @@ import { useIntl, FormattedMessage } from "react-intl"; import Tabs, { Tab } from "Element/Tabs"; import Timeline from "Element/Timeline"; -import { System } from "System"; +import { System } from "index"; import { TimelineSubject } from "Feed/TimelineFeed"; -import { debounce, getRelayName, sha256, unixNow, unwrap } from "Util"; +import { debounce, getRelayName, sha256, unixNow, unwrap } from "SnortUtils"; import useLogin from "Hooks/useLogin"; import Discover from "Pages/Discover"; @@ -64,7 +64,7 @@ export default function RootPage() { switch (pTab) { case "conversations": { - return RootTab.NotesAndReplies; + return RootTab.Conversations; } case "global": { return RootTab.Global; @@ -170,10 +170,10 @@ const GlobalTab = () => { useEffect(() => { return debounce(500, () => { const ret: RelayOption[] = []; - System.Sockets.forEach((v, k) => { + System.Sockets.forEach(v => { ret.push({ - url: k, - paid: v.Info?.limitation?.payment_required ?? false, + url: v.address, + paid: v.info?.limitation?.payment_required ?? false, }); }); ret.sort(a => (a.paid ? -1 : 1)); diff --git a/packages/app/src/Pages/SearchPage.tsx b/packages/app/src/Pages/SearchPage.tsx index 09ef2e2b..28ddd82b 100644 --- a/packages/app/src/Pages/SearchPage.tsx +++ b/packages/app/src/Pages/SearchPage.tsx @@ -3,10 +3,9 @@ import { useParams } from "react-router-dom"; import Timeline from "Element/Timeline"; import { Tab, TabElement } from "Element/Tabs"; import { useEffect, useState } from "react"; -import { debounce } from "Util"; -import { router } from "index"; +import { debounce } from "SnortUtils"; +import { System, router } from "index"; import { SearchRelays } from "Const"; -import { System } from "System"; import TrendingUsers from "Element/TrendingUsers"; import TrendingNotes from "Element/TrendingPosts"; @@ -43,7 +42,7 @@ const SearchPage = () => { useEffect(() => { const addedRelays: string[] = []; for (const [k, v] of SearchRelays) { - if (!System.Sockets.has(k)) { + if (!System.Sockets.some(v => v.address === k)) { System.ConnectToRelay(k, v); addedRelays.push(k); } diff --git a/packages/app/src/Pages/WalletPage.tsx b/packages/app/src/Pages/WalletPage.tsx index e0861694..e6ca2f92 100644 --- a/packages/app/src/Pages/WalletPage.tsx +++ b/packages/app/src/Pages/WalletPage.tsx @@ -7,7 +7,7 @@ import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; import NoteTime from "Element/NoteTime"; import { WalletInvoice, Sats, WalletInfo, WalletInvoiceState, useWallet, LNWallet, Wallets } from "Wallet"; import AsyncButton from "Element/AsyncButton"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; import { WebLNWallet } from "Wallet/WebLN"; import Icon from "Icons/Icon"; diff --git a/packages/app/src/Pages/ZapPool.tsx b/packages/app/src/Pages/ZapPool.tsx index 95a889d1..44614edd 100644 --- a/packages/app/src/Pages/ZapPool.tsx +++ b/packages/app/src/Pages/ZapPool.tsx @@ -6,13 +6,13 @@ import { FormattedMessage, FormattedNumber } from "react-intl"; import { SnortPubKey } from "Const"; import ProfilePreview from "Element/ProfilePreview"; import useLogin from "Hooks/useLogin"; -import { System } from "System"; import { UploaderServices } from "Upload"; -import { bech32ToHex, getRelayName, unwrap } from "Util"; +import { bech32ToHex, getRelayName, unwrap } from "SnortUtils"; import { ZapPoolController, ZapPoolRecipient, ZapPoolRecipientType } from "ZapPoolController"; import { useUserProfile } from "Hooks/useUserProfile"; import AsyncButton from "Element/AsyncButton"; import { useWallet } from "Wallet"; +import { System } from "index"; const DataProviders = [ { @@ -78,15 +78,14 @@ export default function ZapPoolPage() { const { wallet } = useWallet(); const relayConnections = useMemo(() => { - return [...System.Sockets.values()] - .map(a => { - if (a.Info?.pubkey) { - return { - address: a.Address, - pubkey: a.Info.pubkey, - }; - } - }) + return System.Sockets.map(a => { + if (a.info?.pubkey && !a.ephemeral) { + return { + address: a.address, + pubkey: a.info.pubkey, + }; + } + }) .filter(a => a !== undefined) .map(unwrap); }, [login.relays]); diff --git a/packages/app/src/Pages/new/ImportFollows.tsx b/packages/app/src/Pages/new/ImportFollows.tsx index f00fb0b2..3a1654c5 100644 --- a/packages/app/src/Pages/new/ImportFollows.tsx +++ b/packages/app/src/Pages/new/ImportFollows.tsx @@ -6,7 +6,7 @@ import { ApiHost } from "Const"; import Logo from "Element/Logo"; import AsyncButton from "Element/AsyncButton"; import FollowListBase from "Element/FollowListBase"; -import { bech32ToHex } from "Util"; +import { bech32ToHex } from "SnortUtils"; import SnortApi from "SnortApi"; import useLogin from "Hooks/useLogin"; diff --git a/packages/app/src/Pages/new/NewUserFlow.tsx b/packages/app/src/Pages/new/NewUserFlow.tsx index 528f9da5..07ad76c7 100644 --- a/packages/app/src/Pages/new/NewUserFlow.tsx +++ b/packages/app/src/Pages/new/NewUserFlow.tsx @@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom"; import Logo from "Element/Logo"; import { CollapsedSection } from "Element/Collapsed"; import Copy from "Element/Copy"; -import { hexToBech32 } from "Util"; +import { hexToBech32 } from "SnortUtils"; import { hexToMnemonic } from "nip6"; import useLogin from "Hooks/useLogin"; import { PROFILE } from "."; diff --git a/packages/app/src/Pages/new/ProfileSetup.tsx b/packages/app/src/Pages/new/ProfileSetup.tsx index 20d6864d..26f5f23a 100644 --- a/packages/app/src/Pages/new/ProfileSetup.tsx +++ b/packages/app/src/Pages/new/ProfileSetup.tsx @@ -1,12 +1,13 @@ import { useEffect, useState } from "react"; import { useIntl, FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; +import { mapEventToProfile } from "@snort/system"; import Logo from "Element/Logo"; import useEventPublisher from "Feed/EventPublisher"; import useLogin from "Hooks/useLogin"; import { useUserProfile } from "Hooks/useUserProfile"; -import { mapEventToProfile, UserCache } from "Cache"; +import { UserCache } from "Cache"; import AvatarEditor from "Element/AvatarEditor"; import messages from "./messages"; diff --git a/packages/app/src/Pages/settings/Index.tsx b/packages/app/src/Pages/settings/Index.tsx index 7c929d41..202a96c3 100644 --- a/packages/app/src/Pages/settings/Index.tsx +++ b/packages/app/src/Pages/settings/Index.tsx @@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom"; import Icon from "Icons/Icon"; import { LoginStore, logout } from "Login"; import useLogin from "Hooks/useLogin"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; import { getCurrentSubscription } from "Subscription"; import { CollapsedSection } from "Element/Collapsed"; diff --git a/packages/app/src/Pages/settings/Keys.tsx b/packages/app/src/Pages/settings/Keys.tsx index e5220cf1..00958e95 100644 --- a/packages/app/src/Pages/settings/Keys.tsx +++ b/packages/app/src/Pages/settings/Keys.tsx @@ -1,11 +1,11 @@ import "./Keys.css"; import { FormattedMessage } from "react-intl"; -import { encodeTLV, NostrPrefix } from "@snort/nostr"; +import { encodeTLV, NostrPrefix } from "@snort/system"; import Copy from "Element/Copy"; import useLogin from "Hooks/useLogin"; import { hexToMnemonic } from "nip6"; -import { hexToBech32 } from "Util"; +import { hexToBech32 } from "SnortUtils"; export default function ExportKeys() { const { publicKey, privateKey, generatedEntropy } = useLogin(); diff --git a/packages/app/src/Pages/settings/Preferences.tsx b/packages/app/src/Pages/settings/Preferences.tsx index f75351b7..b4cb9ee3 100644 --- a/packages/app/src/Pages/settings/Preferences.tsx +++ b/packages/app/src/Pages/settings/Preferences.tsx @@ -5,7 +5,7 @@ import { useEffect, useState } from "react"; import useLogin from "Hooks/useLogin"; import { DefaultPreferences, updatePreferences, UserPreferences } from "Login"; import { DefaultImgProxy } from "Const"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; import messages from "./messages"; diff --git a/packages/app/src/Pages/settings/Profile.tsx b/packages/app/src/Pages/settings/Profile.tsx index 87eaa9b8..1918036c 100644 --- a/packages/app/src/Pages/settings/Profile.tsx +++ b/packages/app/src/Pages/settings/Profile.tsx @@ -3,13 +3,14 @@ import Nostrich from "nostrich.webp"; import { useEffect, useState } from "react"; import { FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; +import { mapEventToProfile } from "@snort/system"; import useEventPublisher from "Feed/EventPublisher"; import { useUserProfile } from "Hooks/useUserProfile"; -import { openFile } from "Util"; +import { openFile } from "SnortUtils"; import useFileUpload from "Upload"; import AsyncButton from "Element/AsyncButton"; -import { mapEventToProfile, UserCache } from "Cache"; +import { UserCache } from "Cache"; import useLogin from "Hooks/useLogin"; import AvatarEditor from "Element/AvatarEditor"; import Icon from "Icons/Icon"; diff --git a/packages/app/src/Pages/settings/RelayInfo.tsx b/packages/app/src/Pages/settings/RelayInfo.tsx index ac51b073..cf292887 100644 --- a/packages/app/src/Pages/settings/RelayInfo.tsx +++ b/packages/app/src/Pages/settings/RelayInfo.tsx @@ -2,8 +2,8 @@ import { FormattedMessage } from "react-intl"; import ProfilePreview from "Element/ProfilePreview"; import useRelayState from "Feed/RelayState"; import { useNavigate, useParams } from "react-router-dom"; -import { parseId, unwrap } from "Util"; -import { System } from "System"; +import { parseId, unwrap } from "SnortUtils"; +import { System } from "index"; import { removeRelay } from "Login"; import useLogin from "Hooks/useLogin"; @@ -14,8 +14,8 @@ const RelayInfo = () => { const navigate = useNavigate(); const login = useLogin(); - const conn = Array.from(System.Sockets.values()).find(a => a.Id === params.id); - const stats = useRelayState(conn?.Address ?? ""); + const conn = System.Sockets.find(a => a.id === params.id); + const stats = useRelayState(conn?.address ?? ""); return ( <> @@ -105,7 +105,7 @@ const RelayInfo = () => {
{ - removeRelay(login, unwrap(conn).Address); + removeRelay(login, unwrap(conn).address); navigate("/settings/relays"); }}> diff --git a/packages/app/src/Pages/settings/Relays.tsx b/packages/app/src/Pages/settings/Relays.tsx index 7cb7572e..d5338309 100644 --- a/packages/app/src/Pages/settings/Relays.tsx +++ b/packages/app/src/Pages/settings/Relays.tsx @@ -1,10 +1,10 @@ import { useMemo, useState } from "react"; import { FormattedMessage } from "react-intl"; -import { randomSample, unixNowMs } from "Util"; +import { randomSample, unixNowMs } from "SnortUtils"; import Relay from "Element/Relay"; import useEventPublisher from "Feed/EventPublisher"; -import { System } from "System"; +import { System } from "index"; import useLogin from "Hooks/useLogin"; import { setRelays } from "Login"; @@ -16,18 +16,17 @@ const RelaySettingsPage = () => { const [newRelay, setNewRelay] = useState(); const otherConnections = useMemo(() => { - return [...System.Sockets.keys()].filter(a => relays.item[a] === undefined); + return System.Sockets.filter(a => relays.item[a.address] === undefined); }, [relays]); async function saveRelays() { if (publisher) { const ev = await publisher.contactList(login.follows.item, login.relays.item); publisher.broadcast(ev); - publisher.broadcastForBootstrap(ev); try { const onlineRelays = await fetch("https://api.nostr.watch/v1/online").then(r => r.json()); const relayList = await publisher.relayList(login.relays.item); - const rs = Object.keys(relays).concat(randomSample(onlineRelays, 20)); + const rs = Object.keys(relays.item).concat(randomSample(onlineRelays, 20)); publisher.broadcastAll(relayList, rs); } catch (error) { console.error(error); @@ -98,7 +97,7 @@ const RelaySettingsPage = () => {
{otherConnections.map(a => ( - + ))}
diff --git a/packages/app/src/Pages/settings/wallet/Cashu.tsx b/packages/app/src/Pages/settings/wallet/Cashu.tsx index 02e4b728..3c41d72c 100644 --- a/packages/app/src/Pages/settings/wallet/Cashu.tsx +++ b/packages/app/src/Pages/settings/wallet/Cashu.tsx @@ -3,7 +3,7 @@ import { FormattedMessage, useIntl } from "react-intl"; import { v4 as uuid } from "uuid"; import AsyncButton from "Element/AsyncButton"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; import { WalletConfig, WalletKind, Wallets } from "Wallet"; import { useNavigate } from "react-router-dom"; diff --git a/packages/app/src/Pages/settings/wallet/LNC.tsx b/packages/app/src/Pages/settings/wallet/LNC.tsx index 85763c84..206d02a5 100644 --- a/packages/app/src/Pages/settings/wallet/LNC.tsx +++ b/packages/app/src/Pages/settings/wallet/LNC.tsx @@ -5,7 +5,7 @@ import { v4 as uuid } from "uuid"; import AsyncButton from "Element/AsyncButton"; import { LNWallet, WalletInfo, WalletKind, Wallets } from "Wallet"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; const ConnectLNC = () => { const { formatMessage } = useIntl(); diff --git a/packages/app/src/Pages/settings/wallet/LNDHub.tsx b/packages/app/src/Pages/settings/wallet/LNDHub.tsx index 869b1271..85ddc0fe 100644 --- a/packages/app/src/Pages/settings/wallet/LNDHub.tsx +++ b/packages/app/src/Pages/settings/wallet/LNDHub.tsx @@ -3,7 +3,7 @@ import { FormattedMessage, useIntl } from "react-intl"; import { v4 as uuid } from "uuid"; import AsyncButton from "Element/AsyncButton"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; import LNDHubWallet from "Wallet/LNDHub"; import { WalletConfig, WalletKind, Wallets } from "Wallet"; import { useNavigate } from "react-router-dom"; diff --git a/packages/app/src/Pages/settings/wallet/NWC.tsx b/packages/app/src/Pages/settings/wallet/NWC.tsx index 84116fda..bd5e1feb 100644 --- a/packages/app/src/Pages/settings/wallet/NWC.tsx +++ b/packages/app/src/Pages/settings/wallet/NWC.tsx @@ -3,7 +3,7 @@ import { FormattedMessage, useIntl } from "react-intl"; import { v4 as uuid } from "uuid"; import AsyncButton from "Element/AsyncButton"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; import { WalletConfig, WalletKind, Wallets } from "Wallet"; import { Link, useNavigate } from "react-router-dom"; import { NostrConnectWallet } from "Wallet/NostrWalletConnect"; diff --git a/packages/app/src/SnortApi.ts b/packages/app/src/SnortApi.ts index 217bfcfc..a69f71e6 100644 --- a/packages/app/src/SnortApi.ts +++ b/packages/app/src/SnortApi.ts @@ -1,7 +1,6 @@ -import { EventKind } from "@snort/nostr"; +import { EventKind, EventPublisher } from "@snort/system"; import { ApiHost } from "Const"; import { SubscriptionType } from "Subscription"; -import { EventPublisher } from "System/EventPublisher"; export interface RevenueToday { donations: number; diff --git a/packages/app/src/Util.test.ts b/packages/app/src/SnortUtils/Utils.test.ts similarity index 79% rename from packages/app/src/Util.test.ts rename to packages/app/src/SnortUtils/Utils.test.ts index dfae60e0..b91d02ae 100644 --- a/packages/app/src/Util.test.ts +++ b/packages/app/src/SnortUtils/Utils.test.ts @@ -1,4 +1,6 @@ -import { splitByUrl, magnetURIDecode, getRelayName, validateNostrLink } from "./Util"; +import { NostrPrefix } from "@snort/system"; +import { splitByUrl, magnetURIDecode, getRelayName } from "."; +import { describe, expect } from "@jest/globals"; describe("splitByUrl", () => { it("should split a string by URLs", () => { @@ -12,8 +14,8 @@ describe("splitByUrl", () => { " but I made a ", "https://example.com", "! simple example (", - "https://example.com", - ") of how ", + "https://example.com)", + " of how ", "https://example.com/yo-yo", " ", "https://example.example.com", @@ -90,24 +92,3 @@ describe("getRelayName", () => { expect(output).toEqual("relay.example2.com?broadcast=true"); }); }); - -describe("validateNostrLink", () => { - it("should return true for valid nostr links", () => { - [ - "nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg", - "web+nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg", - "nostr:note15449edq4qa5wzgqvh8td0q0dp6hwtes4pknsrm7eygeenhlj99xsq94wu9", - "nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p", - "nostr:nevent1qqs226juks2sw68pyqxtn4khs8ksath9uc2smfcpalvjyvuemlezjngrd87dq", - "nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu", - ].forEach(link => { - expect(validateNostrLink(link)).toBe(true); - }); - }); - - it("should return false for invalid nostr links", () => { - ["nostr:npub", "web+nostr:npub", "nostr:nevent1xxx"].forEach(link => { - expect(validateNostrLink(link)).toBe(false); - }); - }); -}); diff --git a/packages/app/src/Util.ts b/packages/app/src/SnortUtils/index.ts similarity index 83% rename from packages/app/src/Util.ts rename to packages/app/src/SnortUtils/index.ts index 2def8454..329e2f29 100644 --- a/packages/app/src/Util.ts +++ b/packages/app/src/SnortUtils/index.ts @@ -13,11 +13,9 @@ import { EventKind, encodeTLV, NostrPrefix, - decodeTLV, - TLVEntryType, - RawEvent, -} from "@snort/nostr"; -import { MetadataCache } from "Cache"; + NostrEvent, + MetadataCache, +} from "@snort/system"; export const sha256 = (str: string | Uint8Array): u256 => { return utils.bytesToHex(hash(str)); @@ -65,7 +63,7 @@ export function bech32ToHex(str: string) { const nKey = bech32.decode(str, 1_000); const buff = bech32.fromWords(nKey.words); return utils.bytesToHex(Uint8Array.from(buff)); - } catch { + } catch (e) { return str; } } @@ -483,7 +481,7 @@ export function chunks(arr: T[], length: number) { return result; } -export function findTag(e: RawEvent, tag: string) { +export function findTag(e: NostrEvent, tag: string) { const maybeTag = e.tags.find(evTag => { return evTag[0] === tag; }); @@ -507,104 +505,6 @@ export function getUrlHostname(url?: string) { } } -export interface NostrLink { - type: NostrPrefix; - id: string; - kind?: number; - author?: string; - relays?: Array; - encode(): string; -} - -export function validateNostrLink(link: string): boolean { - try { - const parsedLink = parseNostrLink(link); - - if (!parsedLink) { - return false; - } - - if (parsedLink.type === NostrPrefix.PublicKey || parsedLink.type === NostrPrefix.Note) { - return parsedLink.id.length === 64; - } - - return true; - } catch { - return false; - } -} - -export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink | undefined { - const entity = link.startsWith("web+nostr:") || link.startsWith("nostr:") ? link.split(":")[1] : link; - - const isPrefix = (prefix: NostrPrefix) => { - return entity.startsWith(prefix); - }; - - if (isPrefix(NostrPrefix.PublicKey)) { - const id = bech32ToHex(entity); - return { - type: NostrPrefix.PublicKey, - id: id, - encode: () => hexToBech32(NostrPrefix.PublicKey, id), - }; - } else if (isPrefix(NostrPrefix.Note)) { - const id = bech32ToHex(entity); - return { - type: NostrPrefix.Note, - id: id, - encode: () => hexToBech32(NostrPrefix.Note, id), - }; - } else if (isPrefix(NostrPrefix.Profile) || isPrefix(NostrPrefix.Event) || isPrefix(NostrPrefix.Address)) { - const decoded = decodeTLV(entity); - - const id = decoded.find(a => a.type === TLVEntryType.Special)?.value as string; - const relays = decoded.filter(a => a.type === TLVEntryType.Relay).map(a => a.value as string); - const author = decoded.find(a => a.type === TLVEntryType.Author)?.value as string; - const kind = decoded.find(a => a.type === TLVEntryType.Kind)?.value as number; - - const encode = () => { - return entity; // return original - }; - if (isPrefix(NostrPrefix.Profile)) { - return { - type: NostrPrefix.Profile, - id, - relays, - kind, - author, - encode, - }; - } else if (isPrefix(NostrPrefix.Event)) { - return { - type: NostrPrefix.Event, - id, - relays, - kind, - author, - encode, - }; - } else if (isPrefix(NostrPrefix.Address)) { - return { - type: NostrPrefix.Address, - id, - relays, - kind, - author, - encode, - }; - } - } else if (prefixHint) { - return { - type: prefixHint, - id: link, - encode: () => hexToBech32(prefixHint, link), - }; - } else { - throw new Error("Invalid nostr link"); - } -} - export function sanitizeRelayUrl(url: string) { try { return new URL(url).toString(); diff --git a/packages/app/src/State/NoteCreator.ts b/packages/app/src/State/NoteCreator.ts index 43aaee19..e6a8dda8 100644 --- a/packages/app/src/State/NoteCreator.ts +++ b/packages/app/src/State/NoteCreator.ts @@ -1,19 +1,19 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { RawEvent, TaggedRawEvent } from "@snort/nostr"; +import { NostrEvent, TaggedRawEvent } from "@snort/system"; interface NoteCreatorStore { show: boolean; note: string; error: string; active: boolean; - preview?: RawEvent; + preview?: NostrEvent; replyTo?: TaggedRawEvent; showAdvanced: boolean; selectedCustomRelays: false | Array; zapForward: string; sensitive: string; pollOptions?: Array; - otherEvents: Array; + otherEvents: Array; } const InitState: NoteCreatorStore = { @@ -44,7 +44,7 @@ const NoteCreatorSlice = createSlice({ setActive: (state, action: PayloadAction) => { state.active = action.payload; }, - setPreview: (state, action: PayloadAction) => { + setPreview: (state, action: PayloadAction) => { state.preview = action.payload; }, setReplyTo: (state, action: PayloadAction) => { @@ -65,7 +65,7 @@ const NoteCreatorSlice = createSlice({ setPollOptions: (state, action: PayloadAction | undefined>) => { state.pollOptions = action.payload; }, - setOtherEvents: (state, action: PayloadAction>) => { + setOtherEvents: (state, action: PayloadAction>) => { state.otherEvents = action.payload; }, reset: () => InitState, diff --git a/packages/app/src/State/ReBroadcast.ts b/packages/app/src/State/ReBroadcast.ts index acb1ddec..23cc253b 100644 --- a/packages/app/src/State/ReBroadcast.ts +++ b/packages/app/src/State/ReBroadcast.ts @@ -1,10 +1,10 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { RawEvent } from "@snort/nostr"; +import { NostrEvent } from "@snort/system"; interface ReBroadcastStore { show: boolean; selectedCustomRelays: false | Array; - note?: RawEvent; + note?: NostrEvent; } const InitState: ReBroadcastStore = { @@ -19,7 +19,7 @@ const ReBroadcastSlice = createSlice({ setShow: (state, action: PayloadAction) => { state.show = action.payload; }, - setNote: (state, action: PayloadAction) => { + setNote: (state, action: PayloadAction) => { state.note = action.payload; }, setSelectedCustomRelays: (state, action: PayloadAction>) => { diff --git a/packages/app/src/State/Relays/index.ts b/packages/app/src/State/Relays/index.ts deleted file mode 100644 index 6918255f..00000000 --- a/packages/app/src/State/Relays/index.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { FullRelaySettings, HexKey } from "@snort/nostr"; -import { db } from "Db"; -import { unixNowMs, unwrap } from "Util"; - -export class UserRelays { - #store: Map>; - - #snapshot: Readonly>>; - - constructor() { - this.#store = new Map(); - this.#snapshot = Object.freeze(new Map()); - } - - get snapshot() { - return this.#snapshot; - } - - async get(key: HexKey) { - if (!this.#store.has(key) && db.ready) { - const cached = await db.userRelays.get(key); - if (cached) { - this.#store.set(key, cached.relays); - return cached.relays; - } - } - return this.#store.get(key); - } - - async bulkGet(keys: Array) { - const missing = keys.filter(a => !this.#store.has(a)); - if (missing.length > 0 && db.ready) { - const cached = await db.userRelays.bulkGet(missing); - cached.forEach(a => { - if (a) { - this.#store.set(a.pubkey, a.relays); - } - }); - } - return new Map(keys.map(a => [a, this.#store.get(a) ?? []])); - } - - async set(key: HexKey, relays: Array) { - this.#store.set(key, relays); - if (db.ready) { - await db.userRelays.put({ - pubkey: key, - relays, - }); - } - this._update(); - } - - async bulkSet(obj: Record>) { - if (db.ready) { - await db.userRelays.bulkPut( - Object.entries(obj).map(([k, v]) => { - return { - pubkey: k, - relays: v, - }; - }) - ); - } - Object.entries(obj).forEach(([k, v]) => this.#store.set(k, v)); - this._update(); - } - - async preload() { - const start = unixNowMs(); - const keys = await db.userRelays.toCollection().keys(); - const fullCache = await db.userRelays.bulkGet(keys); - this.#store = new Map(fullCache.filter(a => a !== undefined).map(a => [unwrap(a).pubkey, a?.relays ?? []])); - this._update(); - console.debug(`Preloaded ${this.#store.size} users relays in ${(unixNowMs() - start).toLocaleString()} ms`); - } - - private _update() { - this.#snapshot = Object.freeze(new Map(this.#store)); - } -} - -export const FollowsRelays = new UserRelays(); diff --git a/packages/app/src/Subscription/index.ts b/packages/app/src/Subscription/index.ts index e1faf8aa..6486f2b6 100644 --- a/packages/app/src/Subscription/index.ts +++ b/packages/app/src/Subscription/index.ts @@ -1,4 +1,4 @@ -import { unixNow } from "Util"; +import { unixNow } from "SnortUtils"; export enum SubscriptionType { Supporter = 0, diff --git a/packages/app/src/System/ProfileCache.ts b/packages/app/src/System/ProfileCache.ts deleted file mode 100644 index fe96a8e4..00000000 --- a/packages/app/src/System/ProfileCache.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { EventKind, HexKey, TaggedRawEvent } from "@snort/nostr"; -import { ProfileCacheExpire } from "Const"; -import { mapEventToProfile, MetadataCache } from "Cache"; -import { UserCache } from "Cache/UserCache"; -import { PubkeyReplaceableNoteStore, RequestBuilder, System } from "System"; -import { unixNowMs } from "Util"; - -class ProfileLoaderService { - /** - * List of pubkeys to fetch metadata for - */ - WantsMetadata: Set = new Set(); - - constructor() { - this.#FetchMetadata(); - } - - /** - * Request profile metadata for a set of pubkeys - */ - TrackMetadata(pk: HexKey | Array) { - const bufferNow = []; - for (const p of Array.isArray(pk) ? pk : [pk]) { - if (p.length > 0 && this.WantsMetadata.add(p)) { - bufferNow.push(p); - } - } - UserCache.buffer(bufferNow); - } - - /** - * Stop tracking metadata for a set of pubkeys - */ - UntrackMetadata(pk: HexKey | Array) { - for (const p of Array.isArray(pk) ? pk : [pk]) { - if (p.length > 0) { - this.WantsMetadata.delete(p); - } - } - } - - async onProfileEvent(ev: Readonly>) { - for (const e of ev) { - const profile = mapEventToProfile(e); - if (profile) { - await UserCache.update(profile); - } - } - } - - async #FetchMetadata() { - const missingFromCache = await UserCache.buffer([...this.WantsMetadata]); - - const expire = unixNowMs() - ProfileCacheExpire; - const expired = [...this.WantsMetadata] - .filter(a => !missingFromCache.includes(a)) - .filter(a => (UserCache.getFromCache(a)?.loaded ?? 0) < expire); - const missing = new Set([...missingFromCache, ...expired]); - if (missing.size > 0) { - console.debug(`[UserCache] Wants profiles: ${missingFromCache.length} missing, ${expired.length} expired`); - - const sub = new RequestBuilder(`profiles`); - sub - .withFilter() - .kinds([EventKind.SetMetadata]) - .authors([...missing]); - - const q = System.Query(PubkeyReplaceableNoteStore, sub); - // never release this callback, it will stop firing anyway after eose - const releaseOnEvent = q.onEvent(async e => { - await this.onProfileEvent(e); - }); - const results = await new Promise>>(resolve => { - let timeout: ReturnType | undefined = undefined; - const release = q.hook(() => { - if (!q.loading) { - clearTimeout(timeout); - resolve(q.getSnapshotData() ?? []); - console.debug("Profiles finished: ", sub.id); - release(); - } - }); - timeout = setTimeout(() => { - release(); - resolve(q.getSnapshotData() ?? []); - console.debug("Profiles timeout: ", sub.id); - }, 5_000); - }); - - releaseOnEvent(); - const couldNotFetch = [...missing].filter(a => !results.some(b => b.pubkey === a)); - if (couldNotFetch.length > 0) { - console.debug("No profiles: ", couldNotFetch); - const empty = couldNotFetch.map(a => - UserCache.update({ - pubkey: a, - loaded: unixNowMs() - ProfileCacheExpire + 5_000, // expire in 5s - created: 69, - } as MetadataCache) - ); - await Promise.all(empty); - } - } - - setTimeout(() => this.#FetchMetadata(), 500); - } -} - -export const ProfileLoader = new ProfileLoaderService(); diff --git a/packages/app/src/System/Query.test.ts b/packages/app/src/System/Query.test.ts deleted file mode 100644 index 3fece031..00000000 --- a/packages/app/src/System/Query.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Connection } from "@snort/nostr"; -import { Query } from "./Query"; -import { getRandomValues } from "crypto"; -import { FlatNoteStore } from "./NoteCollection"; - -window.crypto = {} as any; -window.crypto.getRandomValues = getRandomValues as any; - -describe("query", () => { - test("progress", () => { - const q = new Query( - "test", - [ - { - kinds: [1], - authors: ["test"], - }, - ], - new FlatNoteStore() - ); - const opt = { - read: true, - write: true, - }; - const c1 = new Connection("wss://one.com", opt); - c1.Down = false; - const c2 = new Connection("wss://two.com", opt); - c2.Down = false; - const c3 = new Connection("wss://three.com", opt); - c3.Down = false; - - q.sendToRelay(c1); - q.sendToRelay(c2); - q.sendToRelay(c3); - - expect(q.progress).toBe(0); - q.eose(q.id, c1); - expect(q.progress).toBe(1 / 3); - q.eose(q.id, c1); - expect(q.progress).toBe(1 / 3); - q.eose(q.id, c2); - expect(q.progress).toBe(2 / 3); - q.eose(q.id, c3); - expect(q.progress).toBe(1); - - const qs = new Query( - "test-1", - [ - { - kinds: [1], - authors: ["test-sub"], - }, - ], - new FlatNoteStore() - ); - q.subQueries.push(qs); - qs.sendToRelay(c1); - - expect(q.progress).toBe(0.5); - q.eose(qs.id, c1); - expect(q.progress).toBe(1); - qs.sendToRelay(c2); - // 1 + 0.5 (1/2 sent sub query) - expect(q.progress).toBe(1.5 / 2); - }); -}); diff --git a/packages/app/src/System/Query.ts b/packages/app/src/System/Query.ts deleted file mode 100644 index 9b9f7a3e..00000000 --- a/packages/app/src/System/Query.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { v4 as uuid } from "uuid"; -import { Connection, RawReqFilter, Nips } from "@snort/nostr"; -import { unixNowMs } from "Util"; -import { NoteStore } from "./NoteCollection"; -/** - * Tracing for relay query status - */ -class QueryTrace { - readonly id: string; - readonly subId: string; - readonly relay: string; - readonly connId: string; - readonly start: number; - sent?: number; - eose?: number; - close?: number; - #wasForceClosed = false; - readonly #fnClose: (id: string) => void; - - constructor(sub: string, relay: string, connId: string, fnClose: (id: string) => void) { - this.id = uuid(); - this.subId = sub; - this.relay = relay; - this.connId = connId; - this.start = unixNowMs(); - this.#fnClose = fnClose; - } - - sentToRelay() { - this.sent = unixNowMs(); - } - - gotEose() { - this.eose = unixNowMs(); - } - - forceEose() { - this.eose = unixNowMs(); - this.#wasForceClosed = true; - } - - sendClose() { - this.close = unixNowMs(); - this.#fnClose(this.subId); - } - - log() { - console.debug( - `QT:${this.id}, ${this.relay}, ${this.subId}, finished=${ - this.finished - }, queued=${this.queued.toLocaleString()}ms, runtime=${this.runtime?.toLocaleString()}ms` - ); - } - - /** - * Time spent in queue - */ - get queued() { - return (this.sent === undefined ? unixNowMs() : this.sent) - this.start; - } - - /** - * Total query runtime - */ - get runtime() { - return (this.eose === undefined ? unixNowMs() : this.eose) - this.start; - } - - /** - * If tracing is finished, we got EOSE or timeout - */ - get finished() { - return this.eose !== undefined; - } -} - -/** - * Active or queued query on the system - */ -export class Query { - /** - * Uniquie ID of this query - */ - id: string; - - /** - * The query payload (REQ filters) - */ - filters: Array; - - /** - * Sub-Queries which are connected to this subscription - */ - subQueries: Array = []; - - /** - * Which relays this query has already been executed on - */ - #tracing: Array = []; - - /** - * Leave the query open until its removed - */ - leaveOpen = false; - - /** - * List of relays to send this query to - */ - relays: Array = []; - - /** - * Time when this query can be removed - */ - #cancelTimeout?: number; - - /** - * Timer used to track tracing status - */ - #checkTrace?: ReturnType; - - /** - * Feed object which collects events - */ - #feed: NoteStore; - - constructor(id: string, filters: Array, feed: NoteStore) { - this.id = id; - this.filters = filters; - this.#feed = feed; - this.#checkTraces(); - } - - get closing() { - return this.#cancelTimeout !== undefined; - } - - get closingAt() { - return this.#cancelTimeout; - } - - get feed() { - return this.#feed; - } - - cancel() { - this.#cancelTimeout = unixNowMs() + 5_000; - } - - unCancel() { - this.#cancelTimeout = undefined; - } - - cleanup() { - console.debug("Cleanup", this.id); - this.#stopCheckTraces(); - } - - sendToRelay(c: Connection) { - if (this.relays.length > 0 && !this.relays.includes(c.Address)) { - return; - } - if (this.relays.length === 0 && c.Ephemeral) { - console.debug("Cant send non-specific REQ to ephemeral connection"); - return; - } - if (this.filters.some(a => a.search) && !c.SupportsNip(Nips.Search)) { - console.debug("Cant send REQ to non-search relay", c.Address); - return; - } - const qt = new QueryTrace(this.id, c.Address, c.Id, x => c.CloseReq(x)); - this.#tracing.push(qt); - c.QueueReq(["REQ", this.id, ...this.filters], () => qt.sentToRelay()); - } - - connectionLost(c: Connection, active: Array, pending: Array) { - const allQueriesLost = [...active, ...pending].filter(a => this.id === a || this.subQueries.some(b => b.id === a)); - if (allQueriesLost.length > 0) { - console.debug("Lost", allQueriesLost, c.Address, c.Id); - } - } - - sendClose() { - for (const qt of this.#tracing) { - qt.sendClose(); - } - for (const sq of this.subQueries) { - sq.sendClose(); - } - this.cleanup(); - } - - eose(sub: string, conn: Readonly) { - const qt = this.#tracing.find(a => a.subId === sub && a.connId === conn.Id); - qt?.gotEose(); - if (sub === this.id) { - console.debug(`[EOSE][${sub}] ${conn.Address}`); - this.#feed.loading = this.progress < 1; - if (!this.leaveOpen && !this.#feed.loading) { - this.sendClose(); - } - } else { - const subQ = this.subQueries.find(a => a.id === sub); - if (subQ) { - subQ.eose(sub, conn); - } else { - throw new Error("No query found"); - } - } - } - - /** - * Get the progress to EOSE, can be used to determine when we should load more content - */ - get progress() { - let thisProgress = this.#tracing.reduce((acc, v) => (acc += v.finished ? 1 : 0), 0) / this.#tracing.length; - if (isNaN(thisProgress)) { - thisProgress = 0; - } - if (this.subQueries.length === 0) { - return thisProgress; - } - - let totalProgress = thisProgress; - for (const sq of this.subQueries) { - totalProgress += sq.progress; - } - return totalProgress / (this.subQueries.length + 1); - } - - #stopCheckTraces() { - if (this.#checkTrace) { - clearInterval(this.#checkTrace); - } - } - - #checkTraces() { - this.#stopCheckTraces(); - this.#checkTrace = setInterval(() => { - for (const v of this.#tracing) { - //v.log(); - if (v.runtime > 5_000 && !v.finished) { - v.forceEose(); - } - } - }, 2_000); - } -} diff --git a/packages/app/src/System/RequestBuilder.test.ts b/packages/app/src/System/RequestBuilder.test.ts deleted file mode 100644 index ec062046..00000000 --- a/packages/app/src/System/RequestBuilder.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { RequestBuilder } from "./RequestBuilder"; - -describe("RequestBuilder", () => { - describe("basic", () => { - test("empty filter", () => { - const b = new RequestBuilder("test"); - b.withFilter(); - expect(b.build()).toEqual([{}]); - }); - test("only kind", () => { - const b = new RequestBuilder("test"); - b.withFilter().kinds([0]); - expect(b.build()).toEqual([{ kinds: [0] }]); - }); - test("empty authors", () => { - const b = new RequestBuilder("test"); - b.withFilter().authors([]); - expect(b.build()).toEqual([{ authors: [] }]); - }); - test("authors/kinds/ids", () => { - const authors = ["a1", "a2"]; - const kinds = [0, 1, 2, 3]; - const ids = ["id1", "id2", "id3"]; - const b = new RequestBuilder("test"); - b.withFilter().authors(authors).kinds(kinds).ids(ids); - expect(b.build()).toEqual([{ ids, authors, kinds }]); - }); - test("authors and kinds, duplicates removed", () => { - const authors = ["a1", "a2"]; - const kinds = [0, 1, 2, 3]; - const ids = ["id1", "id2", "id3"]; - const b = new RequestBuilder("test"); - b.withFilter().ids(ids).authors(authors).kinds(kinds).ids(ids).authors(authors).kinds(kinds); - expect(b.build()).toEqual([{ ids, authors, kinds }]); - }); - test("search", () => { - const b = new RequestBuilder("test"); - b.withFilter().kinds([1]).search("test-search"); - expect(b.build()).toEqual([{ kinds: [1], search: "test-search" }]); - }); - test("timeline", () => { - const authors = ["a1", "a2"]; - const kinds = [0, 1, 2, 3]; - const until = 10; - const since = 5; - const b = new RequestBuilder("test"); - b.withFilter().kinds(kinds).authors(authors).since(since).until(until); - expect(b.build()).toEqual([{ kinds, authors, until, since }]); - }); - test("multi-filter timeline", () => { - const authors = ["a1", "a2"]; - const kinds = [0, 1, 2, 3]; - const until = 10; - const since = 5; - const b = new RequestBuilder("test"); - b.withFilter().kinds(kinds).authors(authors).since(since).until(until); - b.withFilter().kinds(kinds).authors(authors).since(since).until(until); - expect(b.build()).toEqual([ - { kinds, authors, until, since }, - { kinds, authors, until, since }, - ]); - }); - }); -}); diff --git a/packages/app/src/System/RequestBuilder.ts b/packages/app/src/System/RequestBuilder.ts deleted file mode 100644 index 63a66f2d..00000000 --- a/packages/app/src/System/RequestBuilder.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { RawReqFilter, u256, HexKey, EventKind } from "@snort/nostr"; -import { appendDedupe } from "Util"; - -/** - * Which strategy is used when building REQ filters - */ -export enum NostrRequestStrategy { - /** - * Use the users default relays to fetch events, - * this is the fallback option when there is no better way to query a given filter set - */ - DefaultRelays = 1, - - /** - * Using a cached copy of the authors relay lists NIP-65, split a given set of request filters by pubkey - */ - AuthorsRelays = 2, - - /** - * Relay hints are usually provided when using replies - */ - RelayHintedEventIds = 3, -} - -/** - * A built REQ filter ready for sending to System - */ -export interface BuiltRawReqFilter { - id: string; - filter: Array; - relays: Array; - strategy: NostrRequestStrategy; -} - -export interface RequestBuilderOptions { - leaveOpen?: boolean; - relays?: Array; - /** - * Do not apply diff logic and always use full filters for query - */ - skipDiff?: boolean; -} - -/** - * Nostr REQ builder - */ -export class RequestBuilder { - id: string; - #builders: Array; - #options?: RequestBuilderOptions; - - constructor(id: string) { - this.id = id; - this.#builders = []; - } - - get numFilters() { - return this.#builders.length; - } - - get options() { - return this.#options; - } - - withFilter() { - const ret = new RequestFilterBuilder(); - this.#builders.push(ret); - return ret; - } - - withOptions(opt: RequestBuilderOptions) { - this.#options = { - ...this.#options, - ...opt, - }; - return this; - } - - build(): Array { - return this.#builders.map(a => a.filter); - } -} - -/** - * Builder class for a single request filter - */ -export class RequestFilterBuilder { - #filter: RawReqFilter = {}; - #relayHints: Map> = new Map(); - - get filter() { - return { ...this.#filter }; - } - - get relayHints() { - return new Map(this.#relayHints); - } - - ids(ids: Array) { - this.#filter.ids = appendDedupe(this.#filter.ids, ids); - return this; - } - - id(id: u256, relay?: string) { - if (relay) { - this.#relayHints.set(id, appendDedupe(this.#relayHints.get(id), [relay])); - } - return this.ids([id]); - } - - authors(authors?: Array) { - if (!authors) return this; - this.#filter.authors = appendDedupe(this.#filter.authors, authors); - return this; - } - - kinds(kinds?: Array) { - if (!kinds) return this; - this.#filter.kinds = appendDedupe(this.#filter.kinds, kinds); - return this; - } - - since(since?: number) { - if (!since) return this; - this.#filter.since = since; - return this; - } - - until(until?: number) { - if (!until) return this; - this.#filter.until = until; - return this; - } - - limit(limit?: number) { - if (!limit) return this; - this.#filter.limit = limit; - return this; - } - - tag(key: "e" | "p" | "d" | "t" | "r", value?: Array) { - if (!value) return this; - this.#filter[`#${key}`] = value; - return this; - } - - search(keyword?: string) { - if (!keyword) return this; - this.#filter.search = keyword; - return this; - } -} diff --git a/packages/app/src/System/RequestSplitter.test.ts b/packages/app/src/System/RequestSplitter.test.ts deleted file mode 100644 index 0dd214b2..00000000 --- a/packages/app/src/System/RequestSplitter.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { RawReqFilter } from "@snort/nostr"; -import { diffFilters } from "./RequestSplitter"; - -describe("RequestSplitter", () => { - test("single filter add value", () => { - const a: Array = [{ kinds: [0], authors: ["a"] }]; - const b: Array = [{ kinds: [0], authors: ["a", "b"] }]; - const diff = diffFilters(a, b); - expect(diff).toEqual({ filters: [{ kinds: [0], authors: ["b"] }], changed: true }); - }); - test("single filter remove value", () => { - const a: Array = [{ kinds: [0], authors: ["a"] }]; - const b: Array = [{ kinds: [0], authors: ["b"] }]; - const diff = diffFilters(a, b); - expect(diff).toEqual({ filters: [{ kinds: [0], authors: ["b"] }], changed: true }); - }); - test("single filter change critical key", () => { - const a: Array = [{ kinds: [0], authors: ["a"], since: 100 }]; - const b: Array = [{ kinds: [0], authors: ["a", "b"], since: 101 }]; - const diff = diffFilters(a, b); - expect(diff).toEqual({ filters: [{ kinds: [0], authors: ["a", "b"], since: 101 }], changed: true }); - }); - test("multiple filter add value", () => { - const a: Array = [ - { kinds: [0], authors: ["a"] }, - { kinds: [69], authors: ["a"] }, - ]; - const b: Array = [ - { kinds: [0], authors: ["a", "b"] }, - { kinds: [69], authors: ["a", "c"] }, - ]; - const diff = diffFilters(a, b); - expect(diff).toEqual({ - filters: [ - { kinds: [0], authors: ["b"] }, - { kinds: [69], authors: ["c"] }, - ], - changed: true, - }); - }); - test("multiple filter remove value", () => { - const a: Array = [ - { kinds: [0], authors: ["a"] }, - { kinds: [69], authors: ["a"] }, - ]; - const b: Array = [ - { kinds: [0], authors: ["b"] }, - { kinds: [69], authors: ["c"] }, - ]; - const diff = diffFilters(a, b); - expect(diff).toEqual({ - filters: [ - { kinds: [0], authors: ["b"] }, - { kinds: [69], authors: ["c"] }, - ], - changed: true, - }); - }); - test("add filter", () => { - const a: Array = [{ kinds: [0], authors: ["a"] }]; - const b: Array = [ - { kinds: [0], authors: ["a"] }, - { kinds: [69], authors: ["c"] }, - ]; - const diff = diffFilters(a, b); - expect(diff).toEqual({ - filters: [ - { kinds: [0], authors: ["a"] }, - { kinds: [69], authors: ["c"] }, - ], - changed: true, - }); - }); -}); diff --git a/packages/app/src/System/RequestSplitter.ts b/packages/app/src/System/RequestSplitter.ts deleted file mode 100644 index 23794565..00000000 --- a/packages/app/src/System/RequestSplitter.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { RawReqFilter } from "@snort/nostr"; - -export function diffFilters(a: Array, b: Array) { - const result: Array = []; - let anyChanged = false; - for (const [i, bN] of b.entries()) { - const prev: Record = a[i]; - if (!prev) { - result.push(bN); - anyChanged = true; - } else { - // Critical keys changing means the entire filter has changed - const criticalKeys = ["since", "until", "limit"]; - let anyCriticalKeyChanged = false; - for (const [k, v] of Object.entries(bN)) { - if (Array.isArray(v)) { - const prevArray = prev[k] as Array; - const thisArray = v as Array; - const added = thisArray.filter(a => !prevArray.includes(a)); - // support adding new values to array, removing values is ignored since we only care about getting new values - result[i] = { ...result[i], [k]: added.length === 0 ? prevArray : added }; - if (added.length > 0) { - anyChanged = true; - } - } else if (prev[k] !== v) { - result[i] = { ...result[i], [k]: v }; - if (criticalKeys.includes(k)) { - anyCriticalKeyChanged = anyChanged = true; - break; - } - } - } - if (anyCriticalKeyChanged) { - result[i] = bN; - } - } - } - - return { - filters: result, - changed: anyChanged, - }; -} diff --git a/packages/app/src/System/index.ts b/packages/app/src/System/index.ts deleted file mode 100644 index 42a43aef..00000000 --- a/packages/app/src/System/index.ts +++ /dev/null @@ -1,318 +0,0 @@ -import { AuthHandler, TaggedRawEvent, RelaySettings, Connection, RawReqFilter, RawEvent } from "@snort/nostr"; - -import { sanitizeRelayUrl, unixNowMs, unwrap } from "Util"; -import { RequestBuilder } from "./RequestBuilder"; -import { EventBuilder } from "./EventBuilder"; -import { - FlatNoteStore, - NoteStore, - PubkeyReplaceableNoteStore, - ParameterizedReplaceableNoteStore, - ReplaceableNoteStore, -} from "./NoteCollection"; -import { diffFilters } from "./RequestSplitter"; -import { Query } from "./Query"; - -export { - NoteStore, - RequestBuilder, - FlatNoteStore, - PubkeyReplaceableNoteStore, - ParameterizedReplaceableNoteStore, - ReplaceableNoteStore, - Query, - EventBuilder, -}; - -export interface SystemSnapshot { - queries: Array<{ - id: string; - filters: Array; - subFilters: Array; - closing: boolean; - }>; -} - -export type HookSystemSnapshotRelease = () => void; -export type HookSystemSnapshot = () => void; - -/** - * Manages nostr content retrieval system - */ -export class NostrSystem { - /** - * All currently connected websockets - */ - Sockets: Map; - - /** - * All active queries - */ - Queries: Map = new Map(); - - /** - * Handler function for NIP-42 - */ - HandleAuth?: AuthHandler; - - /** - * State change hooks - */ - #stateHooks: Array = []; - - /** - * Current snapshot of the system - */ - #snapshot: Readonly = { queries: [] }; - - constructor() { - this.Sockets = new Map(); - this.#cleanup(); - } - - hook(cb: HookSystemSnapshot): HookSystemSnapshotRelease { - this.#stateHooks.push(cb); - return () => { - const idx = this.#stateHooks.findIndex(a => a === cb); - this.#stateHooks.splice(idx, 1); - }; - } - - getSnapshot(): Readonly { - return this.#snapshot; - } - - /** - * Connect to a NOSTR relay if not already connected - */ - async ConnectToRelay(address: string, options: RelaySettings) { - try { - const addr = unwrap(sanitizeRelayUrl(address)); - if (!this.Sockets.has(addr)) { - const c = new Connection(addr, options, this.HandleAuth); - this.Sockets.set(addr, c); - c.OnEvent = (s, e) => this.OnEvent(s, e); - c.OnEose = s => this.OnEndOfStoredEvents(c, s); - c.OnDisconnect = (a, p) => this.OnRelayDisconnect(c, a, p); - c.OnConnected = () => { - for (const [, q] of this.Queries) { - q.sendToRelay(c); - } - }; - await c.Connect(); - } else { - // update settings if already connected - unwrap(this.Sockets.get(addr)).Settings = options; - } - } catch (e) { - console.error(e); - } - } - - OnRelayDisconnect(c: Connection, active: Array, pending: Array) { - for (const [, q] of this.Queries) { - q.connectionLost(c, active, pending); - } - } - - OnEndOfStoredEvents(c: Readonly, sub: string) { - const q = this.GetQuery(sub); - if (q) { - q.eose(sub, c); - } - } - - OnEvent(sub: string, ev: TaggedRawEvent) { - const q = this.GetQuery(sub); - if (q?.feed) { - q.feed.add(ev); - } - } - - GetQuery(sub: string) { - const subFilterId = /-\d+$/i; - if (sub.match(subFilterId)) { - // feed events back into parent query - sub = sub.split(subFilterId)[0]; - } - return this.Queries.get(sub); - } - - /** - * - * @param address Relay address URL - */ - async ConnectEphemeralRelay(address: string): Promise { - try { - const addr = unwrap(sanitizeRelayUrl(address)); - if (!this.Sockets.has(addr)) { - const c = new Connection(addr, { read: true, write: false }, this.HandleAuth, true); - this.Sockets.set(addr, c); - c.OnEvent = (s, e) => this.OnEvent(s, e); - c.OnEose = s => this.OnEndOfStoredEvents(c, s); - c.OnDisconnect = (a, p) => this.OnRelayDisconnect(c, a, p); - c.OnConnected = () => { - for (const [, q] of this.Queries) { - q.sendToRelay(c); - } - }; - await c.Connect(); - return c; - } - } catch (e) { - console.error(e); - } - } - - /** - * Disconnect from a relay - */ - DisconnectRelay(address: string) { - const c = this.Sockets.get(address); - if (c) { - this.Sockets.delete(address); - c.Close(); - } - } - - Query(type: { new (): T }, req: RequestBuilder | null): Readonly { - /** - * ## Notes - * - * Given a set of existing filters: - * ["REQ", "1", { kinds: [0, 7], authors: [...], since: now()-1hr, until: now() }] - * ["REQ", "2", { kinds: [0, 7], authors: [...], since: now(), limit: 0 }] - * - * ## Problem 1: - * Assume we now want to update sub "1" with a new set of authors, - * what should we do, should we close sub "1" and send the new set or create another - * subscription with the new pubkeys (diff) - * - * Creating a new subscription sounds great but also is a problem when relays limit - * active subscriptions, maybe we should instead queue the new - * subscription (assuming that we expect to close at EOSE) - * - * ## Problem 2: - * When multiple filters a specifid in a single filter but only 1 filter changes, - * ~~same as above~~ - * - * Seems reasonable to do "Queue Diff", should also be possible to collapse multiple - * pending filters for the same subscription - */ - - if (!req) return new type(); - - if (this.Queries.has(req.id)) { - const filters = req.build(); - const q = unwrap(this.Queries.get(req.id)); - q.unCancel(); - - const diff = diffFilters(q.filters, filters); - if (!diff.changed && !req.options?.skipDiff) { - this.#changed(); - return unwrap(q.feed) as Readonly; - } else { - const subQ = new Query(`${q.id}-${q.subQueries.length + 1}`, filters, q.feed); - q.subQueries.push(subQ); - q.filters = filters; - q.feed.loading = true; - this.SendQuery(subQ); - this.#changed(); - return q.feed as Readonly; - } - } else { - return this.AddQuery(type, req); - } - } - - AddQuery(type: { new (): T }, rb: RequestBuilder): T { - const store = new type(); - const q = new Query(rb.id, rb.build(), store); - if (rb.options?.leaveOpen) { - q.leaveOpen = rb.options.leaveOpen; - } - if (rb.options?.relays) { - q.relays = rb.options.relays; - } - - this.Queries.set(rb.id, q); - this.SendQuery(q); - this.#changed(); - return store; - } - - CancelQuery(sub: string) { - const q = this.Queries.get(sub); - if (q) { - q.cancel(); - } - } - - SendQuery(q: Query) { - for (const [, s] of this.Sockets) { - q.sendToRelay(s); - } - } - - /** - * Send events to writable relays - */ - BroadcastEvent(ev: RawEvent) { - for (const [, s] of this.Sockets) { - s.SendEvent(ev); - } - } - - /** - * Write an event to a relay then disconnect - */ - async WriteOnceToRelay(address: string, ev: RawEvent) { - return new Promise((resolve, reject) => { - const c = new Connection(address, { write: true, read: false }, this.HandleAuth, true); - - const t = setTimeout(reject, 5_000); - c.OnConnected = async () => { - clearTimeout(t); - await c.SendAsync(ev); - c.Close(); - resolve(); - }; - c.Connect(); - }); - } - - #changed() { - this.#snapshot = Object.freeze({ - queries: [...this.Queries.values()].map(a => { - return { - id: a.id, - filters: a.filters, - closing: a.closing, - subFilters: a.subQueries.map(a => a.filters).flat(), - }; - }), - }); - for (const h of this.#stateHooks) { - h(); - } - } - - #cleanup() { - const now = unixNowMs(); - let changed = false; - for (const [k, v] of this.Queries) { - if (v.closingAt && v.closingAt < now) { - v.sendClose(); - this.Queries.delete(k); - console.debug("Removed:", k); - changed = true; - } - } - if (changed) { - this.#changed(); - } - setTimeout(() => this.#cleanup(), 1_000); - } -} - -export const System = new NostrSystem(); diff --git a/packages/app/src/Tasks/Nip5Task.tsx b/packages/app/src/Tasks/Nip5Task.tsx index fd711a24..c737ca61 100644 --- a/packages/app/src/Tasks/Nip5Task.tsx +++ b/packages/app/src/Tasks/Nip5Task.tsx @@ -1,6 +1,6 @@ import { FormattedMessage } from "react-intl"; import { Link } from "react-router-dom"; -import { MetadataCache } from "Cache"; +import { MetadataCache } from "@snort/system"; import { BaseUITask } from "Tasks"; export class Nip5Task extends BaseUITask { diff --git a/packages/app/src/Tasks/index.ts b/packages/app/src/Tasks/index.ts index 384c80af..c966d1e6 100644 --- a/packages/app/src/Tasks/index.ts +++ b/packages/app/src/Tasks/index.ts @@ -1,4 +1,4 @@ -import { MetadataCache } from "Cache"; +import { MetadataCache } from "@snort/system"; export interface UITask { id: string; diff --git a/packages/app/src/Toaster.tsx b/packages/app/src/Toaster.tsx index 29a41379..e00f2ac6 100644 --- a/packages/app/src/Toaster.tsx +++ b/packages/app/src/Toaster.tsx @@ -2,7 +2,7 @@ import { ReactNode, useSyncExternalStore } from "react"; import { v4 as uuid } from "uuid"; import ExternalStore from "ExternalStore"; import Icon from "Icons/Icon"; -import { unixNow } from "Util"; +import { unixNow } from "SnortUtils"; import "./Toaster.css"; diff --git a/packages/app/src/Upload/VoidCat.ts b/packages/app/src/Upload/VoidCat.ts index 9dc7dfd6..8eb8a11a 100644 --- a/packages/app/src/Upload/VoidCat.ts +++ b/packages/app/src/Upload/VoidCat.ts @@ -1,10 +1,9 @@ -import { EventKind } from "@snort/nostr"; +import { EventKind, EventPublisher } from "@snort/system"; import { VoidApi } from "@void-cat/api"; import { FileExtensionRegex, VoidCatHost } from "Const"; -import { EventPublisher } from "System/EventPublisher"; import { UploadResult } from "Upload"; -import { magnetURIDecode } from "Util"; +import { magnetURIDecode } from "SnortUtils"; /** * Upload file to void.cat diff --git a/packages/app/src/Upload/index.ts b/packages/app/src/Upload/index.ts index 9f286009..ce993af4 100644 --- a/packages/app/src/Upload/index.ts +++ b/packages/app/src/Upload/index.ts @@ -1,11 +1,11 @@ import useLogin from "Hooks/useLogin"; -import { RawEvent } from "@snort/nostr"; +import { NostrEvent } from "@snort/system"; import NostrBuild from "Upload/NostrBuild"; import VoidCat from "Upload/VoidCat"; import NostrImg from "Upload/NostrImg"; import { KieranPubKey } from "Const"; -import { bech32ToHex } from "Util"; +import { bech32ToHex } from "SnortUtils"; export interface UploadResult { url?: string; @@ -14,7 +14,7 @@ export interface UploadResult { /** * NIP-94 File Header */ - header?: RawEvent; + header?: NostrEvent; } /** diff --git a/packages/app/src/Wallet/Cashu.ts b/packages/app/src/Wallet/Cashu.ts index 7bbf87e0..541e65ba 100644 --- a/packages/app/src/Wallet/Cashu.ts +++ b/packages/app/src/Wallet/Cashu.ts @@ -1,5 +1,4 @@ -import { InvoiceRequest, LNWallet, Sats, WalletError, WalletErrorCode, WalletInfo, WalletInvoice } from "Wallet"; - +import { LNWallet, Sats, WalletError, WalletErrorCode, WalletInfo, WalletInvoice } from "Wallet"; import { CashuMint, CashuWallet as TheCashuWallet, Proof } from "@cashu/cashu-ts"; export class CashuWallet implements LNWallet { diff --git a/packages/app/src/Wallet/LNCWallet.ts b/packages/app/src/Wallet/LNCWallet.ts index fe8d8e37..342dfec8 100644 --- a/packages/app/src/Wallet/LNCWallet.ts +++ b/packages/app/src/Wallet/LNCWallet.ts @@ -1,5 +1,5 @@ import LNC from "@lightninglabs/lnc-web"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; import { InvoiceRequest, LNWallet, @@ -11,6 +11,7 @@ import { WalletInvoice, WalletInvoiceState, } from "Wallet"; +import debug from "debug"; enum Payment_PaymentStatus { UNKNOWN = "UNKNOWN", @@ -22,6 +23,7 @@ enum Payment_PaymentStatus { export class LNCWallet implements LNWallet { #lnc: LNC; + readonly #log = debug("LNC"); private constructor(pairingPhrase?: string, password?: string) { this.#lnc = new LNC({ @@ -90,7 +92,7 @@ export class LNCWallet implements LNWallet { async getBalance(): Promise { const rsp = await this.#lnc.lnd.lightning.channelBalance(); - console.debug(rsp); + this.#log(rsp); return parseInt(rsp.localBalance?.sat ?? "0"); } @@ -112,7 +114,7 @@ export class LNCWallet implements LNWallet { feeLimitSat: "100", }, msg => { - console.debug(msg); + this.#log(msg); if (msg.status === Payment_PaymentStatus.SUCCEEDED) { resolve({ preimage: msg.paymentPreimage, @@ -122,7 +124,7 @@ export class LNCWallet implements LNWallet { } }, err => { - console.debug(err); + this.#log(err); reject(err); } ); @@ -135,7 +137,7 @@ export class LNCWallet implements LNWallet { reversed: true, }); - console.debug(invoices); + this.#log(invoices); return invoices.payments.map(a => { const parsedInvoice = prToWalletInvoice(a.paymentRequest); if (!parsedInvoice) { diff --git a/packages/app/src/Wallet/LNDHub.ts b/packages/app/src/Wallet/LNDHub.ts index 83cb6519..738dfeb9 100644 --- a/packages/app/src/Wallet/LNDHub.ts +++ b/packages/app/src/Wallet/LNDHub.ts @@ -26,7 +26,6 @@ export default class LNDHubWallet implements LNWallet { if (url.startsWith("lndhub://")) { const regex = /^lndhub:\/\/([\S-]+):([\S-]+)@(.*)$/i; const parsedUrl = url.match(regex); - console.debug(parsedUrl); if (!parsedUrl || parsedUrl.length !== 4) { throw new Error("Invalid LNDHUB config"); } diff --git a/packages/app/src/Wallet/NostrWalletConnect.ts b/packages/app/src/Wallet/NostrWalletConnect.ts index 1fd6d70d..8a94a523 100644 --- a/packages/app/src/Wallet/NostrWalletConnect.ts +++ b/packages/app/src/Wallet/NostrWalletConnect.ts @@ -1,7 +1,6 @@ -import { Connection, EventKind, RawEvent } from "@snort/nostr"; -import { EventBuilder } from "System"; -import { EventExt } from "System/EventExt"; +import { Connection, EventKind, NostrEvent, EventBuilder, EventExt } from "@snort/system"; import { LNWallet, WalletError, WalletErrorCode, WalletInfo, WalletInvoice, WalletInvoiceState } from "Wallet"; +import debug from "debug"; interface WalletConnectConfig { relayUrl: string; @@ -122,7 +121,7 @@ export class NostrConnectWallet implements LNWallet { return Promise.resolve([]); } - async #onReply(sub: string, e: RawEvent) { + async #onReply(sub: string, e: NostrEvent) { if (sub === "info") { const pending = this.#commandQueue.get("info"); if (!pending) { @@ -184,7 +183,7 @@ export class NostrConnectWallet implements LNWallet { this.#commandQueue.set(evCommand.id, { resolve: async (o: string) => { const reply = JSON.parse(await EventExt.decryptData(o, this.#config.secret, this.#config.walletPubkey)); - console.debug("NWC", reply); + debug("NWC")("%o", reply); resolve(reply); }, reject, diff --git a/packages/app/src/Wallet/index.ts b/packages/app/src/Wallet/index.ts index 19f826c7..aa93597d 100644 --- a/packages/app/src/Wallet/index.ts +++ b/packages/app/src/Wallet/index.ts @@ -1,6 +1,7 @@ -import { useSyncExternalStore } from "react"; +import { useEffect, useSyncExternalStore } from "react"; -import { decodeInvoice, unwrap } from "Util"; +import ExternalStore from "ExternalStore"; +import { decodeInvoice, unwrap } from "SnortUtils"; import LNDHubWallet from "./LNDHub"; import { NostrConnectWallet } from "./NostrWalletConnect"; import { setupWebLNWalletConfig, WebLNWallet } from "./WebLN"; @@ -124,33 +125,17 @@ export interface WalletStoreSnapshot { wallet?: LNWallet; } -type WalletStateHook = (state: WalletStoreSnapshot) => void; - -export class WalletStore { +export class WalletStore extends ExternalStore { #configs: Array; #instance: Map; - #hooks: Array; - #snapshot: Readonly; - constructor() { + super(); this.#configs = []; this.#instance = new Map(); - this.#hooks = []; - this.#snapshot = Object.freeze({ - configs: [], - }); this.load(false); setupWebLNWalletConfig(this); - this.snapshotState(); - } - - hook(fn: WalletStateHook) { - this.#hooks.push(fn); - return () => { - const idx = this.#hooks.findIndex(a => a === fn); - this.#hooks = this.#hooks.splice(idx, 1); - }; + this.notifyChange(); } list() { @@ -171,9 +156,9 @@ export class WalletStore { const w = this.#activateWallet(activeConfig); if (w) { if ("then" in w) { - w.then(wx => { + w.then(async wx => { this.#instance.set(activeConfig.id, wx); - this.snapshotState(); + this.notifyChange(); }); return undefined; } @@ -209,7 +194,7 @@ export class WalletStore { save() { const json = JSON.stringify(this.#configs); window.localStorage.setItem("wallet-config", json); - this.snapshotState(); + this.notifyChange(); } load(snapshot = true) { @@ -218,7 +203,7 @@ export class WalletStore { this.#configs = JSON.parse(cfg); } if (snapshot) { - this.snapshotState(); + this.notifyChange(); } } @@ -226,21 +211,12 @@ export class WalletStore { this.#instance.forEach(w => w.close()); } - getSnapshot() { - return this.#snapshot; - } - - snapshotState() { - const newState = { + takeSnapshot(): WalletStoreSnapshot { + return { configs: [...this.#configs], config: this.#configs.find(a => a.active), wallet: this.get(), } as WalletStoreSnapshot; - this.#snapshot = Object.freeze(newState); - for (const hook of this.#hooks) { - console.debug(this.#snapshot); - hook(this.#snapshot); - } } #activateWallet(cfg: WalletConfig): LNWallet | Promise | undefined { @@ -270,8 +246,14 @@ window.document.addEventListener("close", () => { }); export function useWallet() { - return useSyncExternalStore( + const wallet = useSyncExternalStore( h => Wallets.hook(h), - () => Wallets.getSnapshot() + () => Wallets.snapshot() ); + useEffect(() => { + if (wallet.wallet?.isReady() === false && wallet.wallet.canAutoLogin()) { + wallet.wallet.login().catch(console.error); + } + }, [wallet]); + return wallet; } diff --git a/packages/app/src/ZapPoolController.ts b/packages/app/src/ZapPoolController.ts index 052d0872..3db499f2 100644 --- a/packages/app/src/ZapPoolController.ts +++ b/packages/app/src/ZapPoolController.ts @@ -3,7 +3,7 @@ import { getDisplayName } from "Element/ProfileImage"; import ExternalStore from "ExternalStore"; import { LNURL } from "LNURL"; import { Toastore } from "Toaster"; -import { unixNow } from "Util"; +import { unixNow } from "SnortUtils"; import { LNWallet, WalletInvoiceState, Wallets } from "Wallet"; export enum ZapPoolRecipientType { @@ -21,13 +21,12 @@ export interface ZapPoolRecipient { } class ZapPool extends ExternalStore> { - #store: Map; + #store = new Map(); #isPayoutInProgress = false; - #lastPayout: number = 0; + #lastPayout = 0; constructor() { super(); - this.#store = new Map(); this.#load(); setTimeout(() => this.#autoPayout().catch(console.error), 5_000); } diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx index e3b5e458..b91d73f6 100644 --- a/packages/app/src/index.tsx +++ b/packages/app/src/index.tsx @@ -9,7 +9,7 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import * as serviceWorkerRegistration from "serviceWorkerRegistration"; import { IntlProvider } from "IntlProvider"; -import { unwrap } from "Util"; +import { unwrap } from "SnortUtils"; import Store from "State/Store"; import Layout from "Pages/Layout"; import LoginPage from "Pages/LoginPage"; @@ -31,8 +31,26 @@ import NostrLinkHandler from "Pages/NostrLinkHandler"; import Thread from "Element/Thread"; import { SubscribeRoutes } from "Pages/subscribe"; import ZapPoolPage from "Pages/ZapPool"; +import DebugPage from "Pages/Debug"; +import { db } from "Db"; +import { preload, UserCache } from "Cache"; +import { LoginStore } from "Login"; +import { NostrSystem, ProfileLoaderService } from "@snort/system"; +import { UserRelays } from "Cache/UserRelayCache"; -// @ts-ignore +/** + * Singleton nostr system + */ +export const System = new NostrSystem({ + get: pk => UserRelays.getFromCache(pk)?.relays, +}); + +/** + * Singleton user profile loader + */ +export const ProfileLoader = new ProfileLoaderService(System, UserCache); + +// @ts-expect-error Setting webpack nonce window.__webpack_nonce__ = "ZmlhdGphZiBzYWlkIHNub3J0LnNvY2lhbCBpcyBwcmV0dHkgZ29vZCwgd2UgbWFkZSBpdCE="; serviceWorkerRegistration.register(); @@ -41,6 +59,29 @@ export const router = createBrowserRouter([ { element: , errorElement: , + loader: async () => { + const login = LoginStore.takeSnapshot(); + db.ready = await db.isAvailable(); + if (db.ready) { + await preload(login.follows.item); + } + + for (const [k, v] of Object.entries(login.relays.item)) { + System.ConnectToRelay(k, v); + } + try { + if ("registerProtocolHandler" in window.navigator) { + window.navigator.registerProtocolHandler( + "web+nostr", + `${window.location.protocol}//${window.location.host}/%s` + ); + console.info("Registered protocol handler for 'web+nostr'"); + } + } catch (e) { + console.error("Failed to register protocol handler", e); + } + return null; + }, children: [ ...RootRoutes, { @@ -99,6 +140,10 @@ export const router = createBrowserRouter([ ...NewUserRoutes, ...WalletRoutes, ...SubscribeRoutes, + { + path: "/debug", + element: , + }, { path: "/*", element: , diff --git a/packages/app/src/service-worker.ts b/packages/app/src/service-worker.ts index 26cf6bf3..497749be 100644 --- a/packages/app/src/service-worker.ts +++ b/packages/app/src/service-worker.ts @@ -1,6 +1,6 @@ /// import {} from "."; -declare var self: ServiceWorkerGlobalScope; +declare const self: ServiceWorkerGlobalScope; import { clientsClaim } from "workbox-core"; import { ExpirationPlugin } from "workbox-expiration"; diff --git a/packages/app/src/setupTests.js b/packages/app/src/setupTests.ts similarity index 59% rename from packages/app/src/setupTests.js rename to packages/app/src/setupTests.ts index 0b5fd4d5..4930f245 100644 --- a/packages/app/src/setupTests.js +++ b/packages/app/src/setupTests.ts @@ -1,4 +1,3 @@ -// @ts-expect-error - we have a folder called util so TS gets confused import { TextEncoder, TextDecoder } from "util"; Object.assign(global, { TextDecoder, TextEncoder }); diff --git a/packages/app/webpack.config.js b/packages/app/webpack.config.js index 55d2f77d..5ac1695c 100644 --- a/packages/app/webpack.config.js +++ b/packages/app/webpack.config.js @@ -56,7 +56,7 @@ const config = { module: { rules: [ { - test: /\.(ts|tsx)$/i, + test: /\.tsx?$/i, use: [ "babel-loader", { diff --git a/packages/nostr/src/index.ts b/packages/nostr/src/index.ts index 2e897582..5a4d885e 100644 --- a/packages/nostr/src/index.ts +++ b/packages/nostr/src/index.ts @@ -1,4 +1,3 @@ -export * from "./legacy" import "./nostr-object" // TODO This file should only contain re-exports and only re-export what is needed diff --git a/packages/nostr/src/legacy/.prettierrc b/packages/nostr/src/legacy/.prettierrc deleted file mode 100644 index 0967ef42..00000000 --- a/packages/nostr/src/legacy/.prettierrc +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/packages/nostr/src/legacy/Const.ts b/packages/nostr/src/legacy/Const.ts deleted file mode 100644 index e3502ce0..00000000 --- a/packages/nostr/src/legacy/Const.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Websocket re-connect timeout - */ -export const DefaultConnectTimeout = 2000; diff --git a/packages/nostr/src/legacy/Util.ts b/packages/nostr/src/legacy/Util.ts deleted file mode 100644 index dcc7147c..00000000 --- a/packages/nostr/src/legacy/Util.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as utils from "@noble/curves/abstract/utils"; -import { bech32 } from "bech32"; - -export function unwrap(v: T | undefined | null): T { - if (v === undefined || v === null) { - throw new Error("missing value"); - } - return v; -} - -/** - * Convert hex to bech32 - */ -export function hexToBech32(hrp: string, hex?: string) { - if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) { - return ""; - } - - try { - const buf = utils.hexToBytes(hex); - return bech32.encode(hrp, bech32.toWords(buf)); - } catch (e) { - console.warn("Invalid hex", hex, e); - return ""; - } -} - -export function sanitizeRelayUrl(url: string) { - try { - return new URL(url).toString(); - } catch { - // ignore - } -} \ No newline at end of file diff --git a/packages/nostr/test/setup.ts b/packages/nostr/test/setup.ts index 25638acf..2a079c77 100644 --- a/packages/nostr/test/setup.ts +++ b/packages/nostr/test/setup.ts @@ -8,8 +8,7 @@ import { parsePublicKey, PublicKey, } from "../src/crypto" -import { RawEvent } from "../src" -import { signEvent, Unsigned } from "../src/event" +import { RawEvent, signEvent, Unsigned } from "../src/event" export const relayUrl = new URL("ws://localhost:12648") @@ -30,7 +29,7 @@ export interface Setup { } export async function setup( - done: jest.DoneCallback, + done: (e?: unknown) => void, test: (setup: Setup) => void | Promise ) { try { diff --git a/packages/system/.npmignore b/packages/system/.npmignore new file mode 100644 index 00000000..91aa0ab2 --- /dev/null +++ b/packages/system/.npmignore @@ -0,0 +1,6 @@ +tests/ +src/ +*.tgz +jest.config.js +worker.ts +yarn* \ No newline at end of file diff --git a/packages/system/jest.config.js b/packages/system/jest.config.js new file mode 100644 index 00000000..04dee89c --- /dev/null +++ b/packages/system/jest.config.js @@ -0,0 +1,9 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + bail: true, + preset: "ts-jest", + testEnvironment: "jsdom", + roots: ["src", "tests"], + moduleDirectories: ["src", "node_modules"], + setupFiles: ["./tests/setupTests.ts"], +}; diff --git a/packages/system/package.json b/packages/system/package.json new file mode 100644 index 00000000..38616bfe --- /dev/null +++ b/packages/system/package.json @@ -0,0 +1,33 @@ +{ + "name": "@snort/system", + "version": "1.0.1", + "description": "Snort nostr system package", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "repository": "https://git.v0l.io/Kieran/snort", + "author": "v0l", + "license": "GPL-3.0-or-later", + "scripts": { + "build": "rm -rf dist && tsc", + "test": "jest" + }, + "files": [ + "src", + "dist" + ], + "devDependencies": { + "@jest/globals": "^29.5.0", + "@types/jest": "^29.5.1", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "ts-jest": "^29.1.0", + "typescript": "^5.0.4" + }, + "dependencies": { + "@noble/curves": "^1.0.0", + "@protobufjs/base64": "^1.1.2", + "bech32": "^2.0.0", + "debug": "^4.3.4", + "uuid": "^9.0.0" + } +} diff --git a/packages/nostr/src/legacy/Connection.ts b/packages/system/src/Connection.ts similarity index 69% rename from packages/nostr/src/legacy/Connection.ts rename to packages/system/src/Connection.ts index 8a1b5723..0c2c0347 100644 --- a/packages/nostr/src/legacy/Connection.ts +++ b/packages/system/src/Connection.ts @@ -2,15 +2,12 @@ import { v4 as uuid } from "uuid"; import { DefaultConnectTimeout } from "./Const"; import { ConnectionStats } from "./ConnectionStats"; -import { RawEvent, ReqCommand, TaggedRawEvent, u256 } from "./index"; +import { NostrEvent, ReqCommand, TaggedRawEvent, u256 } from "./Nostr"; import { RelayInfo } from "./RelayInfo"; -import { unwrap } from "./Util"; +import { unwrap } from "./Utils"; +import ExternalStore from "./ExternalStore"; -export type CustomHook = (state: Readonly) => void; -export type AuthHandler = ( - challenge: string, - relay: string -) => Promise; +export type AuthHandler = (challenge: string, relay: string) => Promise; /** * Relay settings @@ -23,7 +20,7 @@ export interface RelaySettings { /** * Snapshot of connection stats */ -export interface StateSnapshot { +export interface ConnectionStateSnapshot { connected: boolean; disconnects: number; avgLatency: number; @@ -31,39 +28,39 @@ export interface StateSnapshot { received: number; send: number; }; + settings?: RelaySettings; info?: RelayInfo; pendingRequests: Array; activeRequests: Array; id: string; + ephemeral: boolean; + address: string; } -export class Connection { +export class Connection extends ExternalStore { Id: string; Address: string; Socket: WebSocket | null = null; PendingRaw: Array = []; PendingRequests: Array<{ - cmd: ReqCommand, - cb: () => void + cmd: ReqCommand; + cb: () => void; }> = []; - ActiveRequests: Set = new Set(); + ActiveRequests = new Set(); Settings: RelaySettings; Info?: RelayInfo; ConnectTimeout: number = DefaultConnectTimeout; Stats: ConnectionStats = new ConnectionStats(); - StateHooks: Map = new Map(); HasStateChange: boolean = true; - CurrentState: StateSnapshot; - LastState: Readonly; IsClosed: boolean; ReconnectTimer: ReturnType | null; EventsCallback: Map void>; OnConnected?: () => void; OnEvent?: (sub: string, e: TaggedRawEvent) => void; OnEose?: (sub: string) => void; - OnDisconnect?: (active: Array, pending: Array) => void; + OnDisconnect?: (id: string) => void; Auth?: AuthHandler; AwaitingAuth: Map; Authed = false; @@ -71,35 +68,17 @@ export class Connection { EphemeralTimeout: ReturnType | undefined; Down = true; - constructor( - addr: string, - options: RelaySettings, - auth?: AuthHandler, - ephemeral: boolean = false - ) { + constructor(addr: string, options: RelaySettings, auth?: AuthHandler, ephemeral: boolean = false) { + super(); this.Id = uuid(); this.Address = addr; this.Settings = options; - this.CurrentState = { - connected: false, - disconnects: 0, - avgLatency: 0, - events: { - received: 0, - send: 0, - }, - } as StateSnapshot; - this.LastState = Object.freeze({ ...this.CurrentState }); this.IsClosed = false; this.ReconnectTimer = null; this.EventsCallback = new Map(); this.AwaitingAuth = new Map(); this.Auth = auth; this.Ephemeral = ephemeral; - - if (this.Ephemeral) { - this.ResetEphemeralTimeout(); - } } ResetEphemeralTimeout() { @@ -109,7 +88,7 @@ export class Connection { if (this.Ephemeral) { this.EphemeralTimeout = setTimeout(() => { this.Close(); - }, 10_000); + }, 30_000); } } @@ -117,14 +96,11 @@ export class Connection { try { if (this.Info === undefined) { const u = new URL(this.Address); - const rsp = await fetch( - `${u.protocol === "wss:" ? "https:" : "http:"}//${u.host}`, - { - headers: { - accept: "application/nostr+json", - }, - } - ); + const rsp = await fetch(`${u.protocol === "wss:" ? "https:" : "http:"}//${u.host}`, { + headers: { + accept: "application/nostr+json", + }, + }); if (rsp.ok) { const data = await rsp.json(); for (const [k, v] of Object.entries(data)) { @@ -139,12 +115,19 @@ export class Connection { console.warn("Could not load relay information", e); } + if (this.Socket) { + this.Id = uuid(); + this.Socket.onopen = null; + this.Socket.onmessage = null; + this.Socket.onerror = null; + this.Socket.onclose = null; + } this.IsClosed = false; this.Socket = new WebSocket(this.Address); this.Socket.onopen = () => this.OnOpen(); - this.Socket.onmessage = (e) => this.OnMessage(e); - this.Socket.onerror = (e) => this.OnError(e); - this.Socket.onclose = (e) => this.OnClose(e); + this.Socket.onmessage = e => this.OnMessage(e); + this.Socket.onerror = e => this.OnError(e); + this.Socket.onclose = e => this.OnClose(e); } Close() { @@ -154,29 +137,25 @@ export class Connection { this.ReconnectTimer = null; } this.Socket?.close(); - this.#UpdateState(); + this.notifyChange(); } OnOpen() { this.ConnectTimeout = DefaultConnectTimeout; console.log(`[${this.Address}] Open!`); this.Down = false; + if (this.Ephemeral) { + this.ResetEphemeralTimeout(); + } this.OnConnected?.(); + this.#sendPendingRaw(); } OnClose(e: CloseEvent) { if (!this.IsClosed) { - this.OnDisconnect?.([...this.ActiveRequests], this.PendingRequests.map(a => a.cmd[1])) - this.#ResetQueues(); - - // reset connection Id on disconnect, for query-tracking - this.Id = uuid(); - this.ConnectTimeout = this.ConnectTimeout * 2; console.log( - `[${this.Address}] Closed (${e.reason}), trying again in ${( - this.ConnectTimeout / 1000 - ) + `[${this.Address}] Closed (${e.reason}), trying again in ${(this.ConnectTimeout / 1000) .toFixed(0) .toLocaleString()} sec` ); @@ -188,7 +167,12 @@ export class Connection { console.log(`[${this.Address}] Closed!`); this.ReconnectTimer = null; } - this.#UpdateState(); + + this.OnDisconnect?.(this.Id); + this.#ResetQueues(); + // reset connection Id on disconnect, for query-tracking + this.Id = uuid(); + this.notifyChange(); } OnMessage(e: MessageEvent) { @@ -201,13 +185,16 @@ export class Connection { .then(() => this.#sendPendingRaw()) .catch(console.error); this.Stats.EventsReceived++; - this.#UpdateState(); + this.notifyChange(); break; } case "EVENT": { - this.OnEvent?.(msg[1], msg[2]); + this.OnEvent?.(msg[1], { + ...msg[2], + relays: [this.Address], + }); this.Stats.EventsReceived++; - this.#UpdateState(); + this.notifyChange(); break; } case "EOSE": { @@ -239,27 +226,27 @@ export class Connection { OnError(e: Event) { console.error(e); - this.#UpdateState(); + this.notifyChange(); } /** * Send event on this connection */ - SendEvent(e: RawEvent) { + SendEvent(e: NostrEvent) { if (!this.Settings.write) { return; } const req = ["EVENT", e]; this.#SendJson(req); this.Stats.EventsSent++; - this.#UpdateState(); + this.notifyChange(); } /** * Send event on this connection and wait for OK response */ - async SendAsync(e: RawEvent, timeout = 5000) { - return new Promise((resolve) => { + async SendAsync(e: NostrEvent, timeout = 5000) { + return new Promise(resolve => { if (!this.Settings.write) { resolve(); return; @@ -275,37 +262,15 @@ export class Connection { const req = ["EVENT", e]; this.#SendJson(req); this.Stats.EventsSent++; - this.#UpdateState(); + this.notifyChange(); }); } - /** - * Hook status for connection - */ - StatusHook(fnHook: CustomHook) { - const id = uuid(); - this.StateHooks.set(id, fnHook); - return () => { - this.StateHooks.delete(id); - }; - } - - /** - * Returns the current state of this connection - */ - GetState() { - if (this.HasStateChange) { - this.LastState = Object.freeze({ ...this.CurrentState }); - this.HasStateChange = false; - } - return this.LastState; - } - /** * Using relay document to determine if this relay supports a feature */ SupportsNip(n: number) { - return this.Info?.supported_nips?.some((a) => a === n) ?? false; + return this.Info?.supported_nips?.some(a => a === n) ?? false; } /** @@ -315,7 +280,8 @@ export class Connection { QueueReq(cmd: ReqCommand, cbSent: () => void) { if (this.ActiveRequests.size >= this.#maxSubscriptions) { this.PendingRequests.push({ - cmd, cb: cbSent + cmd, + cb: cbSent, }); console.debug("Queuing:", this.Address, cmd); } else { @@ -323,7 +289,7 @@ export class Connection { this.#SendJson(cmd); cbSent(); } - this.#UpdateState(); + this.notifyChange(); } CloseReq(id: string) { @@ -332,7 +298,28 @@ export class Connection { this.OnEose?.(id); this.#SendQueuedRequests(); } - this.#UpdateState(); + this.notifyChange(); + } + + takeSnapshot(): ConnectionStateSnapshot { + return { + connected: this.Socket?.readyState === WebSocket.OPEN, + events: { + received: this.Stats.EventsReceived, + send: this.Stats.EventsSent, + }, + avgLatency: + this.Stats.Latency.length > 0 + ? this.Stats.Latency.reduce((acc, v) => acc + v, 0) / this.Stats.Latency.length + : 0, + disconnects: this.Stats.Disconnects, + info: this.Info, + id: this.Id, + pendingRequests: [...this.PendingRequests.map(a => a.cmd[1])], + activeRequests: [...this.ActiveRequests], + ephemeral: this.Ephemeral, + address: this.Address, + }; } #SendQueuedRequests() { @@ -354,39 +341,16 @@ export class Connection { this.ActiveRequests.clear(); this.PendingRequests = []; this.PendingRaw = []; - this.#UpdateState(); - } - - #UpdateState() { - this.CurrentState.connected = this.Socket?.readyState === WebSocket.OPEN; - this.CurrentState.events.received = this.Stats.EventsReceived; - this.CurrentState.events.send = this.Stats.EventsSent; - this.CurrentState.avgLatency = - this.Stats.Latency.length > 0 - ? this.Stats.Latency.reduce((acc, v) => acc + v, 0) / - this.Stats.Latency.length - : 0; - this.CurrentState.disconnects = this.Stats.Disconnects; - this.CurrentState.info = this.Info; - this.CurrentState.id = this.Id; - this.CurrentState.pendingRequests = [...this.PendingRequests.map(a => a.cmd[1])]; - this.CurrentState.activeRequests = [...this.ActiveRequests]; - this.Stats.Latency = this.Stats.Latency.slice(-20); // trim - this.HasStateChange = true; - this.#NotifyState(); - } - - #NotifyState() { - const state = this.GetState(); - for (const [, h] of this.StateHooks) { - h(state); - } + this.notifyChange(); } #SendJson(obj: object) { const authPending = !this.Authed && (this.AwaitingAuth.size > 0 || this.Info?.limitation?.auth_required === true); if (this.Socket?.readyState !== WebSocket.OPEN || authPending) { this.PendingRaw.push(obj); + if (this.Socket?.readyState === WebSocket.CLOSED && this.Ephemeral && this.IsClosed) { + this.Connect(); + } return false; } @@ -405,7 +369,7 @@ export class Connection { #sendOnWire(obj: unknown) { if (this.Socket?.readyState !== WebSocket.OPEN) { - throw new Error("Socket is not open"); + throw new Error(`Socket is not open, state is ${this.Socket?.readyState}`); } const json = JSON.stringify(obj); this.Socket.send(json); @@ -421,7 +385,7 @@ export class Connection { } this.AwaitingAuth.set(challenge, true); const authEvent = await this.Auth(challenge, this.Address); - return new Promise((resolve) => { + return new Promise(resolve => { if (!authEvent) { authCleanup(); return Promise.reject("no event"); diff --git a/packages/nostr/src/legacy/ConnectionStats.ts b/packages/system/src/ConnectionStats.ts similarity index 100% rename from packages/nostr/src/legacy/ConnectionStats.ts rename to packages/system/src/ConnectionStats.ts diff --git a/packages/system/src/Const.ts b/packages/system/src/Const.ts new file mode 100644 index 00000000..f2e94a65 --- /dev/null +++ b/packages/system/src/Const.ts @@ -0,0 +1,16 @@ +/** + * Websocket re-connect timeout + */ +export const DefaultConnectTimeout = 2000; + +/** + * Hashtag regex + */ +// eslint-disable-next-line no-useless-escape +export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/g; + + +/** + * How long profile cache should be considered valid for + */ + export const ProfileCacheExpire = 1_000 * 60 * 60 * 6; \ No newline at end of file diff --git a/packages/app/src/System/EventBuilder.ts b/packages/system/src/EventBuilder.ts similarity index 91% rename from packages/app/src/System/EventBuilder.ts rename to packages/system/src/EventBuilder.ts index b2168432..a581c71e 100644 --- a/packages/app/src/System/EventBuilder.ts +++ b/packages/system/src/EventBuilder.ts @@ -1,7 +1,8 @@ -import { EventKind, HexKey, NostrPrefix, RawEvent } from "@snort/nostr"; -import { HashtagRegex } from "Const"; -import { getPublicKey, parseNostrLink, unixNow } from "Util"; +import { EventKind, HexKey, NostrPrefix, NostrEvent } from "."; +import { HashtagRegex } from "./Const"; +import { getPublicKey, unixNow } from "./Utils"; import { EventExt } from "./EventExt"; +import { parseNostrLink } from "./NostrLink"; export class EventBuilder { #kind?: EventKind; @@ -63,7 +64,7 @@ export class EventBuilder { kind: this.#kind, created_at: this.#createdAt ?? unixNow(), tags: this.#tags, - } as RawEvent; + } as NostrEvent; ev.id = EventExt.createId(ev); return ev; } diff --git a/packages/app/src/System/EventExt.ts b/packages/system/src/EventExt.ts similarity index 89% rename from packages/app/src/System/EventExt.ts rename to packages/system/src/EventExt.ts index 934ff32b..c4fa03c8 100644 --- a/packages/app/src/System/EventExt.ts +++ b/packages/system/src/EventExt.ts @@ -1,8 +1,8 @@ import * as secp from "@noble/curves/secp256k1"; import * as utils from "@noble/curves/abstract/utils"; -import { EventKind, HexKey, RawEvent, Tag } from "@snort/nostr"; +import { EventKind, HexKey, NostrEvent, Tag } from "."; import base64 from "@protobufjs/base64"; -import { sha256, unixNow } from "Util"; +import { sha256, unixNow } from "./Utils"; export interface Thread { root?: Tag; @@ -15,9 +15,10 @@ export abstract class EventExt { /** * Get the pub key of the creator of this event NIP-26 */ - static getRootPubKey(e: RawEvent): HexKey { + static getRootPubKey(e: NostrEvent): HexKey { const delegation = e.tags.find(a => a[0] === "delegation"); if (delegation?.[1]) { + // todo: verify sig return delegation[1]; } return e.pubkey; @@ -26,12 +27,12 @@ export abstract class EventExt { /** * Sign this message with a private key */ - static async sign(e: RawEvent, key: HexKey) { + static sign(e: NostrEvent, key: HexKey) { e.id = this.createId(e); - const sig = await secp.schnorr.sign(e.id, key); + const sig = secp.schnorr.sign(e.id, key); e.sig = utils.bytesToHex(sig); - if (!(await secp.schnorr.verify(e.sig, e.id, e.pubkey))) { + if (!(secp.schnorr.verify(e.sig, e.id, e.pubkey))) { throw new Error("Signing failed"); } } @@ -40,13 +41,13 @@ export abstract class EventExt { * Check the signature of this message * @returns True if valid signature */ - static async verify(e: RawEvent) { + static verify(e: NostrEvent) { const id = this.createId(e); - const result = await secp.schnorr.verify(e.sig, id, e.pubkey); + const result = secp.schnorr.verify(e.sig, id, e.pubkey); return result; } - static createId(e: RawEvent) { + static createId(e: NostrEvent) { const payload = [0, e.pubkey, e.created_at, e.kind, e.tags, e.content]; const hash = sha256(JSON.stringify(payload)); @@ -69,10 +70,10 @@ export abstract class EventExt { tags: [], id: "", sig: "", - } as RawEvent; + } as NostrEvent; } - static extractThread(ev: RawEvent) { + static extractThread(ev: NostrEvent) { const isThread = ev.tags.some(a => (a[0] === "e" && a[3] !== "mention") || a[0] == "a"); if (!isThread) { return undefined; diff --git a/packages/nostr/src/legacy/EventKind.ts b/packages/system/src/EventKind.ts similarity index 100% rename from packages/nostr/src/legacy/EventKind.ts rename to packages/system/src/EventKind.ts diff --git a/packages/app/src/System/EventPublisher.ts b/packages/system/src/EventPublisher.ts similarity index 88% rename from packages/app/src/System/EventPublisher.ts rename to packages/system/src/EventPublisher.ts index ac58686b..5f388ddc 100644 --- a/packages/app/src/System/EventPublisher.ts +++ b/packages/system/src/EventPublisher.ts @@ -5,29 +5,46 @@ import { FullRelaySettings, HexKey, Lists, - RawEvent, + NostrEvent, RelaySettings, + SystemInterface, TaggedRawEvent, u256, UserMetadata, -} from "@snort/nostr"; +} from "."; -import { DefaultRelays } from "Const"; -import { System } from "System"; -import { unwrap } from "Util"; +import { unwrap } from "./Utils"; import { EventBuilder } from "./EventBuilder"; import { EventExt } from "./EventExt"; -import { barrierQueue, processWorkQueue, WorkQueueItem } from "WorkQueue"; +import { barrierQueue, processWorkQueue, WorkQueueItem } from "./WorkQueue"; const Nip7Queue: Array = []; processWorkQueue(Nip7Queue); export type EventBuilderHook = (ev: EventBuilder) => EventBuilder; +declare global { + interface Window { + nostr?: { + getPublicKey: () => Promise; + signEvent: (event: T) => Promise; + + getRelays?: () => Promise>; + + nip04?: { + encrypt?: (pubkey: HexKey, plaintext: string) => Promise; + decrypt?: (pubkey: HexKey, ciphertext: string) => Promise; + }; + }; + } +} + export class EventPublisher { + #system: SystemInterface; #pubKey: string; #privateKey?: string; - constructor(pubKey: string, privKey?: string) { + constructor(system: SystemInterface, pubKey: string, privKey?: string) { + this.#system = system; if (privKey) { this.#privateKey = privKey; this.#pubKey = utils.bytesToHex(secp.schnorr.getPublicKey(privKey)); @@ -95,28 +112,17 @@ export class EventPublisher { return await this.#sign(eb); } - broadcast(ev: RawEvent) { + broadcast(ev: NostrEvent) { console.debug(ev); - System.BroadcastEvent(ev); - } - - /** - * Write event to DefaultRelays, this is important for profiles / relay lists to prevent bugs - * If a user removes all the DefaultRelays from their relay list and saves that relay list, - * When they open the site again we wont see that updated relay list and so it will appear to reset back to the previous state - */ - broadcastForBootstrap(ev: RawEvent) { - for (const [k] of DefaultRelays) { - System.WriteOnceToRelay(k, ev); - } + this.#system.BroadcastEvent(ev); } /** * Write event to all given relays. */ - broadcastAll(ev: RawEvent, relays: string[]) { + broadcastAll(ev: NostrEvent, relays: string[]) { for (const k of relays) { - System.WriteOnceToRelay(k, ev); + this.#system.WriteOnceToRelay(k, ev); } } @@ -231,7 +237,7 @@ export class EventPublisher { return await this.#sign(eb); } - async react(evRef: RawEvent, content = "+") { + async react(evRef: NostrEvent, content = "+") { const eb = this.#eb(EventKind.Reaction); eb.content(content); eb.tag(["e", evRef.id]); @@ -280,14 +286,14 @@ export class EventPublisher { /** * Repost a note (NIP-18) */ - async repost(note: RawEvent) { + async repost(note: NostrEvent) { const eb = this.#eb(EventKind.Repost); eb.tag(["e", note.id, ""]); eb.tag(["p", note.pubkey]); return await this.#sign(eb); } - async decryptDm(note: RawEvent) { + async decryptDm(note: NostrEvent) { if (note.pubkey !== this.#pubKey && !note.tags.some(a => a[1] === this.#pubKey)) { throw new Error("Can't decrypt, DM does not belong to this user"); } diff --git a/packages/system/src/ExternalStore.ts b/packages/system/src/ExternalStore.ts new file mode 100644 index 00000000..4b1dedea --- /dev/null +++ b/packages/system/src/ExternalStore.ts @@ -0,0 +1,41 @@ +type HookFn = (e?: TSnapshot) => void; + +interface HookFilter { + fn: HookFn; +} + +/** + * Simple React hookable store with manual change notifications + */ +export default abstract class ExternalStore { + #hooks: Array> = []; + #snapshot: Readonly = {} as Readonly; + #changed = true; + + hook(fn: HookFn) { + this.#hooks.push({ + fn, + }); + return () => { + const idx = this.#hooks.findIndex(a => a.fn === fn); + if (idx >= 0) { + this.#hooks.splice(idx, 1); + } + }; + } + + snapshot() { + if (this.#changed) { + this.#snapshot = this.takeSnapshot(); + this.#changed = false; + } + return this.#snapshot; + } + + protected notifyChange(sn?: TSnapshot) { + this.#changed = true; + this.#hooks.forEach(h => h.fn(sn)); + } + + abstract takeSnapshot(): TSnapshot; +} diff --git a/packages/system/src/GossipModel.ts b/packages/system/src/GossipModel.ts new file mode 100644 index 00000000..18aa1e7c --- /dev/null +++ b/packages/system/src/GossipModel.ts @@ -0,0 +1,117 @@ +import { FullRelaySettings, ReqFilter } from "."; +import { unwrap } from "./Utils"; +import debug from "debug"; + +const PickNRelays = 2; + +export interface RelayTaggedFilter { + relay: string; + filter: ReqFilter; +} + +export interface RelayTaggedFilters { + relay: string; + filters: Array; +} + +export interface RelayCache { + get(pubkey?: string): Array | undefined; +} + +export function splitAllByWriteRelays(cache: RelayCache, filters: Array) { + const allSplit = filters + .map(a => splitByWriteRelays(cache, a)) + .reduce((acc, v) => { + for (const vn of v) { + const existing = acc.get(vn.relay); + if (existing) { + existing.push(vn.filter); + } else { + acc.set(vn.relay, [vn.filter]); + } + } + return acc; + }, new Map>()); + + return [...allSplit.entries()].map(([k, v]) => { + return { + relay: k, + filters: v, + } as RelayTaggedFilters; + }); +} + +/** + * Split filters by authors + * @param filter + * @returns + */ +export function splitByWriteRelays(cache: RelayCache, filter: ReqFilter): Array { + if ((filter.authors?.length ?? 0) === 0) + return [ + { + relay: "", + filter, + }, + ]; + + const allRelays = unwrap(filter.authors).map(a => { + return { + key: a, + relays: cache.get(a)?.filter(a => a.settings.write), + }; + }); + + const missing = allRelays.filter(a => a.relays === undefined); + const hasRelays = allRelays.filter(a => a.relays !== undefined); + const relayUserMap = hasRelays.reduce((acc, v) => { + for (const r of unwrap(v.relays)) { + if (!acc.has(r.url)) { + acc.set(r.url, new Set([v.key])); + } else { + unwrap(acc.get(r.url)).add(v.key); + } + } + return acc; + }, new Map>()); + + // selection algo will just pick relays with the most users + const topRelays = [...relayUserMap.entries()].sort(([, v], [, v1]) => v1.size - v.size); + + // - count keys per relay + // - pick n top relays + // - map keys per relay (for subscription filter) + + const userPickedRelays = unwrap(filter.authors).map(k => { + // pick top 3 relays for this key + const relaysForKey = topRelays + .filter(([, v]) => v.has(k)) + .slice(0, PickNRelays) + .map(([k]) => k); + return { k, relaysForKey }; + }); + + const pickedRelays = new Set(userPickedRelays.map(a => a.relaysForKey).flat()); + + const picked = [...pickedRelays].map(a => { + const keysOnPickedRelay = new Set(userPickedRelays.filter(b => b.relaysForKey.includes(a)).map(b => b.k)); + return { + relay: a, + filter: { + ...filter, + authors: [...keysOnPickedRelay], + }, + } as RelayTaggedFilter; + }); + if (missing.length > 0) { + picked.push({ + relay: "", + filter: { + ...filter, + authors: missing.map(a => a.key), + }, + }); + } + debug("GOSSIP")("Picked %o", picked); + return picked; +} diff --git a/packages/nostr/src/legacy/Links.ts b/packages/system/src/Links.ts similarity index 96% rename from packages/nostr/src/legacy/Links.ts rename to packages/system/src/Links.ts index 8f3d4a35..fd854032 100644 --- a/packages/nostr/src/legacy/Links.ts +++ b/packages/system/src/Links.ts @@ -1,6 +1,6 @@ import * as utils from "@noble/curves/abstract/utils"; import { bech32 } from "bech32"; -import { HexKey } from "."; +import { HexKey } from "./Nostr"; export enum NostrPrefix { PublicKey = "npub", @@ -34,14 +34,14 @@ export function encodeTLV(prefix: NostrPrefix, id: string, relays?: string[], ki const tl0 = [0, buf.length, ...buf]; const tl1 = relays - ?.map((a) => { + ?.map(a => { const data = enc.encode(a); return [1, data.length, ...data]; }) .flat() ?? []; const tl2 = author ? [2, 32, ...utils.hexToBytes(author)] : []; - const tl3 = kind ? [3, 4, ...new Uint8Array(new Uint32Array([kind]).buffer).reverse()] : [] + const tl3 = kind ? [3, 4, ...new Uint8Array(new Uint32Array([kind]).buffer).reverse()] : []; return bech32.encode(prefix, bech32.toWords([...tl0, ...tl1, ...tl2, ...tl3]), 1_000); } diff --git a/packages/nostr/src/legacy/Nips.ts b/packages/system/src/Nips.ts similarity index 100% rename from packages/nostr/src/legacy/Nips.ts rename to packages/system/src/Nips.ts diff --git a/packages/nostr/src/legacy/index.ts b/packages/system/src/Nostr.ts similarity index 78% rename from packages/nostr/src/legacy/index.ts rename to packages/system/src/Nostr.ts index 61f9af03..86e66597 100644 --- a/packages/nostr/src/legacy/index.ts +++ b/packages/system/src/Nostr.ts @@ -1,11 +1,6 @@ -export * from "./Connection"; -export { default as EventKind } from "./EventKind"; -export { default as Tag } from "./Tag"; -export * from "./Links"; -export * from "./Nips"; +import { RelaySettings } from "./Connection"; -import { RelaySettings } from "."; -export type RawEvent = { +export interface NostrEvent { id: u256; pubkey: HexKey; created_at: number; @@ -13,9 +8,9 @@ export type RawEvent = { tags: Array>; content: string; sig: string; -}; +} -export interface TaggedRawEvent extends RawEvent { +export interface TaggedRawEvent extends NostrEvent { /** * A list of relays this event was seen on */ @@ -37,12 +32,12 @@ export type MaybeHexKey = HexKey | undefined; */ export type u256 = string; -export type ReqCommand = [cmd: "REQ", id: string, ...filters: Array]; +export type ReqCommand = [cmd: "REQ", id: string, ...filters: Array]; /** * Raw REQ filter object */ -export type RawReqFilter = { +export interface ReqFilter { ids?: u256[]; authors?: u256[]; kinds?: number[]; @@ -55,7 +50,7 @@ export type RawReqFilter = { since?: number; until?: number; limit?: number; -}; +} /** * Medatadata event content diff --git a/packages/system/src/NostrLink.ts b/packages/system/src/NostrLink.ts new file mode 100644 index 00000000..d866c279 --- /dev/null +++ b/packages/system/src/NostrLink.ts @@ -0,0 +1,110 @@ +import { bech32ToHex, hexToBech32 } from "./Utils"; +import { NostrPrefix, decodeTLV, TLVEntryType } from "."; + +export interface NostrLink { + type: NostrPrefix; + id: string; + kind?: number; + author?: string; + relays?: Array; + encode(): string; + } + + export function validateNostrLink(link: string): boolean { + try { + const parsedLink = parseNostrLink(link); + if (!parsedLink) { + return false; + } + if (parsedLink.type === NostrPrefix.PublicKey || parsedLink.type === NostrPrefix.Note) { + return parsedLink.id.length === 64; + } + + return true; + } catch { + return false; + } + } + + export function tryParseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink | undefined { + try { + return parseNostrLink(link, prefixHint); + } catch { + return undefined; + } + } + + export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink { + const entity = link.startsWith("web+nostr:") || link.startsWith("nostr:") ? link.split(":")[1] : link; + + const isPrefix = (prefix: NostrPrefix) => { + return entity.startsWith(prefix); + }; + + if (isPrefix(NostrPrefix.PublicKey)) { + const id = bech32ToHex(entity); + if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); + return { + type: NostrPrefix.PublicKey, + id: id, + encode: () => hexToBech32(NostrPrefix.PublicKey, id), + }; + } else if (isPrefix(NostrPrefix.Note)) { + const id = bech32ToHex(entity); + if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); + return { + type: NostrPrefix.Note, + id: id, + encode: () => hexToBech32(NostrPrefix.Note, id), + }; + } else if (isPrefix(NostrPrefix.Profile) || isPrefix(NostrPrefix.Event) || isPrefix(NostrPrefix.Address)) { + const decoded = decodeTLV(entity); + + const id = decoded.find(a => a.type === TLVEntryType.Special)?.value as string; + const relays = decoded.filter(a => a.type === TLVEntryType.Relay).map(a => a.value as string); + const author = decoded.find(a => a.type === TLVEntryType.Author)?.value as string; + const kind = decoded.find(a => a.type === TLVEntryType.Kind)?.value as number; + + const encode = () => { + return entity; // return original + }; + if (isPrefix(NostrPrefix.Profile)) { + if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); + return { + type: NostrPrefix.Profile, + id, + relays, + kind, + author, + encode, + }; + } else if (isPrefix(NostrPrefix.Event)) { + if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); + return { + type: NostrPrefix.Event, + id, + relays, + kind, + author, + encode, + }; + } else if (isPrefix(NostrPrefix.Address)) { + return { + type: NostrPrefix.Address, + id, + relays, + kind, + author, + encode, + }; + } + } else if (prefixHint) { + return { + type: prefixHint, + id: link, + encode: () => hexToBech32(prefixHint, link), + }; + } + throw new Error("Invalid nostr link"); + } + \ No newline at end of file diff --git a/packages/system/src/NostrSystem.ts b/packages/system/src/NostrSystem.ts new file mode 100644 index 00000000..4ecc1e33 --- /dev/null +++ b/packages/system/src/NostrSystem.ts @@ -0,0 +1,239 @@ +import debug from "debug"; + +import ExternalStore from "./ExternalStore"; +import { NostrEvent, TaggedRawEvent } from "./Nostr"; +import { AuthHandler, Connection, RelaySettings, ConnectionStateSnapshot } from "./Connection"; +import { Query } from "./Query"; +import { RelayCache } from "./GossipModel"; +import { NoteStore } from "./NoteCollection"; +import { BuiltRawReqFilter, RequestBuilder } from "./RequestBuilder"; +import { unwrap, sanitizeRelayUrl } from "./Utils"; +import { SystemInterface, SystemSnapshot } from "."; + +/** + * Manages nostr content retrieval system + */ +export class NostrSystem extends ExternalStore implements SystemInterface { + /** + * All currently connected websockets + */ + #sockets = new Map(); + + /** + * All active queries + */ + Queries: Map = new Map(); + + /** + * Handler function for NIP-42 + */ + HandleAuth?: AuthHandler; + + #log = debug("System"); + #relayCache: RelayCache; + + constructor(relayCache: RelayCache) { + super(); + this.#relayCache = relayCache; + this.#cleanup(); + } + + get Sockets(): ConnectionStateSnapshot[] { + return [...this.#sockets.values()].map(a => a.snapshot()); + } + + /** + * Connect to a NOSTR relay if not already connected + */ + async ConnectToRelay(address: string, options: RelaySettings) { + try { + const addr = unwrap(sanitizeRelayUrl(address)); + if (!this.#sockets.has(addr)) { + const c = new Connection(addr, options, this.HandleAuth?.bind(this)); + this.#sockets.set(addr, c); + c.OnEvent = (s, e) => this.OnEvent(s, e); + c.OnEose = s => this.OnEndOfStoredEvents(c, s); + c.OnDisconnect = id => this.OnRelayDisconnect(id); + await c.Connect(); + } else { + // update settings if already connected + unwrap(this.#sockets.get(addr)).Settings = options; + } + } catch (e) { + console.error(e); + } + } + + OnRelayDisconnect(id: string) { + for (const [, q] of this.Queries) { + q.connectionLost(id); + } + } + + OnEndOfStoredEvents(c: Readonly, sub: string) { + for (const [, v] of this.Queries) { + v.eose(sub, c); + } + } + + OnEvent(sub: string, ev: TaggedRawEvent) { + for (const [, v] of this.Queries) { + v.onEvent(sub, ev); + } + } + + /** + * + * @param address Relay address URL + */ + async ConnectEphemeralRelay(address: string): Promise { + try { + const addr = unwrap(sanitizeRelayUrl(address)); + if (!this.#sockets.has(addr)) { + const c = new Connection(addr, { read: true, write: false }, this.HandleAuth?.bind(this), true); + this.#sockets.set(addr, c); + c.OnEvent = (s, e) => this.OnEvent(s, e); + c.OnEose = s => this.OnEndOfStoredEvents(c, s); + c.OnDisconnect = id => this.OnRelayDisconnect(id); + await c.Connect(); + return c; + } + } catch (e) { + console.error(e); + } + } + + /** + * Disconnect from a relay + */ + DisconnectRelay(address: string) { + const c = this.#sockets.get(address); + if (c) { + this.#sockets.delete(address); + c.Close(); + } + } + + GetQuery(id: string): Query | undefined { + return this.Queries.get(id); + } + + Query(type: { new (): T }, req: RequestBuilder): Query { + const existing = this.Queries.get(req.id); + if (existing) { + const filters = !req.options?.skipDiff + ? req.buildDiff(this.#relayCache, existing.flatFilters) + : req.build(this.#relayCache); + if (filters.length === 0 && !!req.options?.skipDiff) { + return existing; + } else { + for (const subQ of filters) { + this.SendQuery(existing, subQ); + } + this.notifyChange(); + return existing; + } + } else { + const store = new type(); + + const filters = req.build(this.#relayCache); + const q = new Query(req.id, store, req.options?.leaveOpen); + this.Queries.set(req.id, q); + for (const subQ of filters) { + this.SendQuery(q, subQ); + } + this.notifyChange(); + return q; + } + } + + async SendQuery(q: Query, qSend: BuiltRawReqFilter) { + if (qSend.relay) { + this.#log("Sending query to %s %O", qSend.relay, qSend); + const s = this.#sockets.get(qSend.relay); + if (s) { + const qt = q.sendToRelay(s, qSend); + if (qt) { + return [qt]; + } + } else { + const nc = await this.ConnectEphemeralRelay(qSend.relay); + if (nc) { + const qt = q.sendToRelay(nc, qSend); + if (qt) { + return [qt]; + } + } else { + console.warn("Failed to connect to new relay for:", qSend.relay, q); + } + } + } else { + const ret = []; + for (const [, s] of this.#sockets) { + if (!s.Ephemeral) { + const qt = q.sendToRelay(s, qSend); + if (qt) { + ret.push(qt); + } + } + } + return ret; + } + return []; + } + + /** + * Send events to writable relays + */ + BroadcastEvent(ev: NostrEvent) { + for (const [, s] of this.#sockets) { + s.SendEvent(ev); + } + } + + /** + * Write an event to a relay then disconnect + */ + async WriteOnceToRelay(address: string, ev: NostrEvent) { + return new Promise((resolve, reject) => { + const c = new Connection(address, { write: true, read: false }, this.HandleAuth, true); + + const t = setTimeout(reject, 5_000); + c.OnConnected = async () => { + clearTimeout(t); + await c.SendAsync(ev); + c.Close(); + resolve(); + }; + c.Connect(); + }); + } + + takeSnapshot(): SystemSnapshot { + return { + queries: [...this.Queries.values()].map(a => { + return { + id: a.id, + filters: a.filters, + subFilters: [], + }; + }), + }; + } + + #cleanup() { + let changed = false; + for (const [k, v] of this.Queries) { + if (v.canRemove()) { + v.sendClose(); + this.Queries.delete(k); + this.#log("Deleted query %s", k); + changed = true; + } + } + if (changed) { + this.notifyChange(); + } + setTimeout(() => this.#cleanup(), 1_000); + } +} diff --git a/packages/app/src/System/NoteCollection.ts b/packages/system/src/NoteCollection.ts similarity index 90% rename from packages/app/src/System/NoteCollection.ts rename to packages/system/src/NoteCollection.ts index 3363869b..75fa8647 100644 --- a/packages/app/src/System/NoteCollection.ts +++ b/packages/system/src/NoteCollection.ts @@ -1,5 +1,5 @@ -import { TaggedRawEvent, u256 } from "@snort/nostr"; -import { findTag } from "Util"; +import { TaggedRawEvent, u256 } from "."; +import { appendDedupe, findTag } from "./Utils"; export interface StoreSnapshot { data: TSnapshot | undefined; @@ -57,6 +57,7 @@ export abstract class HookedNoteStore i data: undefined, }; #needsSnapshot = true; + #nextNotifyTimer?: ReturnType; get snapshot() { this.#updateSnapshot(); @@ -89,8 +90,8 @@ export abstract class HookedNoteStore i } onEvent(cb: OnEventCallback): OnEventCallbackRelease { - const existing = this.#eventHooks.findIndex(a => a === cb); - if (existing === -1) { + const existing = this.#eventHooks.find(a => a === cb); + if (!existing) { this.#eventHooks.push(cb); return () => { const idx = this.#eventHooks.findIndex(a => a === cb); @@ -106,8 +107,13 @@ export abstract class HookedNoteStore i protected onChange(changes: Readonly>): void { this.#needsSnapshot = true; - for (const hk of this.#hooks) { - hk(); + if (!this.#nextNotifyTimer) { + this.#nextNotifyTimer = setTimeout(() => { + this.#nextNotifyTimer = undefined; + for (const hk of this.#hooks) { + hk(); + } + }, 500); } if (changes.length > 0) { for (const hkE of this.#eventHooks) { @@ -142,6 +148,11 @@ export class FlatNoteStore extends HookedNoteStore b.id === a.id); + if (existing) { + existing.relays = appendDedupe(existing.relays, a.relays); + } } }); diff --git a/packages/system/src/ProfileCache.ts b/packages/system/src/ProfileCache.ts new file mode 100644 index 00000000..9c59972d --- /dev/null +++ b/packages/system/src/ProfileCache.ts @@ -0,0 +1,124 @@ +import { EventKind, HexKey, SystemInterface, TaggedRawEvent, PubkeyReplaceableNoteStore, RequestBuilder } from "."; +import { ProfileCacheExpire } from "./Const"; +import { CacheStore, mapEventToProfile, MetadataCache } from "./cache"; +import { unixNowMs } from "./Utils"; +import debug from "debug"; + +export class ProfileLoaderService { + #system: SystemInterface; + #cache: CacheStore; + + /** + * List of pubkeys to fetch metadata for + */ + WantsMetadata: Set = new Set(); + + readonly #log = debug("ProfileCache"); + + constructor(system: SystemInterface, cache: CacheStore) { + this.#system = system; + this.#cache = cache; + this.#FetchMetadata(); + } + + /** + * Request profile metadata for a set of pubkeys + */ + TrackMetadata(pk: HexKey | Array) { + const bufferNow = []; + for (const p of Array.isArray(pk) ? pk : [pk]) { + if (p.length > 0 && this.WantsMetadata.add(p)) { + bufferNow.push(p); + } + } + this.#cache.buffer(bufferNow); + } + + /** + * Stop tracking metadata for a set of pubkeys + */ + UntrackMetadata(pk: HexKey | Array) { + for (const p of Array.isArray(pk) ? pk : [pk]) { + if (p.length > 0) { + this.WantsMetadata.delete(p); + } + } + } + + async onProfileEvent(e: Readonly) { + const profile = mapEventToProfile(e); + if (profile) { + await this.#cache.update(profile); + } + } + + async #FetchMetadata() { + const missingFromCache = await this.#cache.buffer([...this.WantsMetadata]); + + const expire = unixNowMs() - ProfileCacheExpire; + const expired = [...this.WantsMetadata] + .filter(a => !missingFromCache.includes(a)) + .filter(a => (this.#cache.getFromCache(a)?.loaded ?? 0) < expire); + const missing = new Set([...missingFromCache, ...expired]); + if (missing.size > 0) { + this.#log("Wants profiles: %d missing, %d expired", missingFromCache.length, expired.length); + + const sub = new RequestBuilder("profiles"); + sub + .withOptions({ + skipDiff: true, + }) + .withFilter() + .kinds([EventKind.SetMetadata]) + .authors([...missing]); + + const newProfiles = new Set(); + const q = this.#system.Query(PubkeyReplaceableNoteStore, sub); + const feed = (q?.feed as PubkeyReplaceableNoteStore) ?? new PubkeyReplaceableNoteStore(); + // never release this callback, it will stop firing anyway after eose + const releaseOnEvent = feed.onEvent(async e => { + for (const pe of e) { + newProfiles.add(pe.id); + await this.onProfileEvent(pe); + } + }); + const results = await new Promise>>(resolve => { + let timeout: ReturnType | undefined = undefined; + const release = feed.hook(() => { + if (!feed.loading) { + clearTimeout(timeout); + resolve(feed.getSnapshotData() ?? []); + this.#log("Profiles finished: %s", sub.id); + release(); + } + }); + timeout = setTimeout(() => { + release(); + resolve(feed.getSnapshotData() ?? []); + this.#log("Profiles timeout: %s", sub.id); + }, 5_000); + }); + + releaseOnEvent(); + const couldNotFetch = [...missing].filter(a => !results.some(b => b.pubkey === a)); + if (couldNotFetch.length > 0) { + this.#log("No profiles: %o", couldNotFetch); + const empty = couldNotFetch.map(a => + this.#cache.update({ + pubkey: a, + loaded: unixNowMs() - ProfileCacheExpire + 5_000, // expire in 5s + created: 69, + } as MetadataCache) + ); + await Promise.all(empty); + } + + // When we fetch an expired profile and its the same as what we already have + // onEvent is not fired and the loaded timestamp never gets updated + const expiredSame = results.filter(a => !newProfiles.has(a.id) && expired.includes(a.pubkey)); + await Promise.all(expiredSame.map(v => this.onProfileEvent(v))); + } + + setTimeout(() => this.#FetchMetadata(), 500); + } +} diff --git a/packages/system/src/Query.ts b/packages/system/src/Query.ts new file mode 100644 index 00000000..f56d7baa --- /dev/null +++ b/packages/system/src/Query.ts @@ -0,0 +1,280 @@ +import { v4 as uuid } from "uuid"; +import debug from "debug"; +import { Connection, ReqFilter, Nips, TaggedRawEvent } from "."; +import { unixNowMs, unwrap } from "./Utils"; +import { NoteStore } from "./NoteCollection"; +import { flatMerge } from "./RequestMerger"; +import { BuiltRawReqFilter } from "./RequestBuilder"; +import { expandFilter } from "./RequestExpander"; + +/** + * Tracing for relay query status + */ +class QueryTrace { + readonly id: string; + readonly start: number; + sent?: number; + eose?: number; + close?: number; + #wasForceClosed = false; + readonly #fnClose: (id: string) => void; + readonly #fnProgress: () => void; + + constructor( + readonly relay: string, + readonly filters: Array, + readonly connId: string, + fnClose: (id: string) => void, + fnProgress: () => void + ) { + this.id = uuid(); + this.start = unixNowMs(); + this.#fnClose = fnClose; + this.#fnProgress = fnProgress; + } + + sentToRelay() { + this.sent = unixNowMs(); + this.#fnProgress(); + } + + gotEose() { + this.eose = unixNowMs(); + this.#fnProgress(); + } + + forceEose() { + this.eose = unixNowMs(); + this.#wasForceClosed = true; + this.#fnProgress(); + this.sendClose(); + } + + sendClose() { + this.close = unixNowMs(); + this.#fnClose(this.id); + this.#fnProgress(); + } + + /** + * Time spent in queue + */ + get queued() { + return (this.sent === undefined ? unixNowMs() : this.#wasForceClosed ? unwrap(this.eose) : this.sent) - this.start; + } + + /** + * Total query runtime + */ + get runtime() { + return (this.eose === undefined ? unixNowMs() : this.eose) - this.start; + } + + /** + * Total time spent waiting for relay to respond + */ + get responseTime() { + return this.finished ? unwrap(this.eose) - unwrap(this.sent) : 0; + } + + /** + * If tracing is finished, we got EOSE or timeout + */ + get finished() { + return this.eose !== undefined; + } +} + +export interface QueryBase { + /** + * Uniquie ID of this query + */ + id: string; + + /** + * The query payload (REQ filters) + */ + filters: Array; + + /** + * List of relays to send this query to + */ + relays?: Array; +} + +/** + * Active or queued query on the system + */ +export class Query implements QueryBase { + /** + * Uniquie ID of this query + */ + id: string; + + /** + * Which relays this query has already been executed on + */ + #tracing: Array = []; + + /** + * Leave the query open until its removed + */ + #leaveOpen = false; + + /** + * Time when this query can be removed + */ + #cancelAt?: number; + + /** + * Timer used to track tracing status + */ + #checkTrace?: ReturnType; + + /** + * Feed object which collects events + */ + #feed: NoteStore; + + #log = debug("Query"); + + constructor(id: string, feed: NoteStore, leaveOpen?: boolean) { + this.id = id; + this.#feed = feed; + this.#leaveOpen = leaveOpen ?? false; + this.#checkTraces(); + } + + canRemove() { + return this.#cancelAt !== undefined && this.#cancelAt < unixNowMs(); + } + + /** + * Recompute the complete set of compressed filters from all query traces + */ + get filters() { + return flatMerge(this.flatFilters); + } + + get flatFilters() { + return this.#tracing.flatMap(a => a.filters).flatMap(expandFilter); + } + + get feed() { + return this.#feed; + } + + onEvent(sub: string, e: TaggedRawEvent) { + for (const t of this.#tracing) { + if (t.id === sub) { + this.feed.add(e); + break; + } + } + } + + /** + * This function should be called when this Query object and FeedStore is no longer needed + */ + cancel() { + this.#cancelAt = unixNowMs() + 5_000; + } + + uncancel() { + this.#cancelAt = undefined; + } + + cleanup() { + this.#stopCheckTraces(); + } + + sendToRelay(c: Connection, subq: BuiltRawReqFilter) { + if (!this.#canSendQuery(c, subq)) { + return; + } + return this.#sendQueryInternal(c, subq); + } + + connectionLost(id: string) { + this.#tracing.filter(a => a.connId == id).forEach(a => a.forceEose()); + } + + sendClose() { + for (const qt of this.#tracing) { + qt.sendClose(); + } + this.cleanup(); + } + + eose(sub: string, conn: Readonly) { + const qt = this.#tracing.find(a => a.id === sub && a.connId === conn.Id); + qt?.gotEose(); + if (!this.#leaveOpen) { + qt?.sendClose(); + } + } + + /** + * Get the progress to EOSE, can be used to determine when we should load more content + */ + get progress() { + const thisProgress = this.#tracing.reduce((acc, v) => (acc += v.finished ? 1 : 0), 0) / this.#tracing.length; + if (isNaN(thisProgress)) { + return 0; + } + return thisProgress; + } + + #onProgress() { + const isFinished = this.progress === 1; + if (this.feed.loading !== isFinished) { + this.#log("%s loading=%s, progress=%d", this.id, this.feed.loading, this.progress); + this.feed.loading = isFinished; + } + } + + #stopCheckTraces() { + if (this.#checkTrace) { + clearInterval(this.#checkTrace); + } + } + + #checkTraces() { + this.#stopCheckTraces(); + this.#checkTrace = setInterval(() => { + for (const v of this.#tracing) { + if (v.runtime > 5_000 && !v.finished) { + v.forceEose(); + } + } + }, 500); + } + + #canSendQuery(c: Connection, q: BuiltRawReqFilter) { + if (q.relay && q.relay !== c.Address) { + return false; + } + if (!q.relay && c.Ephemeral) { + this.#log("Cant send non-specific REQ to ephemeral connection %O %O %O", q, q.relay, c); + return false; + } + if (q.filters.some(a => a.search) && !c.SupportsNip(Nips.Search)) { + this.#log("Cant send REQ to non-search relay", c.Address); + return false; + } + return true; + } + + #sendQueryInternal(c: Connection, q: BuiltRawReqFilter) { + const qt = new QueryTrace( + c.Address, + q.filters, + c.Id, + x => c.CloseReq(x), + () => this.#onProgress() + ); + this.#tracing.push(qt); + c.QueueReq(["REQ", qt.id, ...q.filters], () => qt.sentToRelay()); + return qt; + } +} diff --git a/packages/nostr/src/legacy/RelayInfo.ts b/packages/system/src/RelayInfo.ts similarity index 91% rename from packages/nostr/src/legacy/RelayInfo.ts rename to packages/system/src/RelayInfo.ts index 438fe41f..984af50b 100644 --- a/packages/nostr/src/legacy/RelayInfo.ts +++ b/packages/system/src/RelayInfo.ts @@ -11,6 +11,6 @@ export interface RelayInfo { max_subscriptions: number; max_filters: number; max_event_tags: number; - auth_required: boolean + auth_required: boolean; }; } diff --git a/packages/system/src/RequestBuilder.ts b/packages/system/src/RequestBuilder.ts new file mode 100644 index 00000000..bfc4e2e6 --- /dev/null +++ b/packages/system/src/RequestBuilder.ts @@ -0,0 +1,259 @@ +import { ReqFilter, u256, HexKey, EventKind } from "."; +import { appendDedupe, dedupe, unixNowMs } from "./Utils"; +import { diffFilters } from "./RequestSplitter"; +import { RelayCache, splitAllByWriteRelays, splitByWriteRelays } from "./GossipModel"; +import { mergeSimilar } from "./RequestMerger"; +import { FlatReqFilter, expandFilter } from "./RequestExpander"; +import debug from "debug"; + +/** + * Which strategy is used when building REQ filters + */ +export enum RequestStrategy { + /** + * Use the users default relays to fetch events, + * this is the fallback option when there is no better way to query a given filter set + */ + DefaultRelays = 1, + + /** + * Using a cached copy of the authors relay lists NIP-65, split a given set of request filters by pubkey + */ + AuthorsRelays = 2, + + /** + * Relay hints are usually provided when using replies + */ + RelayHintedEventIds = 3, +} + +/** + * A built REQ filter ready for sending to System + */ +export interface BuiltRawReqFilter { + filters: Array; + relay: string; + strategy: RequestStrategy; +} + +export interface RequestBuilderOptions { + leaveOpen?: boolean; + relays?: Array; + /** + * Do not apply diff logic and always use full filters for query + */ + skipDiff?: boolean; +} + +/** + * Nostr REQ builder + */ +export class RequestBuilder { + id: string; + #builders: Array; + #options?: RequestBuilderOptions; + + constructor(id: string) { + this.id = id; + this.#builders = []; + } + + get numFilters() { + return this.#builders.length; + } + + get options() { + return this.#options; + } + + withFilter() { + const ret = new RequestFilterBuilder(); + this.#builders.push(ret); + return ret; + } + + withOptions(opt: RequestBuilderOptions) { + this.#options = { + ...this.#options, + ...opt, + }; + return this; + } + + buildRaw(): Array { + return this.#builders.map(f => f.filter); + } + + build(relays: RelayCache): Array { + const expanded = this.#builders.flatMap(a => a.build(relays, this.id)); + return this.#groupByRelay(expanded); + } + + /** + * Detects a change in request from a previous set of filters + * @param q All previous filters merged + * @returns + */ + buildDiff(relays: RelayCache, filters: Array): Array { + const start = unixNowMs(); + const next = this.#builders.flatMap(f => expandFilter(f.filter)) + const diff = diffFilters(filters, next); + const ts = (unixNowMs() - start); + const log = debug("buildDiff"); + log("%s %d ms", this.id, ts); + if (ts > 200) { + console.warn(diff, filters); + } + if (diff.changed) { + log(diff); + return splitAllByWriteRelays(relays, diff.added).map(a => { + return { + strategy: RequestStrategy.AuthorsRelays, + filters: a.filters, + relay: a.relay, + }; + }); + } + return []; + } + + /** + * Merge a set of expanded filters into the smallest number of subscriptions by merging similar requests + * @param expanded + * @returns + */ + #groupByRelay(expanded: Array) { + const relayMerged = expanded.reduce((acc, v) => { + const existing = acc.get(v.relay); + if (existing) { + existing.push(v); + } else { + acc.set(v.relay, [v]); + } + return acc; + }, new Map>()); + + const filtersSquashed = [...relayMerged.values()].map(a => { + return { + filters: mergeSimilar(a.flatMap(b => b.filters)), + relay: a[0].relay, + strategy: a[0].strategy, + } as BuiltRawReqFilter; + }); + + return filtersSquashed; + } +} + +/** + * Builder class for a single request filter + */ +export class RequestFilterBuilder { + #filter: ReqFilter = {}; + #relayHints = new Map>(); + + get filter() { + return { ...this.#filter }; + } + + get relayHints() { + return new Map(this.#relayHints); + } + + ids(ids: Array) { + this.#filter.ids = appendDedupe(this.#filter.ids, ids); + return this; + } + + id(id: u256, relay?: string) { + if (relay) { + this.#relayHints.set(id, appendDedupe(this.#relayHints.get(id), [relay])); + } + return this.ids([id]); + } + + authors(authors?: Array) { + if (!authors) return this; + this.#filter.authors = appendDedupe(this.#filter.authors, authors); + return this; + } + + kinds(kinds?: Array) { + if (!kinds) return this; + this.#filter.kinds = appendDedupe(this.#filter.kinds, kinds); + return this; + } + + since(since?: number) { + if (!since) return this; + this.#filter.since = since; + return this; + } + + until(until?: number) { + if (!until) return this; + this.#filter.until = until; + return this; + } + + limit(limit?: number) { + if (!limit) return this; + this.#filter.limit = limit; + return this; + } + + tag(key: "e" | "p" | "d" | "t" | "r", value?: Array) { + if (!value) return this; + this.#filter[`#${key}`] = value; + return this; + } + + search(keyword?: string) { + if (!keyword) return this; + this.#filter.search = keyword; + return this; + } + + /** + * Build/expand this filter into a set of relay specific queries + */ + build(relays: RelayCache, id: string): Array { + // when querying for specific event ids with relay hints + // take the first approach which is to split the filter by relay + if (this.#filter.ids && this.#relayHints.size > 0) { + const relays = dedupe([...this.#relayHints.values()].flat()); + return relays.map(r => { + return { + filters: [ + { + ...this.#filter, + ids: [...this.#relayHints.entries()].filter(([, v]) => v.includes(r)).map(([k]) => k), + }, + ], + relay: r, + strategy: RequestStrategy.RelayHintedEventIds, + }; + }); + } + + // If any authors are set use the gossip model to fetch data for each author + if (this.#filter.authors) { + const split = splitByWriteRelays(relays, this.#filter); + return split.map(a => { + return { + filters: [a.filter], + relay: a.relay, + strategy: RequestStrategy.AuthorsRelays, + }; + }); + } + + return [ + { + filters: [this.filter], + relay: "", + strategy: RequestStrategy.DefaultRelays, + }, + ]; + } +} diff --git a/packages/system/src/RequestExpander.ts b/packages/system/src/RequestExpander.ts new file mode 100644 index 00000000..8aff4238 --- /dev/null +++ b/packages/system/src/RequestExpander.ts @@ -0,0 +1,48 @@ +import { u256, ReqFilter } from "./Nostr"; + +export interface FlatReqFilter { + ids?: u256; + authors?: u256; + kinds?: number; + "#e"?: u256; + "#p"?: u256; + "#t"?: string; + "#d"?: string; + "#r"?: string; + search?: string; + since?: number; + until?: number; + limit?: number; +} + +/** + * Expand a filter into its most fine grained form + */ +export function expandFilter(f: ReqFilter): Array { + const ret: Array = []; + const src = Object.entries(f); + const keys = src.filter(([, v]) => Array.isArray(v)).map(a => a[0]); + const props = src.filter(([, v]) => !Array.isArray(v)); + + function generateCombinations(index: number, currentCombination: FlatReqFilter) { + if (index === keys.length) { + ret.push(currentCombination); + return; + } + + const key = keys[index]; + const values = (f as Record>)[key]; + + for (let i = 0; i < values.length; i++) { + const value = values[i]; + const updatedCombination = { ...currentCombination, [key]: value }; + generateCombinations(index + 1, updatedCombination); + } + } + + generateCombinations(0, { + ...Object.fromEntries(props), + }); + + return ret; +} diff --git a/packages/system/src/RequestMatcher.ts b/packages/system/src/RequestMatcher.ts new file mode 100644 index 00000000..8845702d --- /dev/null +++ b/packages/system/src/RequestMatcher.ts @@ -0,0 +1,20 @@ +import { NostrEvent, ReqFilter } from "./Nostr"; + +export function eventMatchesFilter(ev: NostrEvent, filter: ReqFilter) { + if (!(filter.ids?.includes(ev.id) ?? false)) { + return false; + } + if (!(filter.authors?.includes(ev.pubkey) ?? false)) { + return false; + } + if (!(filter.kinds?.includes(ev.kind) ?? false)) { + return false; + } + if (filter.since && ev.created_at < filter.since) { + return false; + } + if (filter.until && ev.created_at > filter.until) { + return false; + } + return true; +} diff --git a/packages/system/src/RequestMerger.ts b/packages/system/src/RequestMerger.ts new file mode 100644 index 00000000..7dde780f --- /dev/null +++ b/packages/system/src/RequestMerger.ts @@ -0,0 +1,147 @@ +import { distance } from "./Utils"; +import { ReqFilter } from "."; +import { FlatReqFilter } from "./RequestExpander"; + +/** + * Keys which can change the entire meaning of the filter outside the array types + */ +const DiscriminatorKeys = ["since", "until", "limit", "search"]; + +export function canMergeFilters(a: FlatReqFilter | ReqFilter, b: FlatReqFilter | ReqFilter): boolean { + const aObj = a as Record; + const bObj = b as Record; + for (const key of DiscriminatorKeys) { + if (key in aObj || key in bObj) { + if (aObj[key] !== bObj[key]) { + return false; + } + } + } + return distance(a, b) <= 1; +} + +export function mergeSimilar(filters: Array): Array { + const ret = []; + + const fCopy = [...filters]; + while (fCopy.length > 0) { + const current = fCopy.shift()!; + const mergeSet = [current]; + for (let i = 0; i < fCopy.length; i++) { + const f = fCopy[i]; + if (!mergeSet.some(v => !canMergeFilters(v, f))) { + mergeSet.push(fCopy.splice(i, 1)[0]); + i--; + } + } + ret.push(simpleMerge(mergeSet)); + } + return ret; +} + +/** + * Simply flatten all filters into one + * @param filters + * @returns + */ +export function simpleMerge(filters: Array) { + const result: any = {}; + + filters.forEach(filter => { + Object.entries(filter).forEach(([key, value]) => { + if (Array.isArray(value)) { + if (result[key] === undefined) { + result[key] = [...value]; + } else { + const toAdd = value.filter(a => !result[key].includes(a)); + result[key].push(...toAdd); + } + } else { + result[key] = value; + } + }); + }); + + return result as ReqFilter; +} + +/** + * Check if a filter includes another filter, as in the bigger filter will include the same results as the samller filter + * @param bigger + * @param smaller + * @returns + */ +export function filterIncludes(bigger: ReqFilter, smaller: ReqFilter) { + const outside = bigger as Record | number>; + for (const [k, v] of Object.entries(smaller)) { + if (outside[k] === undefined) { + return false; + } + if (Array.isArray(v) && v.some(a => !(outside[k] as Array).includes(a))) { + return false; + } + if (typeof v === "number") { + if (k === "since" && (outside[k] as number) > v) { + return false; + } + if (k === "until" && (outside[k] as number) < v) { + return false; + } + // limit cannot be checked and is ignored + } + } + return true; +} + +/** + * Merge expanded flat filters into combined concise filters + * @param all + * @returns + */ +export function flatMerge(all: Array): Array { + let ret: Array = []; + + // to compute filters which can be merged we need to calucate the distance change between each filter + // then we can merge filters which are exactly 1 change diff from each other + + function mergeFiltersInSet(filters: Array) { + return filters.reduce((acc, a) => { + Object.entries(a).forEach(([k, v]) => { + if (DiscriminatorKeys.includes(k)) { + acc[k] = v; + } else { + acc[k] ??= []; + if (!acc[k].includes(v)) { + acc[k].push(v); + } + } + }) + return acc; + }, {} as any) as ReqFilter; + } + + // reducer, kinda verbose + while (all.length > 0) { + const currentFilter = all.shift()!; + const mergeSet = [currentFilter]; + + for (let i = 0; i < all.length; i++) { + const f = all[i]; + + if (mergeSet.every(a => canMergeFilters(a, f))) { + mergeSet.push(all.splice(i, 1)[0]); + i--; + } + } + ret.push(mergeFiltersInSet(mergeSet)); + } + + while (true) { + const n = mergeSimilar([...ret]); + if (n.length === ret.length) { + break; + } + ret = n; + } + return ret; +} diff --git a/packages/system/src/RequestSplitter.ts b/packages/system/src/RequestSplitter.ts new file mode 100644 index 00000000..7f430895 --- /dev/null +++ b/packages/system/src/RequestSplitter.ts @@ -0,0 +1,15 @@ +import { flatFilterEq } from "./Utils"; +import { FlatReqFilter } from "./RequestExpander"; +import { flatMerge } from "./RequestMerger"; + +export function diffFilters(prev: Array, next: Array, calcRemoved?: boolean) { + const added = next.filter(a => !prev.some(b => flatFilterEq(a, b))); + const removed = calcRemoved ? prev.filter(a => !next.some(b => flatFilterEq(a, b))) : []; + + const changed = added.length > 0 || removed.length > 0; + return { + added: changed ? flatMerge(added) : [], + removed: changed ? flatMerge(removed) : [], + changed, + }; +} diff --git a/packages/system/src/SystemWorker.ts b/packages/system/src/SystemWorker.ts new file mode 100644 index 00000000..ab9dfaf3 --- /dev/null +++ b/packages/system/src/SystemWorker.ts @@ -0,0 +1,64 @@ +import { SystemSnapshot, SystemInterface } from "."; +import { AuthHandler, ConnectionStateSnapshot, RelaySettings } from "./Connection"; +import ExternalStore from "./ExternalStore"; +import { NostrEvent } from "./Nostr"; +import { NoteStore } from "./NoteCollection"; +import { Query } from "./Query"; +import { RequestBuilder } from "./RequestBuilder"; + +export class SystemWorker extends ExternalStore implements SystemInterface { + #port: MessagePort; + + constructor() { + super(); + if ("SharedWorker" in window) { + const worker = new SharedWorker("/system.js"); + this.#port = worker.port; + this.#port.onmessage = m => this.#onMessage(m); + } else { + throw new Error("SharedWorker is not supported"); + } + } + + HandleAuth?: AuthHandler; + + get Sockets(): ConnectionStateSnapshot[] { + throw new Error("Method not implemented."); + } + + Query(type: new () => T, req: RequestBuilder | null): Query | undefined { + throw new Error("Method not implemented."); + } + + CancelQuery(sub: string): void { + throw new Error("Method not implemented."); + } + + GetQuery(sub: string): Query | undefined { + throw new Error("Method not implemented."); + } + + ConnectToRelay(address: string, options: RelaySettings): Promise { + throw new Error("Method not implemented."); + } + + DisconnectRelay(address: string): void { + throw new Error("Method not implemented."); + } + + BroadcastEvent(ev: NostrEvent): void { + throw new Error("Method not implemented."); + } + + WriteOnceToRelay(relay: string, ev: NostrEvent): Promise { + throw new Error("Method not implemented."); + } + + takeSnapshot(): SystemSnapshot { + throw new Error("Method not implemented."); + } + + #onMessage(e: MessageEvent) { + console.debug(e); + } +} diff --git a/packages/nostr/src/legacy/Tag.ts b/packages/system/src/Tag.ts similarity index 90% rename from packages/nostr/src/legacy/Tag.ts rename to packages/system/src/Tag.ts index b3ecfb98..b08128bd 100644 --- a/packages/nostr/src/legacy/Tag.ts +++ b/packages/system/src/Tag.ts @@ -1,5 +1,5 @@ -import { HexKey, u256 } from "./index"; -import { unwrap } from "./Util"; +import { HexKey, u256 } from "./Nostr"; +import { unwrap } from "./Utils"; export default class Tag { Original: string[]; @@ -45,7 +45,7 @@ export default class Tag { break; } case "a": { - this.ATag = tag[1]; + this.ATag = tag[1]; break; } case "t": { @@ -67,7 +67,7 @@ export default class Tag { switch (this.Key) { case "e": { let ret = ["e", this.Event, this.Relay, this.Marker]; - const trimEnd = ret.reverse().findIndex((a) => a !== undefined); + const trimEnd = ret.reverse().findIndex(a => a !== undefined); ret = ret.reverse().slice(0, ret.length - trimEnd); return ret; } diff --git a/packages/system/src/Utils.ts b/packages/system/src/Utils.ts new file mode 100644 index 00000000..40651e49 --- /dev/null +++ b/packages/system/src/Utils.ts @@ -0,0 +1,168 @@ +import * as utils from "@noble/curves/abstract/utils"; +import * as secp from "@noble/curves/secp256k1"; +import { sha256 as sha2 } from "@noble/hashes/sha256"; +import { bech32 } from "bech32"; +import { NostrEvent, ReqFilter, u256 } from "./Nostr"; +import { FlatReqFilter } from "RequestExpander"; + +export function unwrap(v: T | undefined | null): T { + if (v === undefined || v === null) { + throw new Error("missing value"); + } + return v; +} + +/** + * Convert hex to bech32 + */ +export function hexToBech32(hrp: string, hex?: string) { + if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) { + return ""; + } + + try { + const buf = utils.hexToBytes(hex); + return bech32.encode(hrp, bech32.toWords(buf)); + } catch (e) { + console.warn("Invalid hex", hex, e); + return ""; + } +} + +export function sanitizeRelayUrl(url: string) { + try { + return new URL(url).toString(); + } catch { + // ignore + } +} + +export function unixNow() { + return Math.floor(unixNowMs() / 1000); +} + +export function unixNowMs() { + return new Date().getTime(); +} + +export function deepEqual(x: any, y: any): boolean { + const ok = Object.keys, + tx = typeof x, + ty = typeof y; + + return x && y && tx === "object" && tx === ty + ? ok(x).length === ok(y).length && ok(x).every(key => deepEqual(x[key], y[key])) + : x === y; +} + +export function reqFilterEq(a: FlatReqFilter | ReqFilter, b: FlatReqFilter | ReqFilter): boolean { + return equalProp(a.ids, b.ids) + && equalProp(a.kinds, b.kinds) + && equalProp(a.authors, b.authors) + && equalProp(a.limit, b.limit) + && equalProp(a.since, b.since) + && equalProp(a.until, b.until) + && equalProp(a.search, b.search) + && equalProp(a["#e"], b["#e"]) + && equalProp(a["#p"], b["#p"]) + && equalProp(a["#t"], b["#t"]) + && equalProp(a["#d"], b["#d"]) + && equalProp(a["#r"], b["#r"]); +} + +export function flatFilterEq(a: FlatReqFilter, b: FlatReqFilter): boolean { + return a.ids === b.ids + && a.kinds === b.kinds + && a["#e"] === b["#e"] + && a.authors === b.authors + && a.limit === b.limit + && a.since === b.since + && a.until === b.until + && a.search === b.search + && a["#p"] === b["#p"] + && a["#t"] === b["#t"] + && a["#d"] === b["#d"] + && a["#r"] === b["#r"]; +} + +export function equalProp(a: string | number | Array | undefined, b: string | number | Array | undefined) { + if ((a !== undefined && b === undefined) || (a === undefined && b !== undefined)) { + return false; + } + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) { + return false; + } + if (!a.every(v => b.includes(v))) { + return false; + } + } + return a === b; +} + +/** + * Compute the "distance" between two objects by comparing their difference in properties + * Missing/Added keys result in +10 distance + * This is not recursive + */ +export function distance(a: any, b: any): number { + const keys1 = Object.keys(a); + const keys2 = Object.keys(b); + const maxKeys = keys1.length > keys2.length ? keys1 : keys2; + + let distance = 0; + for (const key of maxKeys) { + if (key in a && key in b) { + if (Array.isArray(a[key]) && Array.isArray(b[key])) { + const aa = a[key] as Array; + const bb = b[key] as Array; + if (aa.length === bb.length) { + if (aa.some(v => !bb.includes(v))) { + distance++; + } + } else { + distance++; + } + } else if (a[key] !== b[key]) { + distance++; + } + } else { + distance += 10; + } + } + + return distance; +} + +export function dedupe(v: Array) { + return [...new Set(v)]; +} + +export function appendDedupe(a?: Array, b?: Array) { + return dedupe([...(a ?? []), ...(b ?? [])]); +} + +export function findTag(e: NostrEvent, tag: string) { + const maybeTag = e.tags.find(evTag => { + return evTag[0] === tag; + }); + return maybeTag && maybeTag[1]; +} + +export const sha256 = (str: string | Uint8Array): u256 => { + return utils.bytesToHex(sha2(str)); +} + +export function getPublicKey(privKey: string) { + return utils.bytesToHex(secp.schnorr.getPublicKey(privKey)); +} + +export function bech32ToHex(str: string) { + try { + const nKey = bech32.decode(str, 1_000); + const buff = bech32.fromWords(nKey.words); + return utils.bytesToHex(Uint8Array.from(buff)); + } catch (e) { + return str; + } +} diff --git a/packages/system/src/WorkQueue.ts b/packages/system/src/WorkQueue.ts new file mode 100644 index 00000000..37df22e0 --- /dev/null +++ b/packages/system/src/WorkQueue.ts @@ -0,0 +1,30 @@ +export interface WorkQueueItem { + next: () => Promise; + resolve(v: unknown): void; + reject(e: unknown): void; +} + +export async function processWorkQueue(queue?: Array, queueDelay = 200) { + while (queue && queue.length > 0) { + const v = queue.shift(); + if (v) { + try { + const ret = await v.next(); + v.resolve(ret); + } catch (e) { + v.reject(e); + } + } + } + setTimeout(() => processWorkQueue(queue, queueDelay), queueDelay); +} + +export const barrierQueue = async (queue: Array, then: () => Promise): Promise => { + return new Promise((resolve, reject) => { + queue.push({ + next: then, + resolve, + reject, + }); + }); +}; diff --git a/packages/system/src/cache/index.ts b/packages/system/src/cache/index.ts new file mode 100644 index 00000000..408c1ab7 --- /dev/null +++ b/packages/system/src/cache/index.ts @@ -0,0 +1,76 @@ +import { HexKey, NostrEvent, UserMetadata } from ".."; +import { hexToBech32, unixNowMs } from "../Utils"; + +export interface MetadataCache extends UserMetadata { + /** + * When the object was saved in cache + */ + loaded: number; + + /** + * When the source metadata event was created + */ + created: number; + + /** + * The pubkey of the owner of this metadata + */ + pubkey: HexKey; + + /** + * The bech32 encoded pubkey + */ + npub: string; + + /** + * Pubkey of zapper service + */ + zapService?: HexKey; + + /** + * If the nip05 is valid for this user + */ + isNostrAddressValid: boolean; +} + +export function mapEventToProfile(ev: NostrEvent) { + try { + const data: UserMetadata = JSON.parse(ev.content); + let ret = { + ...data, + pubkey: ev.pubkey, + npub: hexToBech32("npub", ev.pubkey), + created: ev.created_at, + loaded: unixNowMs(), + } as MetadataCache; + + // sanitize non-string/number + for (const [k, v] of Object.entries(ret)) { + if (typeof v !== "number" && typeof v !== "string") { + (ret as any)[k] = undefined; + } + } + return ret; + } catch (e) { + console.error("Failed to parse JSON", ev, e); + } +} + +export interface CacheStore { + preload(): Promise; + getFromCache(key?: string): T | undefined; + get(key?: string): Promise; + bulkGet(keys: Array): Promise>; + set(obj: T): Promise; + bulkSet(obj: Array): Promise; + update(m: TCachedWithCreated): Promise<"new" | "updated" | "refresh" | "no_change"> + + /** + * Loads a list of rows from disk cache + * @param keys List of ids to load + * @returns Keys that do not exist on disk cache + */ + buffer(keys: Array): Promise>; + + clear(): Promise; +} diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts new file mode 100644 index 00000000..4f11456c --- /dev/null +++ b/packages/system/src/index.ts @@ -0,0 +1,44 @@ +import { AuthHandler, RelaySettings, ConnectionStateSnapshot } from "./Connection"; +import { RequestBuilder } from "./RequestBuilder"; +import { NoteStore } from "./NoteCollection"; +import { Query } from "./Query"; +import { NostrEvent, ReqFilter } from "./Nostr"; + +export * from "./NostrSystem"; +export { default as EventKind } from "./EventKind"; +export * from "./Nostr"; +export * from "./Links"; +export { default as Tag } from "./Tag"; +export * from "./Nips"; +export * from "./RelayInfo"; +export * from "./EventExt"; +export * from "./Connection"; +export * from "./NoteCollection"; +export * from "./RequestBuilder"; +export * from "./EventPublisher"; +export * from "./EventBuilder"; +export * from "./NostrLink"; +export * from "./cache"; +export * from "./ProfileCache"; + +export interface SystemInterface { + /** + * Handler function for NIP-42 + */ + HandleAuth?: AuthHandler; + get Sockets(): Array; + GetQuery(id: string): Query | undefined; + Query(type: { new(): T }, req: RequestBuilder | null): Query | undefined; + ConnectToRelay(address: string, options: RelaySettings): Promise; + DisconnectRelay(address: string): void; + BroadcastEvent(ev: NostrEvent): void; + WriteOnceToRelay(relay: string, ev: NostrEvent): Promise; +} + +export interface SystemSnapshot { + queries: Array<{ + id: string; + filters: Array; + subFilters: Array; + }>; +} \ No newline at end of file diff --git a/packages/app/src/System/NoteCollection.test.ts b/packages/system/tests/NoteCollection.test.ts similarity index 90% rename from packages/app/src/System/NoteCollection.test.ts rename to packages/system/tests/NoteCollection.test.ts index 7a992bf4..613bc63d 100644 --- a/packages/app/src/System/NoteCollection.test.ts +++ b/packages/system/tests/NoteCollection.test.ts @@ -1,5 +1,6 @@ -import { TaggedRawEvent } from "@snort/nostr"; -import { FlatNoteStore, ReplaceableNoteStore } from "./NoteCollection"; +import { TaggedRawEvent } from "../src/Nostr"; +import { describe, expect } from "@jest/globals"; +import { FlatNoteStore, ReplaceableNoteStore } from "../src/NoteCollection"; describe("NoteStore", () => { describe("flat", () => { diff --git a/packages/system/tests/Query.test.ts b/packages/system/tests/Query.test.ts new file mode 100644 index 00000000..3c026319 --- /dev/null +++ b/packages/system/tests/Query.test.ts @@ -0,0 +1,113 @@ +import { Connection } from "../src"; +import { describe, expect } from "@jest/globals"; +import { Query } from "../src/Query"; +import { getRandomValues } from "crypto"; +import { FlatNoteStore } from "../src/NoteCollection"; +import { RequestStrategy } from "../src/RequestBuilder"; + +window.crypto = {} as any; +window.crypto.getRandomValues = getRandomValues as any; + +describe("query", () => { + test("progress", () => { + const q = new Query("test", new FlatNoteStore()); + const opt = { + read: true, + write: true, + }; + const c1 = new Connection("wss://one.com", opt); + c1.Down = false; + const c2 = new Connection("wss://two.com", opt); + c2.Down = false; + const c3 = new Connection("wss://three.com", opt); + c3.Down = false; + + const f = { + relay: "", + strategy: RequestStrategy.DefaultRelays, + filters: [ + { + kinds: [1], + authors: ["test"], + }, + ], + }; + const qt1 = q.sendToRelay(c1, f); + const qt2 = q.sendToRelay(c2, f); + const qt3 = q.sendToRelay(c3, f); + + expect(q.progress).toBe(0); + q.eose(qt1!.id, c1); + expect(q.progress).toBe(1 / 3); + q.eose(qt1!.id, c1); + expect(q.progress).toBe(1 / 3); + q.eose(qt2!.id, c2); + expect(q.progress).toBe(2 / 3); + q.eose(qt3!.id, c3); + expect(q.progress).toBe(1); + + const qs = { + relay: "", + strategy: RequestStrategy.DefaultRelays, + filters: [ + { + kinds: [1], + authors: ["test-sub"], + }, + ], + }; + const qt = q.sendToRelay(c1, qs); + + expect(q.progress).toBe(3 / 4); + q.eose(qt!.id, c1); + expect(q.progress).toBe(1); + q.sendToRelay(c2, qs); + expect(q.progress).toBe(4 / 5); + }); + + it("should merge all sub-query filters", () => { + const q = new Query("test", new FlatNoteStore()); + const c0 = new Connection("wss://test.com", { read: true, write: true }); + q.sendToRelay(c0, { + filters: [ + { + authors: ["a"], + kinds: [1], + }, + ], + relay: "", + strategy: RequestStrategy.DefaultRelays, + }); + q.sendToRelay(c0, { + filters: [ + { + authors: ["b"], + kinds: [1, 2], + }, + ], + relay: "", + strategy: RequestStrategy.DefaultRelays, + }); + q.sendToRelay(c0, { + filters: [ + { + authors: ["c"], + kinds: [2], + }, + ], + relay: "", + strategy: RequestStrategy.DefaultRelays, + }); + + expect(q.filters).toEqual([ + { + authors: ["a", "b"], + kinds: [1], + }, + { + authors: ["b", "c"], + kinds: [2], + }, + ]); + }); +}); diff --git a/packages/system/tests/RequestBuilder.test.ts b/packages/system/tests/RequestBuilder.test.ts new file mode 100644 index 00000000..3289900b --- /dev/null +++ b/packages/system/tests/RequestBuilder.test.ts @@ -0,0 +1,164 @@ +import { RelayCache } from "../src/GossipModel"; +import { RequestBuilder, RequestStrategy } from "../src/RequestBuilder"; +import { describe, expect } from "@jest/globals"; +import { expandFilter } from "../src/RequestExpander"; + +const DummyCache = { + get: (pk?: string) => { + if (!pk) return undefined; + + return [ + { + url: `wss://${pk}.com/`, + settings: { + read: true, + write: true, + }, + }, + ]; + }, +} as RelayCache; + +describe("RequestBuilder", () => { + describe("basic", () => { + test("empty filter", () => { + const b = new RequestBuilder("test"); + b.withFilter(); + expect(b.buildRaw()).toEqual([{}]); + }); + test("only kind", () => { + const b = new RequestBuilder("test"); + b.withFilter().kinds([0]); + expect(b.buildRaw()).toMatchObject([{ kinds: [0] }]); + }); + test("empty authors", () => { + const b = new RequestBuilder("test"); + b.withFilter().authors([]); + expect(b.buildRaw()).toMatchObject([{ authors: [] }]); + }); + test("authors/kinds/ids", () => { + const authors = ["a1", "a2"]; + const kinds = [0, 1, 2, 3]; + const ids = ["id1", "id2", "id3"]; + const b = new RequestBuilder("test"); + b.withFilter().authors(authors).kinds(kinds).ids(ids); + expect(b.buildRaw()).toMatchObject([{ ids, authors, kinds }]); + }); + test("authors and kinds, duplicates removed", () => { + const authors = ["a1", "a2"]; + const kinds = [0, 1, 2, 3]; + const ids = ["id1", "id2", "id3"]; + const b = new RequestBuilder("test"); + b.withFilter().ids(ids).authors(authors).kinds(kinds).ids(ids).authors(authors).kinds(kinds); + expect(b.buildRaw()).toMatchObject([{ ids, authors, kinds }]); + }); + test("search", () => { + const b = new RequestBuilder("test"); + b.withFilter().kinds([1]).search("test-search"); + expect(b.buildRaw()).toMatchObject([{ kinds: [1], search: "test-search" }]); + }); + test("timeline", () => { + const authors = ["a1", "a2"]; + const kinds = [0, 1, 2, 3]; + const until = 10; + const since = 5; + const b = new RequestBuilder("test"); + b.withFilter().kinds(kinds).authors(authors).since(since).until(until); + expect(b.buildRaw()).toMatchObject([{ kinds, authors, until, since }]); + }); + test("multi-filter timeline", () => { + const authors = ["a1", "a2"]; + const kinds = [0, 1, 2, 3]; + const until = 10; + const since = 5; + const b = new RequestBuilder("test"); + b.withFilter().kinds(kinds).authors(authors).since(since).until(until); + b.withFilter().kinds(kinds).authors(authors).since(since).until(until); + expect(b.buildRaw()).toMatchObject([ + { kinds, authors, until, since }, + { kinds, authors, until, since }, + ]); + }); + }); + + describe("diff basic", () => { + const rb = new RequestBuilder("test"); + const f0 = rb.withFilter(); + + const a = rb.buildRaw(); + f0.authors(["a"]); + expect(a).toEqual([{}]); + + const b = rb.buildDiff(DummyCache, a.flatMap(expandFilter)); + expect(b).toMatchObject([ + { + filters: [{ authors: ["a"] }], + }, + ]); + }); + + describe("build gossip simply", () => { + const rb = new RequestBuilder("test"); + rb.withFilter().authors(["a", "b"]).kinds([0]); + + const a = rb.build(DummyCache); + expect(a).toEqual([ + { + strategy: RequestStrategy.AuthorsRelays, + relay: "wss://a.com/", + filters: [ + { + kinds: [0], + authors: ["a"], + }, + ], + }, + { + strategy: RequestStrategy.AuthorsRelays, + relay: "wss://b.com/", + filters: [ + { + kinds: [0], + authors: ["b"], + }, + ], + }, + ]); + }); + + describe("build gossip merged similar filters", () => { + const rb = new RequestBuilder("test"); + rb.withFilter().authors(["a", "b"]).kinds([0]); + rb.withFilter().authors(["a", "b"]).kinds([10002]); + rb.withFilter().authors(["a"]).limit(10).kinds([4]); + + const a = rb.build(DummyCache); + expect(a).toEqual([ + { + strategy: RequestStrategy.AuthorsRelays, + relay: "wss://a.com/", + filters: [ + { + kinds: [0, 10002], + authors: ["a"], + }, + { + kinds: [4], + authors: ["a"], + limit: 10, + }, + ], + }, + { + strategy: RequestStrategy.AuthorsRelays, + relay: "wss://b.com/", + filters: [ + { + kinds: [0, 10002], + authors: ["b"], + }, + ], + }, + ]); + }); +}); diff --git a/packages/system/tests/RequestExpander.test.ts b/packages/system/tests/RequestExpander.test.ts new file mode 100644 index 00000000..d87faf7b --- /dev/null +++ b/packages/system/tests/RequestExpander.test.ts @@ -0,0 +1,34 @@ +import { expandFilter } from "../src/RequestExpander"; + +describe("RequestExpander", () => { + test("expand filter", () => { + const a = { + authors: ["a", "b", "c"], + kinds: [1, 2, 3], + ids: ["x", "y"], + "#p": ["a"], + since: 99, + limit: 10, + }; + expect(expandFilter(a)).toEqual([ + { authors: "a", kinds: 1, ids: "x", "#p": "a", since: 99, limit: 10 }, + { authors: "a", kinds: 1, ids: "y", "#p": "a", since: 99, limit: 10 }, + { authors: "a", kinds: 2, ids: "x", "#p": "a", since: 99, limit: 10 }, + { authors: "a", kinds: 2, ids: "y", "#p": "a", since: 99, limit: 10 }, + { authors: "a", kinds: 3, ids: "x", "#p": "a", since: 99, limit: 10 }, + { authors: "a", kinds: 3, ids: "y", "#p": "a", since: 99, limit: 10 }, + { authors: "b", kinds: 1, ids: "x", "#p": "a", since: 99, limit: 10 }, + { authors: "b", kinds: 1, ids: "y", "#p": "a", since: 99, limit: 10 }, + { authors: "b", kinds: 2, ids: "x", "#p": "a", since: 99, limit: 10 }, + { authors: "b", kinds: 2, ids: "y", "#p": "a", since: 99, limit: 10 }, + { authors: "b", kinds: 3, ids: "x", "#p": "a", since: 99, limit: 10 }, + { authors: "b", kinds: 3, ids: "y", "#p": "a", since: 99, limit: 10 }, + { authors: "c", kinds: 1, ids: "x", "#p": "a", since: 99, limit: 10 }, + { authors: "c", kinds: 1, ids: "y", "#p": "a", since: 99, limit: 10 }, + { authors: "c", kinds: 2, ids: "x", "#p": "a", since: 99, limit: 10 }, + { authors: "c", kinds: 2, ids: "y", "#p": "a", since: 99, limit: 10 }, + { authors: "c", kinds: 3, ids: "x", "#p": "a", since: 99, limit: 10 }, + { authors: "c", kinds: 3, ids: "y", "#p": "a", since: 99, limit: 10 }, + ]); + }); +}); diff --git a/packages/system/tests/RequestMatcher.test.ts b/packages/system/tests/RequestMatcher.test.ts new file mode 100644 index 00000000..0b7a4b39 --- /dev/null +++ b/packages/system/tests/RequestMatcher.test.ts @@ -0,0 +1,23 @@ +import { eventMatchesFilter } from "../src/RequestMatcher"; + +describe("RequestMatcher", () => { + it("should match simple filter", () => { + const ev = { + id: "test", + kind: 1, + pubkey: "pubkey", + created_at: 99, + tags: [], + content: "test", + sig: "", + }; + const filter = { + ids: ["test"], + authors: ["pubkey", "other"], + kinds: [1, 2, 3], + since: 1, + before: 100, + }; + expect(eventMatchesFilter(ev, filter)).toBe(true); + }); +}); diff --git a/packages/system/tests/RequestMerger.test.ts b/packages/system/tests/RequestMerger.test.ts new file mode 100644 index 00000000..f9ac464d --- /dev/null +++ b/packages/system/tests/RequestMerger.test.ts @@ -0,0 +1,180 @@ +import { ReqFilter } from "../src"; +import { canMergeFilters, filterIncludes, flatMerge, mergeSimilar, simpleMerge } from "../src/RequestMerger"; +import { FlatReqFilter, expandFilter } from "../src/RequestExpander"; + +describe("RequestMerger", () => { + it("should simple merge authors", () => { + const a = { + authors: ["a"], + } as ReqFilter; + const b = { + authors: ["b"], + } as ReqFilter; + + const merged = mergeSimilar([a, b]); + expect(merged).toEqual([ + { + authors: ["a", "b"], + }, + ]); + }); + + it("should append non-mergable filters", () => { + const a = { + authors: ["a"], + } as ReqFilter; + const b = { + authors: ["b"], + } as ReqFilter; + const c = { + limit: 5, + authors: ["a"], + }; + + const merged = mergeSimilar([a, b, c]); + expect(merged).toEqual([ + { + authors: ["a", "b"], + }, + { + limit: 5, + authors: ["a"], + }, + ]); + }); + + it("filterIncludes", () => { + const bigger = { + authors: ["a", "b", "c"], + since: 99, + } as ReqFilter; + const smaller = { + authors: ["c"], + since: 100, + } as ReqFilter; + expect(filterIncludes(bigger, smaller)).toBe(true); + }); + + it("simpleMerge", () => { + const a = { + authors: ["a", "b", "c"], + since: 99, + } as ReqFilter; + const b = { + authors: ["c", "d", "e"], + since: 100, + } as ReqFilter; + expect(simpleMerge([a, b])).toEqual({ + authors: ["a", "b", "c", "d", "e"], + since: 100, + }); + }); +}); + +describe("flatMerge", () => { + it("should flat merge simple", () => { + const input = [ + { ids: 0, authors: "a" }, + { ids: 0, authors: "b" }, + { kinds: 1 }, + { kinds: 2 }, + { kinds: 2 }, + { ids: 0, authors: "c" }, + { authors: "c", kinds: 1 }, + { authors: "c", limit: 100 }, + { ids: 1, authors: "c" }, + ] as Array; + const output = [ + { ids: [0], authors: ["a", "b", "c"] }, + { kinds: [1, 2] }, + { authors: ["c"], kinds: [1] }, + { authors: ["c"], limit: 100 }, + { ids: [1], authors: ["c"] }, + ] as Array; + + expect(flatMerge(input)).toEqual(output); + }); + + it("should expand and flat merge complex same", () => { + const input = [ + { kinds: [1, 6969, 6], authors: ["kieran", "snort", "c", "d", "e"], since: 1, until: 100 }, + { kinds: [4], authors: ["kieran"] }, + { kinds: [4], "#p": ["kieran"] }, + { kinds: [1000], authors: ["snort"], "#p": ["kieran"] }, + ] as Array; + + const dut = flatMerge(input.flatMap(expandFilter).sort(() => (Math.random() > 0.5 ? 1 : -1))); + expect(dut.every(a => input.some(b => canMergeFilters(b, a) === false))).toEqual(true); + }); +}); + +describe('canMerge', () => { + it("should have 0 distance", () => { + const a = { + ids: "a", + }; + const b = { + ids: "a", + }; + expect(canMergeFilters(a, b)).toEqual(true); + }); + it("should have 1 distance", () => { + const a = { + ids: "a", + }; + const b = { + ids: "b", + }; + expect(canMergeFilters(a, b)).toEqual(true); + }); + it("should have 10 distance", () => { + const a = { + ids: "a", + }; + const b = { + ids: "a", + kinds: 1, + }; + expect(canMergeFilters(a, b)).toEqual(false); + }); + it("should have 11 distance", () => { + const a = { + ids: "a", + }; + const b = { + ids: "b", + kinds: 1, + }; + expect(canMergeFilters(a, b)).toEqual(false); + }); + it("should have 1 distance, arrays", () => { + const a = { + since: 1, + until: 100, + kinds: [1], + authors: ["kieran", "snort", "c", "d", "e"], + }; + const b = { + since: 1, + until: 100, + kinds: [6969], + authors: ["kieran", "snort", "c", "d", "e"], + }; + expect(canMergeFilters(a, b)).toEqual(true); + }); + it("should have 1 distance, array change extra", () => { + const a = { + since: 1, + until: 100, + kinds: [1], + authors: ["f", "kieran", "snort", "c", "d"], + }; + const b = { + since: 1, + until: 100, + kinds: [1], + authors: ["kieran", "snort", "c", "d", "e"], + }; + expect(canMergeFilters(a, b)).toEqual(true); + }); +}) diff --git a/packages/system/tests/RequestSplitter.test.ts b/packages/system/tests/RequestSplitter.test.ts new file mode 100644 index 00000000..9d3ac867 --- /dev/null +++ b/packages/system/tests/RequestSplitter.test.ts @@ -0,0 +1,88 @@ +import { ReqFilter } from "../src"; +import { describe, expect } from "@jest/globals"; +import { diffFilters } from "../src/RequestSplitter"; +import { expandFilter } from "../src/RequestExpander"; + +describe("RequestSplitter", () => { + test("single filter add value", () => { + const a: Array = [{ kinds: [0], authors: ["a"] }]; + const b: Array = [{ kinds: [0], authors: ["a", "b"] }]; + const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true); + expect(diff).toEqual({ + added: [{ kinds: [0], authors: ["b"] }], + removed: [], + changed: true, + }); + }); + test("single filter remove value", () => { + const a: Array = [{ kinds: [0], authors: ["a"] }]; + const b: Array = [{ kinds: [0], authors: ["b"] }]; + const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true); + expect(diff).toEqual({ + added: [{ kinds: [0], authors: ["b"] }], + removed: [{ kinds: [0], authors: ["a"] }], + changed: true, + }); + }); + test("single filter change critical key", () => { + const a: Array = [{ kinds: [0], authors: ["a"], since: 100 }]; + const b: Array = [{ kinds: [0], authors: ["a", "b"], since: 101 }]; + const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true); + expect(diff).toEqual({ + added: [{ kinds: [0], authors: ["a", "b"], since: 101 }], + removed: [{ kinds: [0], authors: ["a"], since: 100 }], + changed: true, + }); + }); + test("multiple filter add value", () => { + const a: Array = [ + { kinds: [0], authors: ["a"] }, + { kinds: [69], authors: ["a"] }, + ]; + const b: Array = [ + { kinds: [0], authors: ["a", "b"] }, + { kinds: [69], authors: ["a", "c"] }, + ]; + const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true); + expect(diff).toEqual({ + added: [ + { kinds: [0], authors: ["b"] }, + { kinds: [69], authors: ["c"] }, + ], + removed: [], + changed: true, + }); + }); + test("multiple filter remove value", () => { + const a: Array = [ + { kinds: [0], authors: ["a"] }, + { kinds: [69], authors: ["a"] }, + ]; + const b: Array = [ + { kinds: [0], authors: ["b"] }, + { kinds: [69], authors: ["c"] }, + ]; + const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true); + expect(diff).toEqual({ + added: [ + { kinds: [0], authors: ["b"] }, + { kinds: [69], authors: ["c"] }, + ], + removed: [{ kinds: [0, 69], authors: ["a"] }], + changed: true, + }); + }); + test("add filter", () => { + const a: Array = [{ kinds: [0], authors: ["a"] }]; + const b: Array = [ + { kinds: [0], authors: ["a"] }, + { kinds: [69], authors: ["c"] }, + ]; + const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true); + expect(diff).toEqual({ + added: [{ kinds: [69], authors: ["c"] }], + removed: [], + changed: true, + }); + }); +}); diff --git a/packages/system/tests/Util.test.ts b/packages/system/tests/Util.test.ts new file mode 100644 index 00000000..9f1499b8 --- /dev/null +++ b/packages/system/tests/Util.test.ts @@ -0,0 +1,46 @@ +import { NostrPrefix } from "../src/Links"; +import { parseNostrLink, tryParseNostrLink } from "../src/NostrLink"; + +describe("tryParseNostrLink", () => { + it("is a valid nostr link", () => { + expect(parseNostrLink("nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg")).toMatchObject({ + id: "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e", + type: NostrPrefix.PublicKey, + }); + expect(parseNostrLink("web+nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg")).toMatchObject({ + id: "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e", + type: NostrPrefix.PublicKey, + }); + expect(parseNostrLink("nostr:note15449edq4qa5wzgqvh8td0q0dp6hwtes4pknsrm7eygeenhlj99xsq94wu9")).toMatchObject({ + id: "a56a5cb4150768e1200cb9d6d781ed0eaee5e6150da701efd9223399dff2294d", + type: NostrPrefix.Note, + }); + expect( + parseNostrLink( + "nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p" + ) + ).toMatchObject({ + id: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", + type: NostrPrefix.Profile, + relays: ["wss://r.x.com", "wss://djbas.sadkb.com"], + }); + expect(parseNostrLink("nostr:nevent1qqs226juks2sw68pyqxtn4khs8ksath9uc2smfcpalvjyvuemlezjngrd87dq")).toMatchObject({ + id: "a56a5cb4150768e1200cb9d6d781ed0eaee5e6150da701efd9223399dff2294d", + type: NostrPrefix.Event, + }); + expect( + parseNostrLink( + "nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu" + ) + ).toMatchObject({ + id: "ipsum", + type: NostrPrefix.Address, + relays: ["wss://relay.nostr.org"], + author: "a695f6b60119d9521934a691347d9f78e8770b56da16bb255ee286ddf9fda919", + kind: 30023, + }); + }); + test.each(["nostr:npub", "web+nostr:npub", "nostr:nevent1xxx"])("should return false for invalid nostr links", lb => { + expect(tryParseNostrLink(lb)).toBeUndefined(); + }); +}); diff --git a/packages/system/tests/setupTests.ts b/packages/system/tests/setupTests.ts new file mode 100644 index 00000000..4930f245 --- /dev/null +++ b/packages/system/tests/setupTests.ts @@ -0,0 +1,3 @@ +import { TextEncoder, TextDecoder } from "util"; + +Object.assign(global, { TextDecoder, TextEncoder }); diff --git a/packages/system/tsconfig.json b/packages/system/tsconfig.json new file mode 100644 index 00000000..b57c5cd3 --- /dev/null +++ b/packages/system/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "target": "ES2020", + "moduleResolution": "node", + "esModuleInterop": true, + "noImplicitOverride": true, + "module": "CommonJS", + "strict": true, + "declaration": true, + "declarationMap": true, + "inlineSourceMap": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src/**/*.ts"], + "files": ["src/index.ts"] +} diff --git a/packages/system/worker.ts b/packages/system/worker.ts new file mode 100644 index 00000000..88315188 --- /dev/null +++ b/packages/system/worker.ts @@ -0,0 +1,21 @@ +/// +import { UsersRelaysCache } from "../Cache/UserRelayCache"; +import { NostrSystem } from "."; +declare const self: SharedWorkerGlobalScope; + +const RelayCache = new UsersRelaysCache(); +const System = new NostrSystem({ + get: pk => RelayCache.getFromCache(pk)?.relays, +}); + +self.onconnect = e => { + const port = e.ports[0]; + + port.addEventListener("message", async e1 => { + console.debug(e1); + const [cmd, ...others] = e1.data; + switch (cmd) { + } + }); + port.start(); +}; diff --git a/yarn.lock b/yarn.lock index 1b09ee9f..100b9383 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,7 +36,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.7.tgz#61caffb60776e49a57ba61a88f02bedd8714f6bc" integrity sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA== -"@babel/core@^7.10.4": +"@babel/core@^7.10.4", "@babel/core@^7.11.6", "@babel/core@^7.12.3": version "7.21.8" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.8.tgz#2a8c7f0f53d60100ba4c32470ba0281c92aa9aa4" integrity sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ== @@ -88,7 +88,7 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/generator@^7.21.5": +"@babel/generator@^7.21.5", "@babel/generator@^7.7.2": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.5.tgz#c0c0e5449504c7b7de8236d99338c3e2a340745f" integrity sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w== @@ -367,7 +367,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8": +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8": version "7.21.8" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== @@ -529,7 +529,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.12.13": +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3": version "7.12.13" resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== @@ -564,7 +571,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.19.0" -"@babel/plugin-syntax-import-meta@^7.10.4": +"@babel/plugin-syntax-import-meta@^7.10.4", "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== @@ -578,14 +585,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@7", "@babel/plugin-syntax-jsx@^7.21.4": +"@babel/plugin-syntax-jsx@7", "@babel/plugin-syntax-jsx@^7.21.4", "@babel/plugin-syntax-jsx@^7.7.2": version "7.21.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz" integrity sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ== dependencies: "@babel/helper-plugin-utils" "^7.20.2" -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== @@ -599,7 +606,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.10.4": +"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== @@ -634,13 +641,20 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-top-level-await@^7.14.5": +"@babel/plugin-syntax-top-level-await@^7.14.5", "@babel/plugin-syntax-top-level-await@^7.8.3": version "7.14.5" resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz#2751948e9b7c6d771a8efa59340c15d4a2891ff8" + integrity sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-transform-arrow-functions@^7.20.7": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz" @@ -1176,7 +1190,7 @@ dependencies: regenerator-runtime "^0.13.11" -"@babel/template@^7.18.10", "@babel/template@^7.20.7": +"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3": version "7.20.7" resolved "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz" integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== @@ -1185,7 +1199,7 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" -"@babel/traverse@7", "@babel/traverse@^7.21.5": +"@babel/traverse@7", "@babel/traverse@^7.21.5", "@babel/traverse@^7.7.2": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.5.tgz#ad22361d352a5154b498299d523cf72998a4b133" integrity sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw== @@ -1217,7 +1231,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.12.11", "@babel/types@^7.21.5", "@babel/types@^7.3.0": +"@babel/types@^7.0.0", "@babel/types@^7.12.11", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6" integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q== @@ -1235,6 +1249,11 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + "@cashu/cashu-ts@^0.6.1": version "0.6.1" resolved "https://registry.npmjs.org/@cashu/cashu-ts/-/cashu-ts-0.6.1.tgz" @@ -1446,6 +1465,78 @@ resolved "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz" integrity sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ== +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.5.0.tgz#593a6c5c0d3f75689835f1b3b4688c4f8544cb57" + integrity sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.5.0" + jest-util "^29.5.0" + slash "^3.0.0" + +"@jest/core@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.5.0.tgz#76674b96904484e8214614d17261cc491e5f1f03" + integrity sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ== + dependencies: + "@jest/console" "^29.5.0" + "@jest/reporters" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.5.0" + jest-config "^29.5.0" + jest-haste-map "^29.5.0" + jest-message-util "^29.5.0" + jest-regex-util "^29.4.3" + jest-resolve "^29.5.0" + jest-resolve-dependencies "^29.5.0" + jest-runner "^29.5.0" + jest-runtime "^29.5.0" + jest-snapshot "^29.5.0" + jest-util "^29.5.0" + jest-validate "^29.5.0" + jest-watcher "^29.5.0" + micromatch "^4.0.4" + pretty-format "^29.5.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.5.0.tgz#9152d56317c1fdb1af389c46640ba74ef0bb4c65" + integrity sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ== + dependencies: + "@jest/fake-timers" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + jest-mock "^29.5.0" + "@jest/expect-utils@^29.5.0": version "29.5.0" resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz" @@ -1453,6 +1544,66 @@ dependencies: jest-get-type "^29.4.3" +"@jest/expect@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.5.0.tgz#80952f5316b23c483fbca4363ce822af79c38fba" + integrity sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g== + dependencies: + expect "^29.5.0" + jest-snapshot "^29.5.0" + +"@jest/fake-timers@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.5.0.tgz#d4d09ec3286b3d90c60bdcd66ed28d35f1b4dc2c" + integrity sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg== + dependencies: + "@jest/types" "^29.5.0" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.5.0" + jest-mock "^29.5.0" + jest-util "^29.5.0" + +"@jest/globals@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.5.0.tgz#6166c0bfc374c58268677539d0c181f9c1833298" + integrity sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/expect" "^29.5.0" + "@jest/types" "^29.5.0" + jest-mock "^29.5.0" + +"@jest/reporters@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.5.0.tgz#985dfd91290cd78ddae4914ba7921bcbabe8ac9b" + integrity sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@jridgewell/trace-mapping" "^0.3.15" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.5.0" + jest-util "^29.5.0" + jest-worker "^29.5.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + "@jest/schemas@^29.4.3": version "29.4.3" resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz" @@ -1460,6 +1611,56 @@ dependencies: "@sinclair/typebox" "^0.25.16" +"@jest/source-map@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.4.3.tgz#ff8d05cbfff875d4a791ab679b4333df47951d20" + integrity sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.15" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.5.0.tgz#7c856a6ca84f45cc36926a4e9c6b57f1973f1408" + integrity sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ== + dependencies: + "@jest/console" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz#34d7d82d3081abd523dbddc038a3ddcb9f6d3cc4" + integrity sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ== + dependencies: + "@jest/test-result" "^29.5.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.5.0" + slash "^3.0.0" + +"@jest/transform@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.5.0.tgz#cf9c872d0965f0cbd32f1458aa44a2b1988b00f9" + integrity sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.5.0" + "@jridgewell/trace-mapping" "^0.3.15" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.5.0" + jest-regex-util "^29.4.3" + jest-util "^29.5.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + "@jest/types@^29.5.0": version "29.5.0" resolved "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz" @@ -1530,6 +1731,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz" @@ -1939,6 +2148,20 @@ resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz" integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== +"@sinonjs/commons@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" + integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.2.0.tgz#b3e322a34c5f26e3184e7f6115695f299c1b1194" + integrity sha512-OPwQlEdg40HAj5KNF8WW6q2KG4Z+cBCZb3m4ninfTZKaBmbIJodviQsDBoYMPHkOyJJMHnOJo5j2+LKDOhOACg== + dependencies: + "@sinonjs/commons" "^3.0.0" + "@surma/rollup-plugin-off-main-thread@^2.2.3": version "2.2.3" resolved "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz" @@ -2052,7 +2275,7 @@ resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== -"@types/babel__core@*", "@types/babel__core@^7.1.7": +"@types/babel__core@*", "@types/babel__core@^7.1.14", "@types/babel__core@^7.1.7": version "7.20.0" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" integrity sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ== @@ -2085,7 +2308,7 @@ "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" -"@types/babel__traverse@*", "@types/babel__traverse@^7.1.7": +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6", "@types/babel__traverse@^7.1.7": version "7.18.5" resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.5.tgz#c107216842905afafd3b6e774f6f935da6f5db80" integrity sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q== @@ -2134,6 +2357,13 @@ dependencies: "@types/node" "*" +"@types/debug@^4.1.8": + version "4.1.8" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317" + integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ== + dependencies: + "@types/ms" "*" + "@types/eslint-scope@^3.7.3": version "3.7.4" resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz" @@ -2206,6 +2436,13 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/graceful-fs@^4.1.3": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" + integrity sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw== + dependencies: + "@types/node" "*" + "@types/hoist-non-react-statics@^3.3.1": version "3.3.1" resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" @@ -2226,7 +2463,7 @@ dependencies: "@types/node" "*" -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz" integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== @@ -2245,14 +2482,23 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.2.5": - version "29.5.0" - resolved "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz" - integrity sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg== +"@types/jest@^29.5.1": + version "29.5.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.1.tgz#83c818aa9a87da27d6da85d3378e5a34d2f31a47" + integrity sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ== dependencies: expect "^29.0.0" pretty-format "^29.0.0" +"@types/jsdom@^20.0.0": + version "20.0.1" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.1.tgz#07c14bc19bd2f918c1929541cdaacae894744808" + integrity sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ== + dependencies: + "@types/node" "*" + "@types/tough-cookie" "*" + parse5 "^7.0.0" + "@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" @@ -2290,6 +2536,11 @@ resolved "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz" integrity sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q== +"@types/ms@*": + version "0.7.31" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + "@types/node@*", "@types/node@^18.11.18": version "18.15.11" resolved "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz" @@ -2331,6 +2582,11 @@ resolved "https://registry.yarnpkg.com/@types/picomatch/-/picomatch-2.3.0.tgz#75db5e75a713c5a83d5b76780c3da84a82806003" integrity sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g== +"@types/prettier@^2.1.5": + version "2.7.2" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0" + integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg== + "@types/prop-types@*": version "15.7.5" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" @@ -2418,6 +2674,11 @@ resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/tough-cookie@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" + integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== + "@types/trusted-types@^2.0.2": version "2.0.3" resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz" @@ -2883,6 +3144,11 @@ resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +abab@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + abbrev@1: version "1.1.1" resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" @@ -2896,6 +3162,14 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" +acorn-globals@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3" + integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== + dependencies: + acorn "^8.1.0" + acorn-walk "^8.0.2" + acorn-import-assertions@^1.7.6: version "1.8.0" resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz" @@ -2906,12 +3180,12 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^8.0.0, acorn-walk@^8.1.1: +acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: +acorn@^8.0.4, acorn@^8.1.0, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.1: version "8.8.2" resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== @@ -3030,7 +3304,7 @@ ansi-styles@^6.0.0: resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -anymatch@~3.1.2: +anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -3158,6 +3432,19 @@ axios@^1.2.1: form-data "^4.0.0" proxy-from-env "^1.1.0" +babel-jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.5.0.tgz#3fe3ddb109198e78b1c88f9ebdecd5e4fc2f50a5" + integrity sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q== + dependencies: + "@jest/transform" "^29.5.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.5.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + babel-loader@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c" @@ -3183,6 +3470,27 @@ babel-plugin-formatjs@^10.5.1: "@types/babel__traverse" "^7.1.7" tslib "^2.4.0" +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz#a97db437936f441ec196990c9738d4b88538618a" + integrity sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + babel-plugin-polyfill-corejs2@^0.3.3: version "0.3.3" resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz" @@ -3207,6 +3515,32 @@ babel-plugin-polyfill-regenerator@^0.4.1: dependencies: "@babel/helper-define-polyfill-provider" "^0.3.3" +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz#57bc8cc88097af7ff6a5ab59d1cd29d52a5916e2" + integrity sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg== + dependencies: + babel-plugin-jest-hoist "^29.5.0" + babel-preset-current-node-syntax "^1.0.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" @@ -3348,6 +3682,20 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^4 node-releases "^2.0.8" update-browserslist-db "^1.0.10" +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + buffer-from@^1.0.0, buffer-from@^1.1.0: version "1.1.2" resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" @@ -3465,7 +3813,12 @@ camel-case@^4.1.2: pascal-case "^3.1.2" tslib "^2.0.3" -camelcase@^6.0.0: +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.0.0, camelcase@^6.2.0: version "6.3.0" resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -3525,6 +3878,11 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1: ansi-styles "^4.1.0" supports-color "^7.1.0" +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + chardet@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" @@ -3565,6 +3923,11 @@ ci-info@^3.2.0: resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz" integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== +cjs-module-lexer@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" + integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + clean-css@^5.2.2: version "5.3.2" resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz" @@ -3626,6 +3989,15 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz" @@ -3671,6 +4043,16 @@ cmd-shim@^5.0.0: dependencies: mkdirp-infer-owner "^2.0.0" +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" @@ -3814,11 +4196,16 @@ content-type@~1.0.4: resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -convert-source-map@^1.7.0: +convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" @@ -4011,6 +4398,23 @@ csso@^5.0.5: dependencies: css-tree "~2.2.0" +cssom@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" + integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + csstype@^3.0.2: version "3.1.2" resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz" @@ -4033,6 +4437,15 @@ dargs@^7.0.0: resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz" integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== +data-urls@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" + integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== + dependencies: + abab "^2.0.6" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + dateformat@^4.5.0: version "4.6.3" resolved "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz" @@ -4047,7 +4460,7 @@ debug@2.6.9: debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" @@ -4062,6 +4475,16 @@ decamelize@^4.0.0: resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== +decimal.js@^10.4.2: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== + deep-eql@^4.1.2: version "4.1.3" resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz" @@ -4074,7 +4497,7 @@ deep-extend@^0.6.0: resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deep-is@^0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== @@ -4141,6 +4564,11 @@ destroy@1.2.0: resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + detect-node@^2.0.4: version "2.1.0" resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" @@ -4250,6 +4678,13 @@ domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== +domexception@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" + integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== + dependencies: + webidl-conversions "^7.0.0" + domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: version "4.3.1" resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz" @@ -4317,6 +4752,11 @@ electron-to-chromium@^1.4.284: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.351.tgz" integrity sha512-W35n4jAsyj6OZGxeWe+gA6+2Md4jDO19fzfsRKEt3DBwIdlVTT8O9Uv8ojgUAoQeXASdgG9zMU+8n8Xg/W6dRQ== +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + emoji-regex@10.2.1, emoji-regex@^10.2.1: version "10.2.1" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.2.1.tgz#a41c330d957191efd3d9dfe6e1e8e1e9ab048b3f" @@ -4370,7 +4810,7 @@ entities@^2.0.0: resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -entities@^4.2.0: +entities@^4.2.0, entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -4495,6 +4935,18 @@ escape-string-regexp@^2.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + eslint-plugin-formatjs@^4.10.1: version "4.10.1" resolved "https://registry.yarnpkg.com/eslint-plugin-formatjs/-/eslint-plugin-formatjs-4.10.1.tgz#c67184ac54188dcad84d6541e6b5467248ab6550" @@ -4613,7 +5065,7 @@ espree@^9.5.2: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" -esprima@^4.0.0: +esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -4697,7 +5149,12 @@ execa@^7.0.0: signal-exit "^3.0.7" strip-final-newline "^3.0.0" -expect@*, expect@^29.0.0: +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@*, expect@^29.0.0, expect@^29.5.0: version "29.5.0" resolved "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz" integrity sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg== @@ -4770,12 +5227,12 @@ fast-glob@^3.2.11, fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@^2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== @@ -4799,6 +5256,13 @@ faye-websocket@^0.11.3: dependencies: websocket-driver ">=0.5.1" +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + figures@^3.0.0: version "3.2.0" resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" @@ -4956,7 +5420,7 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2: +fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -5039,6 +5503,11 @@ get-own-enumerable-property-symbols@^3.0.0: resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" @@ -5262,11 +5731,23 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + html-entities@^2.3.2: version "2.3.3" resolved "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz" integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA== +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + html-minifier-terser@^6.0.2: version "6.1.0" resolved "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" @@ -5375,7 +5856,7 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" -https-proxy-agent@^5.0.0: +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -5412,7 +5893,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: +iconv-lite@0.6.3, iconv-lite@^0.6.2: version "0.6.3" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -5640,6 +6121,11 @@ is-fullwidth-code-point@^4.0.0: resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz" integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" @@ -5711,6 +6197,11 @@ is-plain-object@^5.0.0: resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + is-regex@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" @@ -5827,6 +6318,48 @@ isomorphic-ws@^5.0.0: resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz" integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.5" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.5.tgz#cc9a6ab25cb25659810e4785ed9d9fb742578bae" + integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + jake@^10.8.5: version "10.8.5" resolved "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz" @@ -5837,6 +6370,86 @@ jake@^10.8.5: filelist "^1.0.1" minimatch "^3.0.4" +jest-changed-files@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" + integrity sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag== + dependencies: + execa "^5.0.0" + p-limit "^3.1.0" + +jest-circus@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.5.0.tgz#b5926989449e75bff0d59944bae083c9d7fb7317" + integrity sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/expect" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + is-generator-fn "^2.0.0" + jest-each "^29.5.0" + jest-matcher-utils "^29.5.0" + jest-message-util "^29.5.0" + jest-runtime "^29.5.0" + jest-snapshot "^29.5.0" + jest-util "^29.5.0" + p-limit "^3.1.0" + pretty-format "^29.5.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.5.0.tgz#b34c20a6d35968f3ee47a7437ff8e53e086b4a67" + integrity sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw== + dependencies: + "@jest/core" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/types" "^29.5.0" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^29.5.0" + jest-util "^29.5.0" + jest-validate "^29.5.0" + prompts "^2.0.1" + yargs "^17.3.1" + +jest-config@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.5.0.tgz#3cc972faec8c8aaea9ae158c694541b79f3748da" + integrity sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.5.0" + "@jest/types" "^29.5.0" + babel-jest "^29.5.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.5.0" + jest-environment-node "^29.5.0" + jest-get-type "^29.4.3" + jest-regex-util "^29.4.3" + jest-resolve "^29.5.0" + jest-runner "^29.5.0" + jest-util "^29.5.0" + jest-validate "^29.5.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.5.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + jest-diff@^29.5.0: version "29.5.0" resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz" @@ -5847,11 +6460,82 @@ jest-diff@^29.5.0: jest-get-type "^29.4.3" pretty-format "^29.5.0" +jest-docblock@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8" + integrity sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.5.0.tgz#fc6e7014f83eac68e22b7195598de8554c2e5c06" + integrity sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA== + dependencies: + "@jest/types" "^29.5.0" + chalk "^4.0.0" + jest-get-type "^29.4.3" + jest-util "^29.5.0" + pretty-format "^29.5.0" + +jest-environment-jsdom@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.5.0.tgz#cfe86ebaf1453f3297b5ff3470fbe94739c960cb" + integrity sha512-/KG8yEK4aN8ak56yFVdqFDzKNHgF4BAymCx2LbPNPsUshUlfAl0eX402Xm1pt+eoG9SLZEUVifqXtX8SK74KCw== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/fake-timers" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/jsdom" "^20.0.0" + "@types/node" "*" + jest-mock "^29.5.0" + jest-util "^29.5.0" + jsdom "^20.0.0" + +jest-environment-node@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.5.0.tgz#f17219d0f0cc0e68e0727c58b792c040e332c967" + integrity sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/fake-timers" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + jest-mock "^29.5.0" + jest-util "^29.5.0" + jest-get-type@^29.4.3: version "29.4.3" resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz" integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== +jest-haste-map@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.5.0.tgz#69bd67dc9012d6e2723f20a945099e972b2e94de" + integrity sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA== + dependencies: + "@jest/types" "^29.5.0" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.4.3" + jest-util "^29.5.0" + jest-worker "^29.5.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz#cf4bdea9615c72bac4a3a7ba7e7930f9c0610c8c" + integrity sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow== + dependencies: + jest-get-type "^29.4.3" + pretty-format "^29.5.0" + jest-matcher-utils@^29.5.0: version "29.5.0" resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz" @@ -5877,7 +6561,133 @@ jest-message-util@^29.5.0: slash "^3.0.0" stack-utils "^2.0.3" -jest-util@^29.5.0: +jest-mock@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.5.0.tgz#26e2172bcc71d8b0195081ff1f146ac7e1518aed" + integrity sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + jest-util "^29.5.0" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8" + integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg== + +jest-resolve-dependencies@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz#f0ea29955996f49788bf70996052aa98e7befee4" + integrity sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg== + dependencies: + jest-regex-util "^29.4.3" + jest-snapshot "^29.5.0" + +jest-resolve@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.5.0.tgz#b053cc95ad1d5f6327f0ac8aae9f98795475ecdc" + integrity sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.5.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.5.0" + jest-validate "^29.5.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.5.0.tgz#6a57c282eb0ef749778d444c1d758c6a7693b6f8" + integrity sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ== + dependencies: + "@jest/console" "^29.5.0" + "@jest/environment" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.4.3" + jest-environment-node "^29.5.0" + jest-haste-map "^29.5.0" + jest-leak-detector "^29.5.0" + jest-message-util "^29.5.0" + jest-resolve "^29.5.0" + jest-runtime "^29.5.0" + jest-util "^29.5.0" + jest-watcher "^29.5.0" + jest-worker "^29.5.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.5.0.tgz#c83f943ee0c1da7eb91fa181b0811ebd59b03420" + integrity sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/fake-timers" "^29.5.0" + "@jest/globals" "^29.5.0" + "@jest/source-map" "^29.4.3" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.5.0" + jest-message-util "^29.5.0" + jest-mock "^29.5.0" + jest-regex-util "^29.4.3" + jest-resolve "^29.5.0" + jest-snapshot "^29.5.0" + jest-util "^29.5.0" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.5.0.tgz#c9c1ce0331e5b63cd444e2f95a55a73b84b1e8ce" + integrity sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/babel__traverse" "^7.0.6" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.5.0" + graceful-fs "^4.2.9" + jest-diff "^29.5.0" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.5.0" + jest-message-util "^29.5.0" + jest-util "^29.5.0" + natural-compare "^1.4.0" + pretty-format "^29.5.0" + semver "^7.3.5" + +jest-util@^29.0.0, jest-util@^29.5.0: version "29.5.0" resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz" integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ== @@ -5889,6 +6699,32 @@ jest-util@^29.5.0: graceful-fs "^4.2.9" picomatch "^2.2.3" +jest-validate@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.5.0.tgz#8e5a8f36178d40e47138dc00866a5f3bd9916ffc" + integrity sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ== + dependencies: + "@jest/types" "^29.5.0" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.4.3" + leven "^3.1.0" + pretty-format "^29.5.0" + +jest-watcher@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.5.0.tgz#cf7f0f949828ba65ddbbb45c743a382a4d911363" + integrity sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA== + dependencies: + "@jest/test-result" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.5.0" + string-length "^4.0.1" + jest-worker@^26.2.1: version "26.6.2" resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz" @@ -5917,6 +6753,16 @@ jest-worker@^29.4.3, jest-worker@^29.5.0: merge-stream "^2.0.0" supports-color "^8.0.0" +jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.5.0.tgz#f75157622f5ce7ad53028f2f8888ab53e1f1f24e" + integrity sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ== + dependencies: + "@jest/core" "^29.5.0" + "@jest/types" "^29.5.0" + import-local "^3.0.2" + jest-cli "^29.5.0" + js-sdsl@^4.1.4: version "4.4.0" resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz" @@ -5934,7 +6780,7 @@ js-yaml@4.1.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -js-yaml@^3.13.0: +js-yaml@^3.13.0, js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -5942,6 +6788,38 @@ js-yaml@^3.13.0: argparse "^1.0.7" esprima "^4.0.0" +jsdom@^20.0.0: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db" + integrity sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ== + dependencies: + abab "^2.0.6" + acorn "^8.8.1" + acorn-globals "^7.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.2" + decimal.js "^10.4.2" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.2" + parse5 "^7.1.1" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.2" + w3c-xmlserializer "^4.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + ws "^8.11.0" + xml-name-validator "^4.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" @@ -5996,7 +6874,7 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" -json5@^2.2.0, json5@^2.2.2: +json5@^2.2.0, json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -6040,6 +6918,11 @@ kind-of@^6.0.2: resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + launch-editor@^2.6.0: version "2.6.0" resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz" @@ -6061,6 +6944,14 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + light-bolt11-decoder@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-2.1.0.tgz" @@ -6152,7 +7043,7 @@ lodash.flow@^3.5.0: resolved "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz" integrity sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw== -lodash.memoize@^4.1.2: +lodash.memoize@4.x, lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== @@ -6249,14 +7140,14 @@ magic-string@^0.30.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" -make-dir@^3.0.2: +make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: semver "^6.0.0" -make-error@^1.1.1: +make-error@1.x, make-error@^1.1.1: version "1.3.6" resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -6305,6 +7196,13 @@ make-fetch-happen@^9.1.0: socks-proxy-agent "^6.0.0" ssri "^8.0.0" +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + match-sorter@4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/match-sorter/-/match-sorter-4.0.0.tgz" @@ -6561,7 +7459,7 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: mocha@^10.2.0: version "10.2.0" - resolved "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg== dependencies: ansi-colors "4.1.1" @@ -6701,6 +7599,11 @@ node-gyp@^8.2.0: tar "^6.1.2" which "^2.0.2" +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + node-releases@^2.0.8: version "2.0.10" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz" @@ -6834,6 +7737,11 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" +nwsapi@^2.2.2: + version "2.2.4" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.4.tgz#fd59d5e904e8e1f03c25a7d5a15cfa16c714a1e5" + integrity sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g== + object-assign@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" @@ -6911,6 +7819,18 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" @@ -6955,7 +7875,7 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.2: +p-limit@^3.0.2, p-limit@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== @@ -7068,7 +7988,7 @@ parse-conflict-json@^2.0.1: just-diff "^5.0.1" just-diff-apply "^5.2.0" -parse-json@^5.0.0: +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -7078,6 +7998,13 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse5@^7.0.0, parse5@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" @@ -7156,6 +8083,11 @@ pify@^4.0.1: resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== +pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" @@ -7441,6 +8373,11 @@ prelude-ls@^1.2.1: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + prettier@2.8.3: version "2.8.3" resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz" @@ -7506,6 +8443,14 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + prop-types@^15.7.2: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" @@ -7528,11 +8473,21 @@ proxy-from-env@^1.1.0: resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== -punycode@^2.1.0: +psl@^1.1.33: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +punycode@^2.1.0, punycode@^2.1.1: version "2.3.0" resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== +pure-rand@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306" + integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ== + qr-code-styling@^1.6.0-rc.1: version "1.6.0-rc.1" resolved "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.6.0-rc.1.tgz" @@ -7552,6 +8507,11 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" @@ -7897,6 +8857,11 @@ resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve.exports@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== + resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0: version "1.22.1" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz" @@ -8001,6 +8966,13 @@ safe-regex-test@^1.0.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + scheduler@^0.23.0: version "0.23.0" resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz" @@ -8063,18 +9035,18 @@ selfsigned@^2.1.1: resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: +semver@7.x, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: version "7.5.1" resolved "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz" integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw== dependencies: lru-cache "^6.0.0" +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + semver@^7.3.7, semver@^7.3.8: version "7.3.8" resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz" @@ -8216,6 +9188,11 @@ sirv@^1.0.7: mrmime "^1.0.0" totalist "^1.0.0" +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + sjcl@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/sjcl/-/sjcl-1.0.8.tgz#f2ec8d7dc1f0f21b069b8914a41a8f236b0e252a" @@ -8314,6 +9291,14 @@ source-map-js@^1.0.1, source-map-js@^1.0.2: resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-support@^0.5.6, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" @@ -8434,6 +9419,14 @@ string-argv@^0.3.1: resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" @@ -8557,6 +9550,11 @@ strip-bom@^3.0.0: resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + strip-comments@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz" @@ -8623,6 +9621,11 @@ svgo@^3.0.2: csso "^5.0.5" picocolors "^1.0.0" +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: version "2.2.1" resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" @@ -8697,6 +9700,15 @@ terser@^5.16.8: commander "^2.20.0" source-map-support "~0.5.20" +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" @@ -8729,6 +9741,11 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" @@ -8751,6 +9768,16 @@ totalist@^1.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== +tough-cookie@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" + integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + tr46@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz" @@ -8758,6 +9785,13 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" @@ -8768,6 +9802,20 @@ treeverse@^1.0.4: resolved "https://registry.npmjs.org/treeverse/-/treeverse-1.0.4.tgz" integrity sha512-whw60l7r+8ZU8Tu/Uc2yxtc4ZTZbR/PF3u1IPNKGQ6p8EICLb3Z2lAgoqw9bqYd8IkgnsaOcLzYHFckjqNsf0g== +ts-jest@^29.1.0: + version "29.1.0" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.0.tgz#4a9db4104a49b76d2b368ea775b6c9535c603891" + integrity sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "4.x" + make-error "1.x" + semver "7.x" + yargs-parser "^21.0.1" + ts-loader@^9.4.2: version "9.4.2" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.2.tgz#80a45eee92dd5170b900b3d00abcfa14949aeb78" @@ -8854,7 +9902,14 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@^4.0.0, type-detect@^4.0.5: +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5: version "4.0.8" resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== @@ -8998,6 +10053,11 @@ universal-user-agent@^6.0.0: resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz" integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + universalify@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" @@ -9033,6 +10093,14 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + use-composed-ref@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz" @@ -9090,6 +10158,15 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== +v8-to-istanbul@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265" + integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz" @@ -9133,11 +10210,25 @@ vinyl@^2.0.1: remove-trailing-separator "^1.0.1" replace-ext "^1.0.0" +w3c-xmlserializer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" + integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== + dependencies: + xml-name-validator "^4.0.0" + walk-up-path@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/walk-up-path/-/walk-up-path-1.0.0.tgz" integrity sha512-hwj/qMDUEjCU5h0xr90KGCf0tg0/LgJbmOWgrWKYlcJZM7XvquvUJZ0G/HMGr7F7OQMOUuPHWP9JpriinkAlkg== +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + watchpack@^2.4.0: version "2.4.0" resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz" @@ -9170,6 +10261,11 @@ webidl-conversions@^4.0.2: resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + webpack-bundle-analyzer@^4.8.0: version "4.8.0" resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.8.0.tgz#951b8aaf491f665d2ae325d8b84da229157b1d04" @@ -9347,6 +10443,26 @@ websocket-extensions@>=0.1.1: resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" @@ -9414,7 +10530,7 @@ wildcard@^2.0.0: resolved "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz" integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== -word-wrap@^1.2.3: +word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== @@ -9616,7 +10732,7 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -write-file-atomic@^4.0.0: +write-file-atomic@^4.0.0, write-file-atomic@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz" integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== @@ -9629,11 +10745,21 @@ ws@^7.3.1: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== -ws@^8.12.1, ws@^8.13.0: +ws@^8.11.0, ws@^8.12.1, ws@^8.13.0: version "8.13.0" resolved "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +xml-name-validator@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" + integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" @@ -9659,6 +10785,11 @@ yargs-parser@20.2.4, yargs-parser@^20.2.2: resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== +yargs-parser@^21.0.1, yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + yargs-unparser@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz" @@ -9682,6 +10813,19 @@ yargs@16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.3.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yeoman-environment@^3.9.1: version "3.16.2" resolved "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-3.16.2.tgz"