From fc11381ccdbdea32bfe1c9273d1365b0d8e04e72 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 15 Jun 2023 12:03:05 +0100 Subject: [PATCH] use @snort/system cache --- packages/app/package.json | 18 +- packages/app/src/Cache/DMCache.ts | 2 +- .../app/src/Cache/EventInteractionCache.ts | 2 +- packages/app/src/Cache/PaymentsCache.ts | 2 +- packages/app/src/Cache/index.ts | 9 +- packages/app/src/Db/index.ts | 25 +-- packages/app/src/Element/Bookmarks.tsx | 2 +- packages/app/src/Element/FollowButton.tsx | 5 +- packages/app/src/Element/FollowListBase.tsx | 3 +- packages/app/src/Element/Nip5Service.tsx | 3 +- packages/app/src/Element/Note.tsx | 7 +- packages/app/src/Element/NoteCreator.tsx | 11 +- packages/app/src/Element/NoteFooter.tsx | 13 +- packages/app/src/Element/Poll.tsx | 2 +- packages/app/src/Element/PubkeyList.tsx | 2 +- packages/app/src/Element/ReBroadcaster.tsx | 5 +- packages/app/src/Element/SendSats.tsx | 5 +- packages/app/src/Element/Textarea.tsx | 2 +- packages/app/src/Element/WriteDm.tsx | 3 +- packages/app/src/Element/Zap.tsx | 2 +- packages/app/src/ExternalStore.ts | 41 ---- packages/app/src/Feed/EventPublisher.ts | 3 +- packages/app/src/Feed/LoginFeed.ts | 2 +- packages/app/src/Feed/RelaysFeedFollows.tsx | 2 +- packages/app/src/Hooks/useImgProxy.ts | 6 +- packages/app/src/Hooks/useModeration.tsx | 3 +- packages/app/src/Hooks/useUserProfile.ts | 2 +- packages/app/src/Login/Functions.ts | 4 +- packages/app/src/Login/MultiAccountStore.ts | 3 +- packages/app/src/Notifications.ts | 2 +- packages/app/src/Pages/HashTagsPage.tsx | 3 +- packages/app/src/Pages/Layout.tsx | 8 - packages/app/src/Pages/ProfilePage.tsx | 2 +- packages/app/src/Pages/new/ProfileSetup.tsx | 5 +- packages/app/src/Pages/settings/Profile.tsx | 3 +- packages/app/src/Pages/settings/Relays.tsx | 6 +- .../src/Pages/settings/handle/LNAddress.tsx | 2 +- packages/app/src/SnortUtils/index.ts | 7 +- packages/app/src/Toaster.tsx | 5 +- packages/app/src/Wallet/WebLN.ts | 2 +- packages/app/src/Wallet/index.ts | 2 +- packages/app/src/ZapPoolController.ts | 4 +- packages/app/src/index.tsx | 13 +- packages/app/src/service-worker.ts | 8 +- packages/shared/package.json | 21 ++ packages/shared/src/const.ts | 8 + .../src/external-store.ts} | 6 +- .../FeedCache.ts => shared/src/feed-cache.ts} | 34 +-- packages/shared/src/index.ts | 5 + .../{app/src/LNURL.ts => shared/src/lnurl.ts} | 33 ++- packages/shared/src/utils.ts | 184 ++++++++++++++++ .../WorkQueue.ts => shared/src/work-queue.ts} | 0 packages/shared/tsconfig.json | 18 ++ packages/system/README.md | 66 ++++++ packages/system/examples/simple.ts | 52 +++++ packages/system/package.json | 3 +- packages/system/src/Connection.ts | 3 +- packages/system/src/Const.ts | 2 +- packages/system/src/EventBuilder.ts | 2 +- packages/system/src/EventExt.ts | 2 +- packages/system/src/EventPublisher.ts | 35 ++- packages/system/src/GossipModel.ts | 8 +- packages/system/src/Links.ts | 4 +- packages/system/src/NostrLink.ts | 2 +- packages/system/src/NostrSystem.ts | 24 +- packages/system/src/NoteCollection.ts | 3 +- packages/system/src/ProfileCache.ts | 11 +- packages/system/src/Query.ts | 4 +- packages/system/src/RequestBuilder.ts | 7 +- packages/system/src/RequestMerger.ts | 2 +- packages/system/src/SystemWorker.ts | 3 +- packages/system/src/Utils.ts | 205 +++--------------- packages/system/src/WorkQueue.ts | 30 --- .../Cache => system/src/cache}/UserCache.ts | 13 +- .../src/cache}/UserRelayCache.ts | 10 +- packages/system/src/cache/db.ts | 42 ++++ packages/system/src/cache/index.ts | 41 ++-- packages/system/src/index.ts | 6 +- yarn.lock | 53 +++-- 79 files changed, 679 insertions(+), 524 deletions(-) delete mode 100644 packages/app/src/ExternalStore.ts create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/const.ts rename packages/{system/src/ExternalStore.ts => shared/src/external-store.ts} (83%) rename packages/{app/src/Cache/FeedCache.ts => shared/src/feed-cache.ts} (87%) create mode 100644 packages/shared/src/index.ts rename packages/{app/src/LNURL.ts => shared/src/lnurl.ts} (90%) create mode 100644 packages/shared/src/utils.ts rename packages/{app/src/WorkQueue.ts => shared/src/work-queue.ts} (100%) create mode 100644 packages/shared/tsconfig.json create mode 100644 packages/system/README.md create mode 100644 packages/system/examples/simple.ts delete mode 100644 packages/system/src/WorkQueue.ts rename packages/{app/src/Cache => system/src/cache}/UserCache.ts (91%) rename packages/{app/src/Cache => system/src/cache}/UserRelayCache.ts (72%) create mode 100644 packages/system/src/cache/db.ts diff --git a/packages/app/package.json b/packages/app/package.json index 8bd40d54..0192922e 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -8,19 +8,14 @@ "@lightninglabs/lnc-web": "^0.2.3-alpha", "@noble/curves": "^1.0.0", "@noble/hashes": "^1.2.0", - "@protobufjs/base64": "^1.1.2", "@reduxjs/toolkit": "^1.9.1", "@scure/bip32": "^1.3.0", "@scure/bip39": "^1.1.1", "@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", + "dexie": "^3.2.4", "dns-over-http-resolver": "^2.1.1", - "events": "^3.3.0", "light-bolt11-decoder": "^2.1.0", "qr-code-styling": "^1.6.0-rc.1", "react": "^18.2.0", @@ -32,18 +27,9 @@ "react-textarea-autosize": "^8.4.0", "react-twitter-embed": "^4.0.4", "use-long-press": "^2.0.3", - "workbox-background-sync": "^6.4.2", - "workbox-broadcast-update": "^6.4.2", - "workbox-cacheable-response": "^6.4.2", "workbox-core": "^6.4.2", - "workbox-expiration": "^6.4.2", - "workbox-google-analytics": "^6.4.2", - "workbox-navigation-preload": "^6.4.2", - "workbox-precaching": "^6.4.2", - "workbox-range-requests": "^6.4.2", "workbox-routing": "^6.4.2", - "workbox-strategies": "^6.4.2", - "workbox-streams": "^6.4.2" + "workbox-strategies": "^6.4.2" }, "scripts": { "start": "webpack serve", diff --git a/packages/app/src/Cache/DMCache.ts b/packages/app/src/Cache/DMCache.ts index 4041a0fc..0f1c693c 100644 --- a/packages/app/src/Cache/DMCache.ts +++ b/packages/app/src/Cache/DMCache.ts @@ -1,6 +1,6 @@ import { NostrEvent } from "@snort/system"; +import { FeedCache } from "@snort/shared"; import { db } from "Db"; -import FeedCache from "./FeedCache"; class DMCache extends FeedCache { constructor() { diff --git a/packages/app/src/Cache/EventInteractionCache.ts b/packages/app/src/Cache/EventInteractionCache.ts index 1a727c06..f343d4af 100644 --- a/packages/app/src/Cache/EventInteractionCache.ts +++ b/packages/app/src/Cache/EventInteractionCache.ts @@ -1,7 +1,7 @@ +import { FeedCache } from "@snort/shared"; import { db, EventInteraction } from "Db"; import { LoginStore } from "Login"; import { sha256 } from "SnortUtils"; -import FeedCache from "./FeedCache"; class EventInteractionCache extends FeedCache { constructor() { diff --git a/packages/app/src/Cache/PaymentsCache.ts b/packages/app/src/Cache/PaymentsCache.ts index a52543b0..96d487d3 100644 --- a/packages/app/src/Cache/PaymentsCache.ts +++ b/packages/app/src/Cache/PaymentsCache.ts @@ -1,5 +1,5 @@ import { Payment, db } from "Db"; -import FeedCache from "./FeedCache"; +import { FeedCache } from "@snort/shared"; class Payments extends FeedCache { constructor() { diff --git a/packages/app/src/Cache/index.ts b/packages/app/src/Cache/index.ts index ec3f6c0f..7585f7ee 100644 --- a/packages/app/src/Cache/index.ts +++ b/packages/app/src/Cache/index.ts @@ -1,7 +1,10 @@ +import { UserProfileCache, UserRelaysCache } from "@snort/system"; import { DmCache } from "./DMCache"; import { InteractionCache } from "./EventInteractionCache"; -import { UserCache } from "./UserCache"; -import { UserRelays } from "./UserRelayCache"; + +export const UserCache = new UserProfileCache(); +export const UserRelays = new UserRelaysCache(); +export { DmCache }; export async function preload(follows?: Array) { const preloads = [ @@ -12,5 +15,3 @@ export async function preload(follows?: Array) { ]; await Promise.all(preloads); } - -export { UserCache, DmCache }; diff --git a/packages/app/src/Db/index.ts b/packages/app/src/Db/index.ts index e8d1724c..4ce448d3 100644 --- a/packages/app/src/Db/index.ts +++ b/packages/app/src/Db/index.ts @@ -1,8 +1,8 @@ import Dexie, { Table } from "dexie"; -import { FullRelaySettings, HexKey, NostrEvent, u256, MetadataCache } from "@snort/system"; +import { HexKey, NostrEvent, u256 } from "@snort/system"; export const NAME = "snortDB"; -export const VERSION = 9; +export const VERSION = 10; export interface SubCache { id: string; @@ -11,19 +11,6 @@ export interface SubCache { since?: number; } -export interface RelayMetrics { - addr: string; - events: number; - disconnects: number; - latency: number[]; -} - -export interface UsersRelays { - pubkey: HexKey; - created_at: number; - relays: FullRelaySettings[]; -} - export interface EventInteraction { id: u256; event: u256; @@ -41,10 +28,6 @@ export interface Payment { } const STORES = { - users: "++pubkey, name, display_name, picture, nip05, npub", - relays: "++addr", - userRelays: "++pubkey", - events: "++id, pubkey, created_at", dms: "++id, pubkey", eventInteraction: "++id", payments: "++url", @@ -52,10 +35,6 @@ const STORES = { export class SnortDB extends Dexie { ready = false; - users!: Table; - relayMetrics!: Table; - userRelays!: Table; - events!: Table; dms!: Table; eventInteraction!: Table; payments!: Table; diff --git a/packages/app/src/Element/Bookmarks.tsx b/packages/app/src/Element/Bookmarks.tsx index 118cf917..843ea3bd 100644 --- a/packages/app/src/Element/Bookmarks.tsx +++ b/packages/app/src/Element/Bookmarks.tsx @@ -4,7 +4,7 @@ import { HexKey, TaggedRawEvent } from "@snort/system"; import Note from "Element/Note"; import useLogin from "Hooks/useLogin"; -import { UserCache } from "Cache/UserCache"; +import { UserCache } from "Cache"; import messages from "./messages"; diff --git a/packages/app/src/Element/FollowButton.tsx b/packages/app/src/Element/FollowButton.tsx index 72e3fb33..3319c5fa 100644 --- a/packages/app/src/Element/FollowButton.tsx +++ b/packages/app/src/Element/FollowButton.tsx @@ -6,6 +6,7 @@ import useEventPublisher from "Feed/EventPublisher"; import { parseId } from "SnortUtils"; import useLogin from "Hooks/useLogin"; import AsyncButton from "Element/AsyncButton"; +import { System } from "index"; import messages from "./messages"; @@ -23,7 +24,7 @@ export default function FollowButton(props: FollowButtonProps) { async function follow(pubkey: HexKey) { if (publisher) { const ev = await publisher.contactList([pubkey, ...follows.item], relays.item); - publisher.broadcast(ev); + System.BroadcastEvent(ev); } } @@ -33,7 +34,7 @@ export default function FollowButton(props: FollowButtonProps) { follows.item.filter(a => a !== pubkey), relays.item ); - publisher.broadcast(ev); + System.BroadcastEvent(ev); } } diff --git a/packages/app/src/Element/FollowListBase.tsx b/packages/app/src/Element/FollowListBase.tsx index dfc7fdc5..bcc13c3f 100644 --- a/packages/app/src/Element/FollowListBase.tsx +++ b/packages/app/src/Element/FollowListBase.tsx @@ -5,6 +5,7 @@ import { HexKey } from "@snort/system"; import useEventPublisher from "Feed/EventPublisher"; import ProfilePreview from "Element/ProfilePreview"; import useLogin from "Hooks/useLogin"; +import { System } from "index"; import messages from "./messages"; @@ -31,7 +32,7 @@ export default function FollowListBase({ async function followAll() { if (publisher) { const ev = await publisher.contactList([...pubkeys, ...follows.item], relays.item); - publisher.broadcast(ev); + System.BroadcastEvent(ev); } } diff --git a/packages/app/src/Element/Nip5Service.tsx b/packages/app/src/Element/Nip5Service.tsx index 3eb16982..3d2368a3 100644 --- a/packages/app/src/Element/Nip5Service.tsx +++ b/packages/app/src/Element/Nip5Service.tsx @@ -25,6 +25,7 @@ import SnortServiceProvider from "Nip05/SnortServiceProvider"; import { UserCache } from "Cache"; import messages from "./messages"; +import { System } from "index"; type Nip05ServiceProps = { name: string; @@ -215,7 +216,7 @@ export default function Nip5Service(props: Nip05ServiceProps) { nip05, } as UserMetadata; const ev = await publisher.metadata(newProfile); - publisher.broadcast(ev); + System.BroadcastEvent(ev); if (props.onSuccess) { props.onSuccess(nip05); } diff --git a/packages/app/src/Element/Note.tsx b/packages/app/src/Element/Note.tsx index ef824778..a5530c3d 100644 --- a/packages/app/src/Element/Note.tsx +++ b/packages/app/src/Element/Note.tsx @@ -5,6 +5,7 @@ import { useInView } from "react-intersection-observer"; import { useIntl, FormattedMessage } from "react-intl"; import { TaggedRawEvent, HexKey, EventKind, NostrPrefix, Lists, EventExt } from "@snort/system"; +import { System } from "index"; import useEventPublisher from "Feed/EventPublisher"; import Icon from "Icons/Icon"; import { parseZap } from "Element/Zap"; @@ -24,7 +25,7 @@ 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 { UserCache } from "Cache"; import Poll from "Element/Poll"; import useLogin from "Hooks/useLogin"; import { setBookmarked, setPinned } from "Login"; @@ -151,7 +152,7 @@ export default function Note(props: NoteProps) { if (window.confirm(formatMessage(messages.ConfirmUnpin))) { const es = pinned.item.filter(e => e !== id); const ev = await publisher.noteList(es, Lists.Pinned); - publisher.broadcast(ev); + System.BroadcastEvent(ev); setPinned(login, es, ev.created_at * 1000); } } @@ -162,7 +163,7 @@ export default function Note(props: NoteProps) { if (window.confirm(formatMessage(messages.ConfirmUnbookmark))) { const es = bookmarked.item.filter(e => e !== id); const ev = await publisher.noteList(es, Lists.Bookmarked); - publisher.broadcast(ev); + System.BroadcastEvent(ev); setBookmarked(login, es, ev.created_at * 1000); } } diff --git a/packages/app/src/Element/NoteCreator.tsx b/packages/app/src/Element/NoteCreator.tsx index 958cc179..ef793f83 100644 --- a/packages/app/src/Element/NoteCreator.tsx +++ b/packages/app/src/Element/NoteCreator.tsx @@ -2,6 +2,7 @@ import "./NoteCreator.css"; import { FormattedMessage, useIntl } from "react-intl"; import { useDispatch, useSelector } from "react-redux"; import { encodeTLV, EventKind, NostrPrefix, TaggedRawEvent, EventBuilder } from "@snort/system"; +import { LNURL } from "@snort/shared"; import Icon from "Icons/Icon"; import useEventPublisher from "Feed/EventPublisher"; @@ -26,7 +27,6 @@ import { setOtherEvents, } from "State/NoteCreator"; import type { RootState } from "State/Store"; -import { LNURL } from "LNURL"; import messages from "./messages"; import { ClipboardEventHandler, useState } from "react"; @@ -35,6 +35,7 @@ import { Menu, MenuItem } from "@szhsin/react-menu"; import { LoginStore } from "Login"; import { getCurrentSubscription } from "Subscription"; import useLogin from "Hooks/useLogin"; +import { System } from "index"; interface NotePreviewProps { note: TaggedRawEvent; @@ -111,12 +112,12 @@ export function NoteCreator() { return eb; }; const ev = replyTo ? await publisher.reply(replyTo, note, hk) : await publisher.note(note, hk); - if (selectedCustomRelays) publisher.broadcastAll(ev, selectedCustomRelays); - else publisher.broadcast(ev); + if (selectedCustomRelays) selectedCustomRelays.forEach(r => System.WriteOnceToRelay(r, ev)); + else System.BroadcastEvent(ev); dispatch(reset()); for (const oe of otherEvents) { - if (selectedCustomRelays) publisher.broadcastAll(oe, selectedCustomRelays); - else publisher.broadcast(oe); + if (selectedCustomRelays) selectedCustomRelays.forEach(r => System.WriteOnceToRelay(r, oe)); + else System.BroadcastEvent(oe); } dispatch(reset()); } diff --git a/packages/app/src/Element/NoteFooter.tsx b/packages/app/src/Element/NoteFooter.tsx index 800c4d31..959ec04b 100644 --- a/packages/app/src/Element/NoteFooter.tsx +++ b/packages/app/src/Element/NoteFooter.tsx @@ -4,6 +4,7 @@ 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/system"; +import { LNURL } from "@snort/shared"; import Icon from "Icons/Icon"; import Spinner from "Icons/Spinner"; @@ -26,12 +27,12 @@ import { } from "State/ReBroadcast"; import useModeration from "Hooks/useModeration"; import { TranslateHost } from "Const"; -import { LNURL } from "LNURL"; import { useWallet } from "Wallet"; import useLogin from "Hooks/useLogin"; import { setBookmarked, setPinned } from "Login"; import { useInteractionCache } from "Hooks/useInteractionCache"; import { ZapPoolController } from "ZapPoolController"; +import { System } from "index"; import messages from "./messages"; @@ -117,7 +118,7 @@ export default function NoteFooter(props: NoteFooterProps) { async function react(content: string) { if (!hasReacted(content) && publisher) { const evLike = await publisher.react(ev, content); - publisher.broadcast(evLike); + System.BroadcastEvent(evLike); await interactionCache.react(); } } @@ -125,7 +126,7 @@ export default function NoteFooter(props: NoteFooterProps) { async function deleteEvent() { if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.id.substring(0, 8) })) && publisher) { const evDelete = await publisher.delete(ev.id); - publisher.broadcast(evDelete); + System.BroadcastEvent(evDelete); } } @@ -133,7 +134,7 @@ export default function NoteFooter(props: NoteFooterProps) { if (!hasReposted() && publisher) { if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.id }))) { const evRepost = await publisher.repost(ev); - publisher.broadcast(evRepost); + System.BroadcastEvent(evRepost); await interactionCache.repost(); } } @@ -292,7 +293,7 @@ export default function NoteFooter(props: NoteFooterProps) { if (publisher) { const es = [...pinned.item, id]; const ev = await publisher.noteList(es, Lists.Pinned); - publisher.broadcast(ev); + System.BroadcastEvent(ev); setPinned(login, es, ev.created_at * 1000); } } @@ -301,7 +302,7 @@ export default function NoteFooter(props: NoteFooterProps) { if (publisher) { const es = [...bookmarked.item, id]; const ev = await publisher.noteList(es, Lists.Bookmarked); - publisher.broadcast(ev); + System.BroadcastEvent(ev); setBookmarked(login, es, ev.created_at * 1000); } } diff --git a/packages/app/src/Element/Poll.tsx b/packages/app/src/Element/Poll.tsx index 4ef7dcf9..65cbf949 100644 --- a/packages/app/src/Element/Poll.tsx +++ b/packages/app/src/Element/Poll.tsx @@ -1,4 +1,5 @@ import { TaggedRawEvent } from "@snort/system"; +import { LNURL } from "@snort/shared"; import { useState } from "react"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; @@ -7,7 +8,6 @@ import Text from "Element/Text"; import useEventPublisher from "Feed/EventPublisher"; import { useWallet } from "Wallet"; import { useUserProfile } from "Hooks/useUserProfile"; -import { LNURL } from "LNURL"; import { unwrap } from "SnortUtils"; import { formatShort } from "Number"; import Spinner from "Icons/Spinner"; diff --git a/packages/app/src/Element/PubkeyList.tsx b/packages/app/src/Element/PubkeyList.tsx index f9bf86a4..062562c1 100644 --- a/packages/app/src/Element/PubkeyList.tsx +++ b/packages/app/src/Element/PubkeyList.tsx @@ -1,5 +1,6 @@ import { NostrEvent } from "@snort/system"; import { FormattedMessage, FormattedNumber } from "react-intl"; +import { LNURL } from "@snort/shared"; import { dedupe, hexToBech32, unixNow } from "SnortUtils"; import FollowListBase from "Element/FollowListBase"; @@ -9,7 +10,6 @@ import { Toastore } from "Toaster"; import { getDisplayName } from "Element/ProfileImage"; import { UserCache } from "Cache"; import useLogin from "Hooks/useLogin"; -import { LNURL } from "LNURL"; import useEventPublisher from "Feed/EventPublisher"; import { WalletInvoiceState } from "Wallet"; diff --git a/packages/app/src/Element/ReBroadcaster.tsx b/packages/app/src/Element/ReBroadcaster.tsx index fc280dfe..b3ad9dab 100644 --- a/packages/app/src/Element/ReBroadcaster.tsx +++ b/packages/app/src/Element/ReBroadcaster.tsx @@ -6,6 +6,7 @@ import type { RootState } from "State/Store"; import { setShow, reset, setSelectedCustomRelays } from "State/ReBroadcast"; import messages from "./messages"; import useLogin from "Hooks/useLogin"; +import { System } from "index"; export function ReBroadcaster() { const publisher = useEventPublisher(); @@ -14,8 +15,8 @@ export function ReBroadcaster() { async function sendReBroadcast() { if (note && publisher) { - if (selectedCustomRelays) publisher.broadcastAll(note, selectedCustomRelays); - else publisher.broadcast(note); + if (selectedCustomRelays) selectedCustomRelays.forEach(r => System.WriteOnceToRelay(r, note)); + else System.BroadcastEvent(note); dispatch(reset()); } } diff --git a/packages/app/src/Element/SendSats.tsx b/packages/app/src/Element/SendSats.tsx index 1c55269d..9963fd8b 100644 --- a/packages/app/src/Element/SendSats.tsx +++ b/packages/app/src/Element/SendSats.tsx @@ -3,6 +3,8 @@ import React, { useEffect, useMemo, useState } from "react"; import { useIntl, FormattedMessage } from "react-intl"; import { HexKey, NostrEvent, EventPublisher } from "@snort/system"; +import { LNURL, LNURLError, LNURLErrorCode, LNURLInvoice, LNURLSuccessAction } from "@snort/shared"; + import { System } from "index"; import { formatShort } from "Number"; import Icon from "Icons/Icon"; @@ -11,7 +13,6 @@ import ProfileImage from "Element/ProfileImage"; 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 "SnortUtils"; import { useWallet } from "Wallet"; import useLogin from "Hooks/useLogin"; @@ -133,7 +134,7 @@ export default function SendSats(props: SendSatsProps) { const randomKey = generateRandomKey(); console.debug("Generated new key for zap: ", randomKey); - const publisher = new EventPublisher(System, randomKey.publicKey, randomKey.privateKey); + const publisher = new EventPublisher(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/Textarea.tsx b/packages/app/src/Element/Textarea.tsx index 352ade10..4d172507 100644 --- a/packages/app/src/Element/Textarea.tsx +++ b/packages/app/src/Element/Textarea.tsx @@ -9,7 +9,7 @@ import { NostrPrefix, MetadataCache } from "@snort/system"; import Avatar from "Element/Avatar"; import Nip05 from "Element/Nip05"; import { hexToBech32 } from "SnortUtils"; -import { UserCache } from "Cache/UserCache"; +import { UserCache } from "Cache"; import messages from "./messages"; diff --git a/packages/app/src/Element/WriteDm.tsx b/packages/app/src/Element/WriteDm.tsx index e060c1f7..9c6ede07 100644 --- a/packages/app/src/Element/WriteDm.tsx +++ b/packages/app/src/Element/WriteDm.tsx @@ -6,6 +6,7 @@ import { useState } from "react"; import useFileUpload from "Upload"; import { openFile } from "SnortUtils"; import Textarea from "./Textarea"; +import { System } from "index"; export default function WriteDm({ chatPubKey }: { chatPubKey: string }) { const [msg, setMsg] = useState(""); @@ -57,7 +58,7 @@ export default function WriteDm({ chatPubKey }: { chatPubKey: string }) { if (msg && publisher) { setSending(true); const ev = await publisher.sendDm(msg, chatPubKey); - publisher.broadcast(ev); + System.BroadcastEvent(ev); setMsg(""); setSending(false); } diff --git a/packages/app/src/Element/Zap.tsx b/packages/app/src/Element/Zap.tsx index cb59eee1..0bcb7821 100644 --- a/packages/app/src/Element/Zap.tsx +++ b/packages/app/src/Element/Zap.tsx @@ -8,7 +8,7 @@ import { formatShort } from "Number"; import Text from "Element/Text"; import ProfileImage from "Element/ProfileImage"; import { findTag } from "SnortUtils"; -import { UserCache } from "Cache/UserCache"; +import { UserCache } from "Cache"; import useLogin from "Hooks/useLogin"; import messages from "./messages"; diff --git a/packages/app/src/ExternalStore.ts b/packages/app/src/ExternalStore.ts deleted file mode 100644 index 4b1dedea..00000000 --- a/packages/app/src/ExternalStore.ts +++ /dev/null @@ -1,41 +0,0 @@ -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/app/src/Feed/EventPublisher.ts b/packages/app/src/Feed/EventPublisher.ts index ff238977..d8ed51f9 100644 --- a/packages/app/src/Feed/EventPublisher.ts +++ b/packages/app/src/Feed/EventPublisher.ts @@ -1,13 +1,12 @@ import { useMemo } from "react"; import useLogin from "Hooks/useLogin"; import { EventPublisher } from "@snort/system"; -import { System } from "index"; export default function useEventPublisher() { const { publicKey, privateKey } = useLogin(); return useMemo(() => { if (publicKey) { - return new EventPublisher(System, publicKey, privateKey); + return new EventPublisher(publicKey, privateKey); } }, [publicKey, privateKey]); } diff --git a/packages/app/src/Feed/LoginFeed.ts b/packages/app/src/Feed/LoginFeed.ts index bf4a71b6..9ac53b26 100644 --- a/packages/app/src/Feed/LoginFeed.ts +++ b/packages/app/src/Feed/LoginFeed.ts @@ -14,7 +14,7 @@ import { addSubscription, setBlocked, setBookmarked, setFollows, setMuted, setPi import { SnortPubKey } from "Const"; import { SubscriptionEvent } from "Subscription"; import useRelaysFeedFollows from "./RelaysFeedFollows"; -import { UserRelays } from "Cache/UserRelayCache"; +import { UserRelays } from "Cache"; /** * Managed loading data for the current logged in user diff --git a/packages/app/src/Feed/RelaysFeedFollows.tsx b/packages/app/src/Feed/RelaysFeedFollows.tsx index 77367f4d..248ed587 100644 --- a/packages/app/src/Feed/RelaysFeedFollows.tsx +++ b/packages/app/src/Feed/RelaysFeedFollows.tsx @@ -12,7 +12,7 @@ import debug from "debug"; import { sanitizeRelayUrl } from "SnortUtils"; import useRequestBuilder from "Hooks/useRequestBuilder"; -import { UserRelays } from "Cache/UserRelayCache"; +import { UserRelays } from "Cache"; interface RelayList { pubkey: string; diff --git a/packages/app/src/Hooks/useImgProxy.ts b/packages/app/src/Hooks/useImgProxy.ts index 76bb8747..090fa4f4 100644 --- a/packages/app/src/Hooks/useImgProxy.ts +++ b/packages/app/src/Hooks/useImgProxy.ts @@ -1,5 +1,5 @@ import * as utils from "@noble/curves/abstract/utils"; -import * as base64 from "@protobufjs/base64"; +import { base64 } from "@scure/base"; import { hmacSha256, unwrap } from "SnortUtils"; import useLogin from "Hooks/useLogin"; @@ -23,7 +23,7 @@ export default function useImgProxy() { utils.hexToBytes(unwrap(settings).salt), te.encode(u) ); - return urlSafe(base64.encode(result, 0, result.byteLength)); + return urlSafe(base64.encode(result)); } return { @@ -32,7 +32,7 @@ export default function useImgProxy() { if (url.startsWith("data:") || url.startsWith("blob:")) return url; const opt = resize ? `rs:fit:${resize}:${resize}/dpr:${window.devicePixelRatio}` : ""; const urlBytes = te.encode(url); - const urlEncoded = urlSafe(base64.encode(urlBytes, 0, urlBytes.byteLength)); + const urlEncoded = urlSafe(base64.encode(urlBytes)); const path = `/${opt}/${urlEncoded}`; const sig = signUrl(path); return `${new URL(settings.url).toString()}${sig}${path}`; diff --git a/packages/app/src/Hooks/useModeration.tsx b/packages/app/src/Hooks/useModeration.tsx index 3a530c5a..0e19d86d 100644 --- a/packages/app/src/Hooks/useModeration.tsx +++ b/packages/app/src/Hooks/useModeration.tsx @@ -3,6 +3,7 @@ import useEventPublisher from "Feed/EventPublisher"; import useLogin from "Hooks/useLogin"; import { setBlocked, setMuted } from "Login"; import { appendDedupe } from "SnortUtils"; +import { System } from "index"; export default function useModeration() { const login = useLogin(); @@ -12,7 +13,7 @@ export default function useModeration() { async function setMutedList(pub: HexKey[], priv: HexKey[]) { if (publisher) { const ev = await publisher.muted(pub, priv); - publisher.broadcast(ev); + System.BroadcastEvent(ev); return ev.created_at * 1000; } return 0; diff --git a/packages/app/src/Hooks/useUserProfile.ts b/packages/app/src/Hooks/useUserProfile.ts index 3909774a..e10a2840 100644 --- a/packages/app/src/Hooks/useUserProfile.ts +++ b/packages/app/src/Hooks/useUserProfile.ts @@ -1,8 +1,8 @@ import { useEffect, useSyncExternalStore } from "react"; import { HexKey, MetadataCache } from "@snort/system"; -import { UserCache } from "Cache/UserCache"; import { ProfileLoader } from "index"; +import { UserCache } from "Cache"; export function useUserProfile(pubKey?: HexKey): MetadataCache | undefined { const user = useSyncExternalStore( diff --git a/packages/app/src/Login/Functions.ts b/packages/app/src/Login/Functions.ts index 1ba6ffcf..08e4a3be 100644 --- a/packages/app/src/Login/Functions.ts +++ b/packages/app/src/Login/Functions.ts @@ -78,9 +78,9 @@ export async function generateNewLogin() { } const publicKey = utils.bytesToHex(secp.schnorr.getPublicKey(privateKey)); - const publisher = new EventPublisher(System, publicKey, privateKey); + const publisher = new EventPublisher(publicKey, privateKey); const ev = await publisher.contactList([bech32ToHex(SnortPubKey), publicKey], newRelays); - publisher.broadcast(ev); + System.BroadcastEvent(ev); LoginStore.loginWithPrivateKey(privateKey, entropy, newRelays); } diff --git a/packages/app/src/Login/MultiAccountStore.ts b/packages/app/src/Login/MultiAccountStore.ts index a723879c..aeccb629 100644 --- a/packages/app/src/Login/MultiAccountStore.ts +++ b/packages/app/src/Login/MultiAccountStore.ts @@ -2,11 +2,10 @@ import * as secp from "@noble/curves/secp256k1"; import * as utils from "@noble/curves/abstract/utils"; import { HexKey, RelaySettings } from "@snort/system"; +import { deepClone, sanitizeRelayUrl, unwrap, ExternalStore } from "@snort/shared"; import { DefaultRelays } from "Const"; -import ExternalStore from "ExternalStore"; import { LoginSession } from "Login"; -import { deepClone, sanitizeRelayUrl, unwrap } from "SnortUtils"; import { DefaultPreferences, UserPreferences } from "./Preferences"; const AccountStoreKey = "sessions"; diff --git a/packages/app/src/Notifications.ts b/packages/app/src/Notifications.ts index 46a4fdef..37e6e837 100644 --- a/packages/app/src/Notifications.ts +++ b/packages/app/src/Notifications.ts @@ -4,7 +4,7 @@ import { TaggedRawEvent, EventKind, MetadataCache } from "@snort/system"; import { getDisplayName } from "Element/ProfileImage"; import { MentionRegex } from "Const"; import { tagFilterOfTextRepost, unwrap } from "SnortUtils"; -import { UserCache } from "Cache/UserCache"; +import { UserCache } from "Cache"; import { LoginSession } from "Login"; export interface NotificationRequest { diff --git a/packages/app/src/Pages/HashTagsPage.tsx b/packages/app/src/Pages/HashTagsPage.tsx index 8dc27e06..cbcf4db2 100644 --- a/packages/app/src/Pages/HashTagsPage.tsx +++ b/packages/app/src/Pages/HashTagsPage.tsx @@ -6,6 +6,7 @@ import Timeline from "Element/Timeline"; import useEventPublisher from "Feed/EventPublisher"; import useLogin from "Hooks/useLogin"; import { setTags } from "Login"; +import { System } from "index"; const HashTagsPage = () => { const params = useParams(); @@ -19,7 +20,7 @@ const HashTagsPage = () => { async function followTags(ts: string[]) { if (publisher) { const ev = await publisher.tags(ts); - publisher.broadcast(ev); + System.BroadcastEvent(ev); setTags(login, ts, ev.created_at * 1000); } } diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx index 6c450958..014ee7b9 100644 --- a/packages/app/src/Pages/Layout.tsx +++ b/packages/app/src/Pages/Layout.tsx @@ -14,7 +14,6 @@ import useLoginFeed from "Feed/LoginFeed"; import { totalUnread } from "Pages/MessagesPage"; import useModeration from "Hooks/useModeration"; import { NoteCreator } from "Element/NoteCreator"; -import useEventPublisher from "Feed/EventPublisher"; import { useDmCache } from "Hooks/useDmsCache"; import { mapPlanName } from "./subscribe"; import useLogin from "Hooks/useLogin"; @@ -34,7 +33,6 @@ export default function Layout() { const { publicKey, relays, preferences, subscriptions } = useLogin(); const currentSubscription = getCurrentSubscription(subscriptions); const [pageClass, setPageClass] = useState("page"); - const pub = useEventPublisher(); useLoginFeed(); const handleNoteCreatorButtonClick = () => { @@ -63,12 +61,6 @@ export default function Layout() { } }, [location]); - useEffect(() => { - if (pub) { - System.HandleAuth = pub.nip42Auth.bind(pub); - } - }, [pub]); - useEffect(() => { if (relays) { (async () => { diff --git a/packages/app/src/Pages/ProfilePage.tsx b/packages/app/src/Pages/ProfilePage.tsx index 5394f2c4..401a690a 100644 --- a/packages/app/src/Pages/ProfilePage.tsx +++ b/packages/app/src/Pages/ProfilePage.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { useIntl, FormattedMessage } from "react-intl"; import { useNavigate, useParams } from "react-router-dom"; import { encodeTLV, EventKind, HexKey, NostrPrefix, tryParseNostrLink } from "@snort/system"; +import { LNURL } from "@snort/shared"; import { getReactions, unwrap } from "SnortUtils"; import { formatShort } from "Number"; @@ -43,7 +44,6 @@ import { ProxyImg } from "Element/ProxyImg"; import useHorizontalScroll from "Hooks/useHorizontalScroll"; import { EmailRegex } from "Const"; import { getNip05PubKey } from "Pages/LoginPage"; -import { LNURL } from "LNURL"; import useLogin from "Hooks/useLogin"; import messages from "./messages"; diff --git a/packages/app/src/Pages/new/ProfileSetup.tsx b/packages/app/src/Pages/new/ProfileSetup.tsx index 26f5f23a..8023d8b6 100644 --- a/packages/app/src/Pages/new/ProfileSetup.tsx +++ b/packages/app/src/Pages/new/ProfileSetup.tsx @@ -9,9 +9,10 @@ import useLogin from "Hooks/useLogin"; import { useUserProfile } from "Hooks/useUserProfile"; import { UserCache } from "Cache"; import AvatarEditor from "Element/AvatarEditor"; +import { DISCOVER } from "."; +import { System } from "index"; import messages from "./messages"; -import { DISCOVER } from "."; export default function ProfileSetup() { const login = useLogin(); @@ -36,7 +37,7 @@ export default function ProfileSetup() { name: username, picture, }); - publisher.broadcast(ev); + System.BroadcastEvent(ev); const profile = mapEventToProfile(ev); if (profile) { UserCache.set(profile); diff --git a/packages/app/src/Pages/settings/Profile.tsx b/packages/app/src/Pages/settings/Profile.tsx index 1918036c..145dd583 100644 --- a/packages/app/src/Pages/settings/Profile.tsx +++ b/packages/app/src/Pages/settings/Profile.tsx @@ -5,6 +5,7 @@ import { FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; import { mapEventToProfile } from "@snort/system"; +import { System } from "index"; import useEventPublisher from "Feed/EventPublisher"; import { useUserProfile } from "Hooks/useUserProfile"; import { openFile } from "SnortUtils"; @@ -77,7 +78,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) { if (publisher) { const ev = await publisher.metadata(userCopy); - publisher.broadcast(ev); + System.BroadcastEvent(ev); const newProfile = mapEventToProfile(ev); if (newProfile) { diff --git a/packages/app/src/Pages/settings/Relays.tsx b/packages/app/src/Pages/settings/Relays.tsx index d5338309..9470ed22 100644 --- a/packages/app/src/Pages/settings/Relays.tsx +++ b/packages/app/src/Pages/settings/Relays.tsx @@ -22,12 +22,14 @@ const RelaySettingsPage = () => { async function saveRelays() { if (publisher) { const ev = await publisher.contactList(login.follows.item, login.relays.item); - publisher.broadcast(ev); + System.BroadcastEvent(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.item).concat(randomSample(onlineRelays, 20)); - publisher.broadcastAll(relayList, rs); + rs.forEach(r => { + System.WriteOnceToRelay(r, relayList); + }); } catch (error) { console.error(error); } diff --git a/packages/app/src/Pages/settings/handle/LNAddress.tsx b/packages/app/src/Pages/settings/handle/LNAddress.tsx index 8f282eba..0adea2f4 100644 --- a/packages/app/src/Pages/settings/handle/LNAddress.tsx +++ b/packages/app/src/Pages/settings/handle/LNAddress.tsx @@ -1,10 +1,10 @@ import { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; +import { LNURL } from "@snort/shared"; import { ApiHost } from "Const"; import AsyncButton from "Element/AsyncButton"; import useEventPublisher from "Feed/EventPublisher"; -import { LNURL } from "LNURL"; import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider"; export default function LNForwardAddress({ handle }: { handle: ManageHandle }) { diff --git a/packages/app/src/SnortUtils/index.ts b/packages/app/src/SnortUtils/index.ts index 329e2f29..77f2ab97 100644 --- a/packages/app/src/SnortUtils/index.ts +++ b/packages/app/src/SnortUtils/index.ts @@ -4,8 +4,7 @@ import { sha256 as hash } from "@noble/hashes/sha256"; import { hmac } from "@noble/hashes/hmac"; import { bytesToHex } from "@noble/hashes/utils"; import { decode as invoiceDecode } from "light-bolt11-decoder"; -import { bech32 } from "bech32"; -import base32Decode from "base32-decode"; +import { bech32, base32hex } from "@scure/base"; import { HexKey, TaggedRawEvent, @@ -433,8 +432,8 @@ export function magnetURIDecode(uri: string): Magnet | undefined { if ((m = xt.match(/^urn:btih:(.{40})/))) { result.infoHash = [m[1].toLowerCase()]; } else if ((m = xt.match(/^urn:btih:(.{32})/))) { - const decodedStr = base32Decode(m[1], "RFC4648-HEX"); - result.infoHash = [bytesToHex(new Uint8Array(decodedStr))]; + const decodedStr = base32hex.decode(m[1]); + result.infoHash = [bytesToHex(decodedStr)]; } else if ((m = xt.match(/^urn:btmh:1220(.{64})/))) { result.infoHashV2 = [m[1].toLowerCase()]; } diff --git a/packages/app/src/Toaster.tsx b/packages/app/src/Toaster.tsx index e00f2ac6..eecb85ca 100644 --- a/packages/app/src/Toaster.tsx +++ b/packages/app/src/Toaster.tsx @@ -1,9 +1,8 @@ import { ReactNode, useSyncExternalStore } from "react"; import { v4 as uuid } from "uuid"; -import ExternalStore from "ExternalStore"; -import Icon from "Icons/Icon"; -import { unixNow } from "SnortUtils"; +import { ExternalStore, unixNow } from "@snort/shared"; +import Icon from "Icons/Icon"; import "./Toaster.css"; interface ToastNotification { diff --git a/packages/app/src/Wallet/WebLN.ts b/packages/app/src/Wallet/WebLN.ts index bec72fdf..e9708277 100644 --- a/packages/app/src/Wallet/WebLN.ts +++ b/packages/app/src/Wallet/WebLN.ts @@ -12,7 +12,7 @@ import { WalletKind, WalletStore, } from "Wallet"; -import { barrierQueue, processWorkQueue, WorkQueueItem } from "WorkQueue"; +import { barrierQueue, processWorkQueue, WorkQueueItem } from "@snort/shared"; const WebLNQueue: Array = []; processWorkQueue(WebLNQueue); diff --git a/packages/app/src/Wallet/index.ts b/packages/app/src/Wallet/index.ts index aa93597d..8ae307fc 100644 --- a/packages/app/src/Wallet/index.ts +++ b/packages/app/src/Wallet/index.ts @@ -1,6 +1,6 @@ import { useEffect, useSyncExternalStore } from "react"; -import ExternalStore from "ExternalStore"; +import { ExternalStore } from "@snort/shared"; import { decodeInvoice, unwrap } from "SnortUtils"; import LNDHubWallet from "./LNDHub"; import { NostrConnectWallet } from "./NostrWalletConnect"; diff --git a/packages/app/src/ZapPoolController.ts b/packages/app/src/ZapPoolController.ts index 3db499f2..0695b5d7 100644 --- a/packages/app/src/ZapPoolController.ts +++ b/packages/app/src/ZapPoolController.ts @@ -1,9 +1,7 @@ import { UserCache } from "Cache"; import { getDisplayName } from "Element/ProfileImage"; -import ExternalStore from "ExternalStore"; -import { LNURL } from "LNURL"; +import { LNURL, ExternalStore, unixNow } from "@snort/shared"; import { Toastore } from "Toaster"; -import { unixNow } from "SnortUtils"; import { LNWallet, WalletInvoiceState, Wallets } from "Wallet"; export enum ZapPoolRecipientType { diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx index b91d73f6..37106301 100644 --- a/packages/app/src/index.tsx +++ b/packages/app/src/index.tsx @@ -35,14 +35,21 @@ 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"; +import { EventPublisher, NostrSystem, ProfileLoaderService } from "@snort/system"; +import { UserRelays } from "Cache"; /** * Singleton nostr system */ export const System = new NostrSystem({ - get: pk => UserRelays.getFromCache(pk)?.relays, + relayCache: UserRelays, + authHandler: async (c, r) => { + const { publicKey, privateKey } = LoginStore.snapshot(); + if (publicKey) { + const pub = new EventPublisher(publicKey, privateKey); + return await pub.nip42Auth(c, r); + } + }, }); /** diff --git a/packages/app/src/service-worker.ts b/packages/app/src/service-worker.ts index 497749be..6c2c1d91 100644 --- a/packages/app/src/service-worker.ts +++ b/packages/app/src/service-worker.ts @@ -3,18 +3,16 @@ import {} from "."; declare const self: ServiceWorkerGlobalScope; import { clientsClaim } from "workbox-core"; -import { ExpirationPlugin } from "workbox-expiration"; import { registerRoute } from "workbox-routing"; -import { StaleWhileRevalidate, CacheFirst } from "workbox-strategies"; +import { CacheFirst } from "workbox-strategies"; clientsClaim(); -const staticTypes = ["image", "video", "audio"]; +const staticTypes = ["image", "video", "audio", "script", "style", "font"]; registerRoute( ({ request, url }) => url.origin === self.location.origin && staticTypes.includes(request.destination), - new StaleWhileRevalidate({ + new CacheFirst({ cacheName: "static-content", - plugins: [new ExpirationPlugin({ maxEntries: 50 })], }) ); diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 00000000..027f5c4e --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,21 @@ +{ + "name": "@snort/shared", + "version": "1.0.0", + "description": "Shared components for Snort", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "repository": "https://git.v0l.io/Kieran/snort", + "author": "Kieran", + "license": "GPL-3.0-or-later", + "private": false, + "scripts": { + "build": "tsc" + }, + "dependencies": { + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", + "@scure/base": "^1.1.1", + "debug": "^4.3.4", + "dexie": "^3.2.4" + } +} diff --git a/packages/shared/src/const.ts b/packages/shared/src/const.ts new file mode 100644 index 00000000..051e6842 --- /dev/null +++ b/packages/shared/src/const.ts @@ -0,0 +1,8 @@ + +/** + * Regex to match email address + */ + export const EmailRegex = + // eslint-disable-next-line no-useless-escape + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + \ No newline at end of file diff --git a/packages/system/src/ExternalStore.ts b/packages/shared/src/external-store.ts similarity index 83% rename from packages/system/src/ExternalStore.ts rename to packages/shared/src/external-store.ts index 4b1dedea..72e537dd 100644 --- a/packages/system/src/ExternalStore.ts +++ b/packages/shared/src/external-store.ts @@ -1,13 +1,13 @@ -type HookFn = (e?: TSnapshot) => void; +export type HookFn = (e?: TSnapshot) => void; -interface HookFilter { +export interface HookFilter { fn: HookFn; } /** * Simple React hookable store with manual change notifications */ -export default abstract class ExternalStore { +export abstract class ExternalStore { #hooks: Array> = []; #snapshot: Readonly = {} as Readonly; #changed = true; diff --git a/packages/app/src/Cache/FeedCache.ts b/packages/shared/src/feed-cache.ts similarity index 87% rename from packages/app/src/Cache/FeedCache.ts rename to packages/shared/src/feed-cache.ts index 1e62fb2d..22be7384 100644 --- a/packages/app/src/Cache/FeedCache.ts +++ b/packages/shared/src/feed-cache.ts @@ -1,27 +1,29 @@ -import { db } from "Db"; import debug from "debug"; import { Table } from "dexie"; -import { unixNowMs, unwrap } from "SnortUtils"; +import { unixNowMs, unwrap } from "./utils"; type HookFn = () => void; -interface HookFilter { +export interface KeyedHookFilter { key: string; fn: HookFn; } -export default abstract class FeedCache { +/** + * Dexie backed generic hookable store + */ +export abstract class FeedCache { #name: string; - #hooks: Array = []; + #hooks: Array = []; #snapshot: Readonly> = []; #changed = true; #hits = 0; #miss = 0; - protected table: Table; + protected table?: Table; protected onTable: Set = new Set(); protected cache: Map = new Map(); - constructor(name: string, table: Table) { + constructor(name: string, table?: Table) { this.#name = name; this.table = table; setInterval(() => { @@ -36,10 +38,8 @@ export default abstract class FeedCache { } async preload() { - if (db.ready) { - const keys = await this.table.toCollection().primaryKeys(); - this.onTable = new Set(keys.map(a => a as string)); - } + const keys = await this.table?.toCollection().primaryKeys() ?? []; + this.onTable = new Set(keys.map(a => a as string)); } hook(fn: HookFn, key: string | undefined) { @@ -74,7 +74,7 @@ export default abstract class FeedCache { } async get(key?: string) { - if (key && !this.cache.has(key) && db.ready) { + if (key && !this.cache.has(key) && this.table) { const cached = await this.table.get(key); if (cached) { this.cache.set(this.key(cached), cached); @@ -87,7 +87,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) { + if (missing.length > 0 && this.table) { const cached = await this.table.bulkGet(missing); cached.forEach(a => { if (a) { @@ -104,7 +104,7 @@ export default abstract class FeedCache { async set(obj: TCached) { const k = this.key(obj); this.cache.set(k, obj); - if (db.ready) { + if (this.table) { await this.table.put(obj); this.onTable.add(k); } @@ -112,7 +112,7 @@ export default abstract class FeedCache { } async bulkSet(obj: Array) { - if (db.ready) { + if (this.table) { await this.table.bulkPut(obj); obj.forEach(a => this.onTable.add(this.key(a))); } @@ -158,7 +158,7 @@ export default abstract class FeedCache { */ async buffer(keys: Array): Promise> { const needsBuffer = keys.filter(a => !this.cache.has(a)); - if (db.ready && needsBuffer.length > 0) { + if (this.table && needsBuffer.length > 0) { const mapped = needsBuffer.map(a => ({ has: this.onTable.has(a), key: a, @@ -184,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/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 00000000..40e8f328 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,5 @@ +export * from "./external-store"; +export * from "./lnurl"; +export * from "./utils"; +export * from "./work-queue"; +export * from "./feed-cache"; \ No newline at end of file diff --git a/packages/app/src/LNURL.ts b/packages/shared/src/lnurl.ts similarity index 90% rename from packages/app/src/LNURL.ts rename to packages/shared/src/lnurl.ts index 42978610..bd63dc2f 100644 --- a/packages/app/src/LNURL.ts +++ b/packages/shared/src/lnurl.ts @@ -1,6 +1,5 @@ -import { HexKey, NostrEvent } from "@snort/system"; -import { EmailRegex } from "Const"; -import { bech32ToText, unwrap } from "SnortUtils"; +import { EmailRegex } from "./const"; +import { bech32ToText, unwrap } from "./utils"; const PayServiceTag = "payRequest"; @@ -119,7 +118,7 @@ export class LNURL { * @param zap * @returns */ - async getInvoice(amount: number, comment?: string, zap?: NostrEvent) { + async getInvoice(amount: number, comment?: string, zap?: object) { const callback = new URL(unwrap(this.#service?.callback)); const query = new Map(); @@ -206,26 +205,26 @@ export class LNURL { } export interface LNURLService { - tag: string; - nostrPubkey?: HexKey; - minSendable?: number; - maxSendable?: number; - metadata: string; - callback: string; - commentAllowed?: number; + tag: string + nostrPubkey?: string + minSendable?: number + maxSendable?: number + metadata: string + callback: string + commentAllowed?: number } export interface LNURLStatus { - status: "SUCCESS" | "ERROR"; - reason?: string; + status: "SUCCESS" | "ERROR" + reason?: string } export interface LNURLInvoice extends LNURLStatus { - pr?: string; - successAction?: LNURLSuccessAction; + pr?: string + successAction?: LNURLSuccessAction } export interface LNURLSuccessAction { - description?: string; - url?: string; + description?: string + url?: string } diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts new file mode 100644 index 00000000..f0faec9f --- /dev/null +++ b/packages/shared/src/utils.ts @@ -0,0 +1,184 @@ +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 "@scure/base"; + +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 deepClone(obj: T) { + if ("structuredClone" in window) { + return structuredClone(obj); + } else { + return JSON.parse(JSON.stringify(obj)); + } +} + +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 countMembers(a: any) { + let ret = 0; + for (const [k, v] of Object.entries(a)) { + if (Array.isArray(v)) { + ret += v.length; + } + } + return ret; +} + +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 const sha256 = (str: string | Uint8Array): string => { + 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; + } +} + +/** + * Decode bech32 to string UTF-8 + * @param str bech32 encoded string + * @returns + */ +export function bech32ToText(str: string) { + try { + const decoded = bech32.decode(str, 1000); + const buf = bech32.fromWords(decoded.words); + return new TextDecoder().decode(Uint8Array.from(buf)); + } catch { + return ""; + } +} + +export async function fetchNip05Pubkey(name: string, domain: string, timeout = 2_000) { + interface NostrJson { + names: Record; + } + if (!name || !domain) { + return undefined; + } + try { + const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`, { + signal: AbortSignal.timeout(timeout), + }); + const data: NostrJson = await res.json(); + const match = Object.keys(data.names).find(n => { + return n.toLowerCase() === name.toLowerCase(); + }); + return match ? data.names[match] : undefined; + } catch { + // ignored + } + return undefined; +} diff --git a/packages/app/src/WorkQueue.ts b/packages/shared/src/work-queue.ts similarity index 100% rename from packages/app/src/WorkQueue.ts rename to packages/shared/src/work-queue.ts diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 00000000..b57c5cd3 --- /dev/null +++ b/packages/shared/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/README.md b/packages/system/README.md new file mode 100644 index 00000000..ad7bf86e --- /dev/null +++ b/packages/system/README.md @@ -0,0 +1,66 @@ +## @snort/system + +A collection of caching and querying techniquies used by https://snort.social to serve all content from the nostr protocol. + +Simple example: +```js +import { + NostrSystem, + EventPublisher, + UserRelaysCache, + RequestBuilder, + FlatNoteStore, + StoreSnapshot +} from "@snort/system" + +// Provided in-memory / indexedDb cache for relays +// You can also implement your own with "RelayCache" interface +const RelaysCache = new UserRelaysCache(); + +// example auth handler using NIP-07 +const AuthHandler = async (challenge: string, relay: string) => { + const pub = await EventPublisher.nip7(); + if (pub) { + return await pub.nip42Auth(challenge, relay); + } +} + +// Singleton instance to store all connections and access query fetching system +const System = new NostrSystem({ + relayCache: RelaysCache, + authHandler: AuthHandler // can be left undefined if you dont care about NIP-42 Auth +}); + +(async () => { + // connec to one "bootstrap" relay to pull profiles/relay lists from + // also used as a fallback relay when gossip model doesnt know which relays to pick, or "authors" are not provided in the request + await System.ConnectToRelay("wss://relay.snort.social", { read: true, write: false }); + + // ID should be unique to the use case, this is important as all data fetched from this ID will be merged into the same NoteStore + const rb = new RequestBuilder("get-posts"); + rb.withFilter() + .authors(["63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"]) // Kieran pubkey + .kinds([1]) + .limit(10); + + const q = System.Query(FlatNoteStore, rb); + // basic usage using "onEvent", fired for every event added to the store + q.onEvent = (sub, e) => { + console.debug(sub, e); + } + + // Hookable type using change notification, limited to every 500ms + const release = q.feed.hook(() => { + // since we use the FlatNoteStore we expect NostrEvent[] + // other stores provide different data, like a single event instead of an array (latest version) + const state = q.feed.snapshot as StoreSnapshot>; + + // do something with snapshot of store + console.log(`We have ${state.data.length} events now!`) + }); + + // release the hook when its not needed anymore + // these patterns will be managed in @snort/system-react to make it easier to use react or other UI frameworks + // release(); +})(); +``` \ No newline at end of file diff --git a/packages/system/examples/simple.ts b/packages/system/examples/simple.ts new file mode 100644 index 00000000..700a9eb8 --- /dev/null +++ b/packages/system/examples/simple.ts @@ -0,0 +1,52 @@ +import { NostrSystem, EventPublisher, UserRelaysCache, RequestBuilder, FlatNoteStore, StoreSnapshot } from "../src" + +// Provided in-memory / indexedDb cache for relays +// You can also implement your own with "RelayCache" interface +const RelaysCache = new UserRelaysCache(); + +// example auth handler using NIP-07 +const AuthHandler = async (challenge: string, relay: string) => { + const pub = await EventPublisher.nip7(); + if (pub) { + return await pub.nip42Auth(challenge, relay); + } +} + +// Singleton instance to store all connections and access query fetching system +const System = new NostrSystem({ + relayCache: RelaysCache, + authHandler: AuthHandler // can be left undefined if you dont care about NIP-42 Auth +}); + +(async () => { + // connec to one "bootstrap" relay to pull profiles/relay lists from + // also used as a fallback relay when gossip model doesnt know which relays to pick, or "authors" are not provided in the request + await System.ConnectToRelay("wss://relay.snort.social", { read: true, write: false }); + + // ID should be unique to the use case, this is important as all data fetched from this ID will be merged into the same NoteStore + const rb = new RequestBuilder("get-posts"); + rb.withFilter() + .authors(["63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"]) // Kieran pubkey + .kinds([1]) + .limit(10); + + const q = System.Query(FlatNoteStore, rb); + // basic usage using "onEvent", fired for every event added to the store + q.onEvent = (sub, e) => { + console.debug(sub, e); + } + + // Hookable type using change notification, limited to every 500ms + const release = q.feed.hook(() => { + // since we use the FlatNoteStore we expect NostrEvent[] + // other stores provide different data, like a single event instead of an array (latest version) + const state = q.feed.snapshot as StoreSnapshot>; + + // do something with snapshot of store + console.log(`We have ${state.data.length} events now!`) + }); + + // release the hook when its not needed anymore + // these patterns will be managed in @snort/system-react to make it easier to use react or other UI frameworks + // release(); +})(); \ No newline at end of file diff --git a/packages/system/package.json b/packages/system/package.json index 892388a5..b43c00ab 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -1,6 +1,6 @@ { "name": "@snort/system", - "version": "1.0.2", + "version": "1.0.3", "description": "Snort nostr system package", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -30,6 +30,7 @@ "@stablelib/xchacha20": "^1.0.1", "bech32": "^2.0.0", "debug": "^4.3.4", + "dexie": "^3.2.4", "uuid": "^9.0.0" } } diff --git a/packages/system/src/Connection.ts b/packages/system/src/Connection.ts index 0c2c0347..8a096ad3 100644 --- a/packages/system/src/Connection.ts +++ b/packages/system/src/Connection.ts @@ -1,11 +1,10 @@ import { v4 as uuid } from "uuid"; +import { unwrap, ExternalStore } from "@snort/shared"; import { DefaultConnectTimeout } from "./Const"; import { ConnectionStats } from "./ConnectionStats"; import { NostrEvent, ReqCommand, TaggedRawEvent, u256 } from "./Nostr"; import { RelayInfo } from "./RelayInfo"; -import { unwrap } from "./Utils"; -import ExternalStore from "./ExternalStore"; export type AuthHandler = (challenge: string, relay: string) => Promise; diff --git a/packages/system/src/Const.ts b/packages/system/src/Const.ts index f2e94a65..85168944 100644 --- a/packages/system/src/Const.ts +++ b/packages/system/src/Const.ts @@ -13,4 +13,4 @@ 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 +export const ProfileCacheExpire = 1_000 * 60 * 60 * 6; diff --git a/packages/system/src/EventBuilder.ts b/packages/system/src/EventBuilder.ts index a8c6099b..0aecd4bd 100644 --- a/packages/system/src/EventBuilder.ts +++ b/packages/system/src/EventBuilder.ts @@ -1,6 +1,6 @@ import { EventKind, HexKey, NostrPrefix, NostrEvent } from "."; import { HashtagRegex } from "./Const"; -import { getPublicKey, unixNow } from "./Utils"; +import { getPublicKey, unixNow } from "@snort/shared"; import { EventExt } from "./EventExt"; import { tryParseNostrLink } from "./NostrLink"; diff --git a/packages/system/src/EventExt.ts b/packages/system/src/EventExt.ts index e217557d..cad639db 100644 --- a/packages/system/src/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 { sha256, unixNow } from "@snort/shared"; import { EventKind, HexKey, NostrEvent } from "."; -import { sha256, unixNow } from "./Utils"; import { Nip4WebCryptoEncryptor } from "./impl/nip4"; export interface Tag { diff --git a/packages/system/src/EventPublisher.ts b/packages/system/src/EventPublisher.ts index a6d605e3..19d7475a 100644 --- a/packages/system/src/EventPublisher.ts +++ b/packages/system/src/EventPublisher.ts @@ -1,5 +1,7 @@ import * as secp from "@noble/curves/secp256k1"; import * as utils from "@noble/curves/abstract/utils"; +import { unwrap, barrierQueue, processWorkQueue, WorkQueueItem } from "@snort/shared"; + import { EventKind, FullRelaySettings, @@ -7,16 +9,13 @@ import { Lists, NostrEvent, RelaySettings, - SystemInterface, TaggedRawEvent, u256, UserMetadata, } from "."; -import { unwrap } from "./Utils"; import { EventBuilder } from "./EventBuilder"; import { EventExt } from "./EventExt"; -import { barrierQueue, processWorkQueue, WorkQueueItem } from "./WorkQueue"; const Nip7Queue: Array = []; processWorkQueue(Nip7Queue); @@ -39,12 +38,10 @@ declare global { } export class EventPublisher { - #system: SystemInterface; #pubKey: string; #privateKey?: string; - constructor(system: SystemInterface, pubKey: string, privKey?: string) { - this.#system = system; + constructor(pubKey: string, privKey?: string) { if (privKey) { this.#privateKey = privKey; this.#pubKey = utils.bytesToHex(secp.schnorr.getPublicKey(privKey)); @@ -57,6 +54,18 @@ export class EventPublisher { return "nostr" in window; } + /** + * Get a NIP-07 EventPublisher + */ + static async nip7() { + if("nostr" in window) { + const pubkey = await window.nostr?.getPublicKey(); + if(pubkey) { + return new EventPublisher(pubkey); + } + } + } + #eb(k: EventKind) { const eb = new EventBuilder(); return eb.pubKey(this.#pubKey).kind(k); @@ -112,20 +121,6 @@ export class EventPublisher { return await this.#sign(eb); } - broadcast(ev: NostrEvent) { - console.debug(ev); - this.#system.BroadcastEvent(ev); - } - - /** - * Write event to all given relays. - */ - broadcastAll(ev: NostrEvent, relays: string[]) { - for (const k of relays) { - this.#system.WriteOnceToRelay(k, ev); - } - } - async muted(keys: HexKey[], priv: HexKey[]) { const eb = this.#eb(EventKind.PubkeyLists); diff --git a/packages/system/src/GossipModel.ts b/packages/system/src/GossipModel.ts index a85748fa..bc6d7d6d 100644 --- a/packages/system/src/GossipModel.ts +++ b/packages/system/src/GossipModel.ts @@ -1,5 +1,5 @@ -import { FullRelaySettings, ReqFilter } from "."; -import { unwrap } from "./Utils"; +import { ReqFilter, UsersRelays } from "."; +import { unwrap } from "@snort/shared"; import debug from "debug"; const PickNRelays = 2; @@ -15,7 +15,7 @@ export interface RelayTaggedFilters { } export interface RelayCache { - get(pubkey?: string): Array | undefined; + getFromCache(pubkey?: string): UsersRelays | undefined; } export function splitAllByWriteRelays(cache: RelayCache, filters: Array) { @@ -59,7 +59,7 @@ export function splitByWriteRelays(cache: RelayCache, filter: ReqFilter): Array< const allRelays = unwrap(filter.authors).map(a => { return { key: a, - relays: cache.get(a)?.filter(a => a.settings.write).sort(() => Math.random() < 0.5 ? 1 : -1), + relays: cache.getFromCache(a)?.relays?.filter(a => a.settings.write).sort(() => Math.random() < 0.5 ? 1 : -1), }; }); diff --git a/packages/system/src/Links.ts b/packages/system/src/Links.ts index fd854032..7a0070bb 100644 --- a/packages/system/src/Links.ts +++ b/packages/system/src/Links.ts @@ -1,5 +1,5 @@ import * as utils from "@noble/curves/abstract/utils"; -import { bech32 } from "bech32"; +import { bech32 } from "@scure/base"; import { HexKey } from "./Nostr"; export enum NostrPrefix { @@ -43,7 +43,7 @@ export function encodeTLV(prefix: NostrPrefix, id: string, relays?: string[], ki const tl2 = author ? [2, 32, ...utils.hexToBytes(author)] : []; const tl3 = kind ? [3, 4, ...new Uint8Array(new Uint32Array([kind]).buffer).reverse()] : []; - return bech32.encode(prefix, bech32.toWords([...tl0, ...tl1, ...tl2, ...tl3]), 1_000); + return bech32.encode(prefix, bech32.toWords(new Uint8Array([...tl0, ...tl1, ...tl2, ...tl3])), 1_000); } export function decodeTLV(str: string) { diff --git a/packages/system/src/NostrLink.ts b/packages/system/src/NostrLink.ts index d866c279..39968eb5 100644 --- a/packages/system/src/NostrLink.ts +++ b/packages/system/src/NostrLink.ts @@ -1,4 +1,4 @@ -import { bech32ToHex, hexToBech32 } from "./Utils"; +import { bech32ToHex, hexToBech32 } from "@snort/shared"; import { NostrPrefix, decodeTLV, TLVEntryType } from "."; export interface NostrLink { diff --git a/packages/system/src/NostrSystem.ts b/packages/system/src/NostrSystem.ts index c5bb7758..c5a92657 100644 --- a/packages/system/src/NostrSystem.ts +++ b/packages/system/src/NostrSystem.ts @@ -1,19 +1,20 @@ import debug from "debug"; -import ExternalStore from "./ExternalStore"; +import { unwrap, sanitizeRelayUrl, ExternalStore } from "@snort/shared"; 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 { + #log = debug("System"); + /** * All currently connected websockets */ @@ -25,16 +26,19 @@ export class NostrSystem extends ExternalStore implements System Queries: Map = new Map(); /** - * Handler function for NIP-42 + * NIP-42 Auth handler */ - HandleAuth?: AuthHandler; + #handleAuth?: AuthHandler; - #log = debug("System"); + /** + * Storage class for user relay lists + */ #relayCache: RelayCache; - constructor(relayCache: RelayCache) { + constructor(props: { authHandler?: AuthHandler, relayCache: RelayCache }) { super(); - this.#relayCache = relayCache; + this.#handleAuth = props.authHandler; + this.#relayCache = props.relayCache; this.#cleanup(); } @@ -49,7 +53,7 @@ export class NostrSystem extends ExternalStore implements System try { const addr = unwrap(sanitizeRelayUrl(address)); if (!this.#sockets.has(addr)) { - const c = new Connection(addr, options, this.HandleAuth?.bind(this)); + 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); @@ -90,7 +94,7 @@ export class NostrSystem extends ExternalStore implements System 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); + 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); @@ -200,7 +204,7 @@ export class NostrSystem extends ExternalStore implements System */ async WriteOnceToRelay(address: string, ev: NostrEvent) { return new Promise((resolve, reject) => { - const c = new Connection(address, { write: true, read: false }, this.HandleAuth, true); + const c = new Connection(address, { write: true, read: false }, this.#handleAuth?.bind(this), true); const t = setTimeout(reject, 5_000); c.OnConnected = async () => { diff --git a/packages/system/src/NoteCollection.ts b/packages/system/src/NoteCollection.ts index 75fa8647..e8b82898 100644 --- a/packages/system/src/NoteCollection.ts +++ b/packages/system/src/NoteCollection.ts @@ -1,5 +1,6 @@ +import { appendDedupe } from "@snort/shared"; import { TaggedRawEvent, u256 } from "."; -import { appendDedupe, findTag } from "./Utils"; +import { findTag } from "./Utils"; export interface StoreSnapshot { data: TSnapshot | undefined; diff --git a/packages/system/src/ProfileCache.ts b/packages/system/src/ProfileCache.ts index 9c59972d..740036bc 100644 --- a/packages/system/src/ProfileCache.ts +++ b/packages/system/src/ProfileCache.ts @@ -1,12 +1,13 @@ + +import debug from "debug"; +import { unixNowMs, FeedCache } from "@snort/shared"; 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"; +import { mapEventToProfile, MetadataCache } from "./cache"; export class ProfileLoaderService { #system: SystemInterface; - #cache: CacheStore; + #cache: FeedCache; /** * List of pubkeys to fetch metadata for @@ -15,7 +16,7 @@ export class ProfileLoaderService { readonly #log = debug("ProfileCache"); - constructor(system: SystemInterface, cache: CacheStore) { + constructor(system: SystemInterface, cache: FeedCache) { this.#system = system; this.#cache = cache; this.#FetchMetadata(); diff --git a/packages/system/src/Query.ts b/packages/system/src/Query.ts index d7849654..2d5f0720 100644 --- a/packages/system/src/Query.ts +++ b/packages/system/src/Query.ts @@ -1,7 +1,9 @@ import { v4 as uuid } from "uuid"; import debug from "debug"; +import { unixNowMs, unwrap } from "@snort/shared"; + import { Connection, ReqFilter, Nips, TaggedRawEvent } from "."; -import { reqFilterEq, unixNowMs, unwrap } from "./Utils"; +import { reqFilterEq } from "./Utils"; import { NoteStore } from "./NoteCollection"; import { flatMerge } from "./RequestMerger"; import { BuiltRawReqFilter } from "./RequestBuilder"; diff --git a/packages/system/src/RequestBuilder.ts b/packages/system/src/RequestBuilder.ts index 84ff6043..01d728af 100644 --- a/packages/system/src/RequestBuilder.ts +++ b/packages/system/src/RequestBuilder.ts @@ -1,11 +1,12 @@ +import debug from "debug"; +import { v4 as uuid } from "uuid"; +import { appendDedupe, dedupe, unixNowMs } from "@snort/shared"; + 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"; -import { v4 as uuid } from "uuid"; /** * Which strategy is used when building REQ filters diff --git a/packages/system/src/RequestMerger.ts b/packages/system/src/RequestMerger.ts index b5a6556a..52990ad8 100644 --- a/packages/system/src/RequestMerger.ts +++ b/packages/system/src/RequestMerger.ts @@ -1,4 +1,4 @@ -import { distance } from "./Utils"; +import { distance } from "@snort/shared"; import { ReqFilter } from "."; import { FlatReqFilter } from "./RequestExpander"; diff --git a/packages/system/src/SystemWorker.ts b/packages/system/src/SystemWorker.ts index ab9dfaf3..431f594f 100644 --- a/packages/system/src/SystemWorker.ts +++ b/packages/system/src/SystemWorker.ts @@ -1,6 +1,7 @@ +import { ExternalStore } from "@snort/shared"; + 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"; diff --git a/packages/system/src/Utils.ts b/packages/system/src/Utils.ts index 68082c65..b5ec3b0b 100644 --- a/packages/system/src/Utils.ts +++ b/packages/system/src/Utils.ts @@ -1,179 +1,42 @@ -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; -} +import { equalProp } from "@snort/shared"; +import { FlatReqFilter } from "./RequestExpander"; +import { NostrEvent, ReqFilter } from "./Nostr"; -/** - * 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 findTag(e: NostrEvent, tag: string) { + const maybeTag = e.tags.find(evTag => { + return evTag[0] === tag; + }); + return maybeTag && maybeTag[1]; } 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"]); + 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.keys === b.keys - && a.since === b.since - && a.until === b.until - && a.limit === b.limit - && a.search === b.search - && a.ids === b.ids - && a.kinds === b.kinds - && a.authors === b.authors - && a["#e"] === b["#e"] - && a["#p"] === b["#p"] - && a["#t"] === b["#t"] - && a["#d"] === b["#d"] - && a["#r"] === b["#r"]; -} - -export function countMembers(a: any) { - let ret = 0; - for (const [k, v] of Object.entries(a)) { - if (Array.isArray(v)) { - ret += v.length; - } - } - return ret; -} - -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; - } -} + return a.keys === b.keys + && a.since === b.since + && a.until === b.until + && a.limit === b.limit + && a.search === b.search + && a.ids === b.ids + && a.kinds === b.kinds + && a.authors === b.authors + && a["#e"] === b["#e"] + && a["#p"] === b["#p"] + && a["#t"] === b["#t"] + && a["#d"] === b["#d"] + && a["#r"] === b["#r"]; +} \ No newline at end of file diff --git a/packages/system/src/WorkQueue.ts b/packages/system/src/WorkQueue.ts deleted file mode 100644 index 37df22e0..00000000 --- a/packages/system/src/WorkQueue.ts +++ /dev/null @@ -1,30 +0,0 @@ -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/app/src/Cache/UserCache.ts b/packages/system/src/cache/UserCache.ts similarity index 91% rename from packages/app/src/Cache/UserCache.ts rename to packages/system/src/cache/UserCache.ts index 1b9ce04b..df1aa063 100644 --- a/packages/app/src/Cache/UserCache.ts +++ b/packages/system/src/cache/UserCache.ts @@ -1,10 +1,7 @@ -import FeedCache from "Cache/FeedCache"; -import { db } from "Db"; -import { MetadataCache } from "@snort/system"; -import { LNURL } from "LNURL"; -import { fetchNip05Pubkey } from "Nip05/Verifier"; +import { db, MetadataCache } from "."; +import { fetchNip05Pubkey, FeedCache, LNURL } from "@snort/shared"; -class UserProfileCache extends FeedCache { +export class UserProfileCache extends FeedCache { #zapperQueue: Array<{ pubkey: string; lnurl: string }> = []; #nip5Queue: Array<{ pubkey: string; nip05: string }> = []; @@ -148,6 +145,4 @@ class UserProfileCache extends FeedCache { } } } -} - -export const UserCache = new UserProfileCache(); +} \ No newline at end of file diff --git a/packages/app/src/Cache/UserRelayCache.ts b/packages/system/src/cache/UserRelayCache.ts similarity index 72% rename from packages/app/src/Cache/UserRelayCache.ts rename to packages/system/src/cache/UserRelayCache.ts index c0c9fb5a..24bed929 100644 --- a/packages/app/src/Cache/UserRelayCache.ts +++ b/packages/system/src/cache/UserRelayCache.ts @@ -1,7 +1,7 @@ -import { db, UsersRelays } from "Db"; -import FeedCache from "./FeedCache"; +import { db, UsersRelays } from "."; +import { FeedCache } from "@snort/shared"; -export class UsersRelaysCache extends FeedCache { +export class UserRelaysCache extends FeedCache { constructor() { super("UserRelays", db.userRelays); } @@ -26,6 +26,4 @@ export class UsersRelaysCache extends FeedCache { takeSnapshot(): Array { return [...this.cache.values()]; } -} - -export const UserRelays = new UsersRelaysCache(); +} \ No newline at end of file diff --git a/packages/system/src/cache/db.ts b/packages/system/src/cache/db.ts new file mode 100644 index 00000000..31b2e021 --- /dev/null +++ b/packages/system/src/cache/db.ts @@ -0,0 +1,42 @@ +import { MetadataCache, RelayMetrics, UsersRelays } from "."; +import { NostrEvent } from "../Nostr"; +import Dexie, { Table } from "dexie"; + +const NAME = "snort-system"; +const VERSION = 1; + +const STORES = { + users: "++pubkey, name, display_name, picture, nip05, npub", + relays: "++addr", + userRelays: "++pubkey", + events: "++id, pubkey, created_at" +}; + +export class SnortSystemDb extends Dexie { + ready = false; + users!: Table; + relayMetrics!: Table; + userRelays!: Table; + events!: Table; + dms!: Table; + + constructor() { + super(NAME); + this.version(VERSION).stores(STORES); + } + + isAvailable() { + if ("indexedDB" in window) { + return new Promise(resolve => { + const req = window.indexedDB.open("dummy", 1); + req.onsuccess = () => { + resolve(true); + }; + req.onerror = () => { + resolve(false); + }; + }); + } + return Promise.resolve(false); + } +} \ No newline at end of file diff --git a/packages/system/src/cache/index.ts b/packages/system/src/cache/index.ts index 408c1ab7..42bd6bf3 100644 --- a/packages/system/src/cache/index.ts +++ b/packages/system/src/cache/index.ts @@ -1,5 +1,8 @@ -import { HexKey, NostrEvent, UserMetadata } from ".."; -import { hexToBech32, unixNowMs } from "../Utils"; +import { FullRelaySettings, HexKey, NostrEvent, UserMetadata } from ".."; +import { hexToBech32, unixNowMs } from "@snort/shared"; +import { SnortSystemDb } from "./db"; + +export const db = new SnortSystemDb(); export interface MetadataCache extends UserMetadata { /** @@ -33,6 +36,19 @@ export interface MetadataCache extends UserMetadata { isNostrAddressValid: boolean; } +export interface RelayMetrics { + addr: string; + events: number; + disconnects: number; + latency: number[]; +} + +export interface UsersRelays { + pubkey: string; + created_at: number; + relays: FullRelaySettings[]; +} + export function mapEventToProfile(ev: NostrEvent) { try { const data: UserMetadata = JSON.parse(ev.content); @@ -54,23 +70,4 @@ export function mapEventToProfile(ev: NostrEvent) { } 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; -} +} \ No newline at end of file diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts index 3c9c7134..0afd5134 100644 --- a/packages/system/src/index.ts +++ b/packages/system/src/index.ts @@ -17,11 +17,15 @@ export * from "./RequestBuilder"; export * from "./EventPublisher"; export * from "./EventBuilder"; export * from "./NostrLink"; -export * from "./cache"; export * from "./ProfileCache"; + export * from "./impl/nip4"; export * from "./impl/nip44"; +export * from "./cache"; +export * from "./cache/UserRelayCache"; +export * from "./cache/UserCache"; + export interface SystemInterface { /** * Handler function for NIP-42 diff --git a/yarn.lock b/yarn.lock index 2c0c35df..aae381b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1781,11 +1781,23 @@ dependencies: "@noble/hashes" "1.3.0" +"@noble/curves@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" + integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== + dependencies: + "@noble/hashes" "1.3.1" + "@noble/hashes@1.3.0", "@noble/hashes@^1.2.0", "@noble/hashes@~1.3.0": version "1.3.0" resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz" integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== +"@noble/hashes@1.3.1", "@noble/hashes@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" + integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== + "@noble/secp256k1@^1.7.0": version "1.7.1" resolved "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz" @@ -2091,11 +2103,6 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== -"@protobufjs/base64@^1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz" - integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== - "@reduxjs/toolkit@^1.9.1": version "1.9.3" resolved "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz" @@ -3616,11 +3623,6 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base32-decode@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/base32-decode/-/base32-decode-1.0.0.tgz" - integrity sha512-KNWUX/R7wKenwE/G/qFMzGScOgVntOmbE27vvc6GrniDGYb6a5+qWcuoXl8WIOQL7q0TpK7nZDm1Y04Yi3Yn5g== - base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" @@ -4644,15 +4646,10 @@ detect-node@^2.0.4: resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== -dexie-react-hooks@^1.1.1: - version "1.1.3" - resolved "https://registry.npmjs.org/dexie-react-hooks/-/dexie-react-hooks-1.1.3.tgz" - integrity sha512-bXXE1gfYtfuVYTNiOlyam+YVaO8KaqacgRuxFuP37YtpS6o/jxT6KOl5h+hhqY36s0UavlHWbL+HWJFMcQumIg== - -dexie@^3.2.2: - version "3.2.3" - resolved "https://registry.npmjs.org/dexie/-/dexie-3.2.3.tgz" - integrity sha512-iHayBd4UYryDCVUNa3PMsJMEnd8yjyh5p7a+RFeC8i8n476BC9wMhVvqiImq5zJZJf5Tuer+s4SSj+AA3x+ZbQ== +dexie@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.4.tgz#b22a9729be1102acb2eee16102ea6e2bc76454cf" + integrity sha512-VKoTQRSv7+RnffpOJ3Dh6ozknBqzWw/F3iqMdsZg958R0AS8AnY9x9d1lbwENr0gzeGJHXKcGhAMRaqys6SxqA== dezalgo@^1.0.0: version "1.0.4" @@ -10633,7 +10630,7 @@ word-wrap@^1.2.3, word-wrap@~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== -workbox-background-sync@6.5.4, workbox-background-sync@^6.4.2: +workbox-background-sync@6.5.4: version "6.5.4" resolved "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz" integrity sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g== @@ -10641,7 +10638,7 @@ workbox-background-sync@6.5.4, workbox-background-sync@^6.4.2: idb "^7.0.1" workbox-core "6.5.4" -workbox-broadcast-update@6.5.4, workbox-broadcast-update@^6.4.2: +workbox-broadcast-update@6.5.4: version "6.5.4" resolved "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.5.4.tgz" integrity sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw== @@ -10691,7 +10688,7 @@ workbox-build@6.5.4: workbox-sw "6.5.4" workbox-window "6.5.4" -workbox-cacheable-response@6.5.4, workbox-cacheable-response@^6.4.2: +workbox-cacheable-response@6.5.4: version "6.5.4" resolved "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.5.4.tgz" integrity sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug== @@ -10703,7 +10700,7 @@ workbox-core@6.5.4, workbox-core@^6.4.2: resolved "https://registry.npmjs.org/workbox-core/-/workbox-core-6.5.4.tgz" integrity sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q== -workbox-expiration@6.5.4, workbox-expiration@^6.4.2: +workbox-expiration@6.5.4: version "6.5.4" resolved "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.5.4.tgz" integrity sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ== @@ -10711,7 +10708,7 @@ workbox-expiration@6.5.4, workbox-expiration@^6.4.2: idb "^7.0.1" workbox-core "6.5.4" -workbox-google-analytics@6.5.4, workbox-google-analytics@^6.4.2: +workbox-google-analytics@6.5.4: version "6.5.4" resolved "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.5.4.tgz" integrity sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg== @@ -10721,14 +10718,14 @@ workbox-google-analytics@6.5.4, workbox-google-analytics@^6.4.2: workbox-routing "6.5.4" workbox-strategies "6.5.4" -workbox-navigation-preload@6.5.4, workbox-navigation-preload@^6.4.2: +workbox-navigation-preload@6.5.4: version "6.5.4" resolved "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.5.4.tgz" integrity sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng== dependencies: workbox-core "6.5.4" -workbox-precaching@6.5.4, workbox-precaching@^6.4.2: +workbox-precaching@6.5.4: version "6.5.4" resolved "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.5.4.tgz" integrity sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg== @@ -10737,7 +10734,7 @@ workbox-precaching@6.5.4, workbox-precaching@^6.4.2: workbox-routing "6.5.4" workbox-strategies "6.5.4" -workbox-range-requests@6.5.4, workbox-range-requests@^6.4.2: +workbox-range-requests@6.5.4: version "6.5.4" resolved "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.5.4.tgz" integrity sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg== @@ -10770,7 +10767,7 @@ workbox-strategies@6.5.4, workbox-strategies@^6.4.2: dependencies: workbox-core "6.5.4" -workbox-streams@6.5.4, workbox-streams@^6.4.2: +workbox-streams@6.5.4: version "6.5.4" resolved "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.5.4.tgz" integrity sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==