use @snort/system cache
This commit is contained in:
parent
c2a3a706de
commit
fc11381ccd
@ -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",
|
||||
|
@ -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<NostrEvent> {
|
||||
constructor() {
|
||||
|
@ -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<EventInteraction> {
|
||||
constructor() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Payment, db } from "Db";
|
||||
import FeedCache from "./FeedCache";
|
||||
import { FeedCache } from "@snort/shared";
|
||||
|
||||
class Payments extends FeedCache<Payment> {
|
||||
constructor() {
|
||||
|
@ -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<string>) {
|
||||
const preloads = [
|
||||
@ -12,5 +15,3 @@ export async function preload(follows?: Array<string>) {
|
||||
];
|
||||
await Promise.all(preloads);
|
||||
}
|
||||
|
||||
export { UserCache, DmCache };
|
||||
|
@ -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<MetadataCache>;
|
||||
relayMetrics!: Table<RelayMetrics>;
|
||||
userRelays!: Table<UsersRelays>;
|
||||
events!: Table<NostrEvent>;
|
||||
dms!: Table<NostrEvent>;
|
||||
eventInteraction!: Table<EventInteraction>;
|
||||
payments!: Table<Payment>;
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -1,41 +0,0 @@
|
||||
type HookFn<TSnapshot> = (e?: TSnapshot) => void;
|
||||
|
||||
interface HookFilter<TSnapshot> {
|
||||
fn: HookFn<TSnapshot>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple React hookable store with manual change notifications
|
||||
*/
|
||||
export default abstract class ExternalStore<TSnapshot> {
|
||||
#hooks: Array<HookFilter<TSnapshot>> = [];
|
||||
#snapshot: Readonly<TSnapshot> = {} as Readonly<TSnapshot>;
|
||||
#changed = true;
|
||||
|
||||
hook(fn: HookFn<TSnapshot>) {
|
||||
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;
|
||||
}
|
@ -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]);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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}`;
|
||||
|
@ -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;
|
||||
|
@ -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<MetadataCache | undefined>(
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 () => {
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 }) {
|
||||
|
@ -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()];
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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<WorkQueueItem> = [];
|
||||
processWorkQueue(WebLNQueue);
|
||||
|
@ -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";
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -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 })],
|
||||
})
|
||||
);
|
||||
|
||||
|
21
packages/shared/package.json
Normal file
21
packages/shared/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
8
packages/shared/src/const.ts
Normal file
8
packages/shared/src/const.ts
Normal file
@ -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,}))$/;
|
||||
|
@ -1,13 +1,13 @@
|
||||
type HookFn<TSnapshot> = (e?: TSnapshot) => void;
|
||||
export type HookFn<TSnapshot> = (e?: TSnapshot) => void;
|
||||
|
||||
interface HookFilter<TSnapshot> {
|
||||
export interface HookFilter<TSnapshot> {
|
||||
fn: HookFn<TSnapshot>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple React hookable store with manual change notifications
|
||||
*/
|
||||
export default abstract class ExternalStore<TSnapshot> {
|
||||
export abstract class ExternalStore<TSnapshot> {
|
||||
#hooks: Array<HookFilter<TSnapshot>> = [];
|
||||
#snapshot: Readonly<TSnapshot> = {} as Readonly<TSnapshot>;
|
||||
#changed = true;
|
@ -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<TCached> {
|
||||
/**
|
||||
* Dexie backed generic hookable store
|
||||
*/
|
||||
export abstract class FeedCache<TCached> {
|
||||
#name: string;
|
||||
#hooks: Array<HookFilter> = [];
|
||||
#hooks: Array<KeyedHookFilter> = [];
|
||||
#snapshot: Readonly<Array<TCached>> = [];
|
||||
#changed = true;
|
||||
#hits = 0;
|
||||
#miss = 0;
|
||||
protected table: Table<TCached>;
|
||||
protected table?: Table<TCached>;
|
||||
protected onTable: Set<string> = new Set();
|
||||
protected cache: Map<string, TCached> = new Map();
|
||||
|
||||
constructor(name: string, table: Table<TCached>) {
|
||||
constructor(name: string, table?: Table<TCached>) {
|
||||
this.#name = name;
|
||||
this.table = table;
|
||||
setInterval(() => {
|
||||
@ -36,11 +38,9 @@ export default abstract class FeedCache<TCached> {
|
||||
}
|
||||
|
||||
async preload() {
|
||||
if (db.ready) {
|
||||
const keys = await this.table.toCollection().primaryKeys();
|
||||
const keys = await this.table?.toCollection().primaryKeys() ?? [];
|
||||
this.onTable = new Set<string>(keys.map(a => a as string));
|
||||
}
|
||||
}
|
||||
|
||||
hook(fn: HookFn, key: string | undefined) {
|
||||
if (!key) {
|
||||
@ -74,7 +74,7 @@ export default abstract class FeedCache<TCached> {
|
||||
}
|
||||
|
||||
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<TCached> {
|
||||
|
||||
async bulkGet(keys: Array<string>) {
|
||||
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<TCached> {
|
||||
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<TCached> {
|
||||
}
|
||||
|
||||
async bulkSet(obj: Array<TCached>) {
|
||||
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<TCached> {
|
||||
*/
|
||||
async buffer(keys: Array<string>): Promise<Array<string>> {
|
||||
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<TCached> {
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await this.table.clear();
|
||||
await this.table?.clear();
|
||||
this.cache.clear();
|
||||
this.onTable.clear();
|
||||
}
|
5
packages/shared/src/index.ts
Normal file
5
packages/shared/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./external-store";
|
||||
export * from "./lnurl";
|
||||
export * from "./utils";
|
||||
export * from "./work-queue";
|
||||
export * from "./feed-cache";
|
@ -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<string, string>();
|
||||
|
||||
@ -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
|
||||
}
|
184
packages/shared/src/utils.ts
Normal file
184
packages/shared/src/utils.ts
Normal file
@ -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<T>(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<T>(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<string | number> | undefined, b: string | number | Array<string | number> | 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<string | number>;
|
||||
const bb = b[key] as Array<string | number>;
|
||||
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<T>(v: Array<T>) {
|
||||
return [...new Set(v)];
|
||||
}
|
||||
|
||||
export function appendDedupe<T>(a?: Array<T>, b?: Array<T>) {
|
||||
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<string, string>;
|
||||
}
|
||||
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;
|
||||
}
|
18
packages/shared/tsconfig.json
Normal file
18
packages/shared/tsconfig.json
Normal file
@ -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"]
|
||||
}
|
66
packages/system/README.md
Normal file
66
packages/system/README.md
Normal file
@ -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>(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<ReturnType<FlatNoteStore["getSnapshotData"]>>;
|
||||
|
||||
// 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();
|
||||
})();
|
||||
```
|
52
packages/system/examples/simple.ts
Normal file
52
packages/system/examples/simple.ts
Normal file
@ -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>(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<ReturnType<FlatNoteStore["getSnapshotData"]>>;
|
||||
|
||||
// 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();
|
||||
})();
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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<NostrEvent | undefined>;
|
||||
|
||||
|
@ -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;
|
||||
export const ProfileCacheExpire = 1_000 * 60 * 60 * 6;
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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<WorkQueueItem> = [];
|
||||
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);
|
||||
|
||||
|
@ -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<FullRelaySettings> | undefined;
|
||||
getFromCache(pubkey?: string): UsersRelays | undefined;
|
||||
}
|
||||
|
||||
export function splitAllByWriteRelays(cache: RelayCache, filters: Array<ReqFilter>) {
|
||||
@ -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),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { bech32ToHex, hexToBech32 } from "./Utils";
|
||||
import { bech32ToHex, hexToBech32 } from "@snort/shared";
|
||||
import { NostrPrefix, decodeTLV, TLVEntryType } from ".";
|
||||
|
||||
export interface NostrLink {
|
||||
|
@ -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<SystemSnapshot> implements SystemInterface {
|
||||
#log = debug("System");
|
||||
|
||||
/**
|
||||
* All currently connected websockets
|
||||
*/
|
||||
@ -25,16 +26,19 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
|
||||
Queries: Map<string, Query> = 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<SystemSnapshot> 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<SystemSnapshot> 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<SystemSnapshot> implements System
|
||||
*/
|
||||
async WriteOnceToRelay(address: string, ev: NostrEvent) {
|
||||
return new Promise<void>((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 () => {
|
||||
|
@ -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<TSnapshot> {
|
||||
data: TSnapshot | undefined;
|
||||
|
@ -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<MetadataCache>;
|
||||
#cache: FeedCache<MetadataCache>;
|
||||
|
||||
/**
|
||||
* List of pubkeys to fetch metadata for
|
||||
@ -15,7 +16,7 @@ export class ProfileLoaderService {
|
||||
|
||||
readonly #log = debug("ProfileCache");
|
||||
|
||||
constructor(system: SystemInterface, cache: CacheStore<MetadataCache>) {
|
||||
constructor(system: SystemInterface, cache: FeedCache<MetadataCache>) {
|
||||
this.#system = system;
|
||||
this.#cache = cache;
|
||||
this.#FetchMetadata();
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { distance } from "./Utils";
|
||||
import { distance } from "@snort/shared";
|
||||
import { ReqFilter } from ".";
|
||||
import { FlatReqFilter } from "./RequestExpander";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -1,58 +1,13 @@
|
||||
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<T>(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 {
|
||||
@ -85,95 +40,3 @@ export function flatFilterEq(a: FlatReqFilter, b: FlatReqFilter): boolean {
|
||||
&& 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<string | number> | undefined, b: string | number | Array<string | number> | 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<string | number>;
|
||||
const bb = b[key] as Array<string | number>;
|
||||
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<T>(v: Array<T>) {
|
||||
return [...new Set(v)];
|
||||
}
|
||||
|
||||
export function appendDedupe<T>(a?: Array<T>, b?: Array<T>) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
export interface WorkQueueItem {
|
||||
next: () => Promise<unknown>;
|
||||
resolve(v: unknown): void;
|
||||
reject(e: unknown): void;
|
||||
}
|
||||
|
||||
export async function processWorkQueue(queue?: Array<WorkQueueItem>, 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 <T>(queue: Array<WorkQueueItem>, then: () => Promise<T>): Promise<T> => {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
queue.push({
|
||||
next: then,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
});
|
||||
};
|
@ -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<MetadataCache> {
|
||||
export class UserProfileCache extends FeedCache<MetadataCache> {
|
||||
#zapperQueue: Array<{ pubkey: string; lnurl: string }> = [];
|
||||
#nip5Queue: Array<{ pubkey: string; nip05: string }> = [];
|
||||
|
||||
@ -149,5 +146,3 @@ class UserProfileCache extends FeedCache<MetadataCache> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const UserCache = new UserProfileCache();
|
@ -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<UsersRelays> {
|
||||
export class UserRelaysCache extends FeedCache<UsersRelays> {
|
||||
constructor() {
|
||||
super("UserRelays", db.userRelays);
|
||||
}
|
||||
@ -27,5 +27,3 @@ export class UsersRelaysCache extends FeedCache<UsersRelays> {
|
||||
return [...this.cache.values()];
|
||||
}
|
||||
}
|
||||
|
||||
export const UserRelays = new UsersRelaysCache();
|
42
packages/system/src/cache/db.ts
vendored
Normal file
42
packages/system/src/cache/db.ts
vendored
Normal file
@ -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<MetadataCache>;
|
||||
relayMetrics!: Table<RelayMetrics>;
|
||||
userRelays!: Table<UsersRelays>;
|
||||
events!: Table<NostrEvent>;
|
||||
dms!: Table<NostrEvent>;
|
||||
|
||||
constructor() {
|
||||
super(NAME);
|
||||
this.version(VERSION).stores(STORES);
|
||||
}
|
||||
|
||||
isAvailable() {
|
||||
if ("indexedDB" in window) {
|
||||
return new Promise<boolean>(resolve => {
|
||||
const req = window.indexedDB.open("dummy", 1);
|
||||
req.onsuccess = () => {
|
||||
resolve(true);
|
||||
};
|
||||
req.onerror = () => {
|
||||
resolve(false);
|
||||
};
|
||||
});
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
39
packages/system/src/cache/index.ts
vendored
39
packages/system/src/cache/index.ts
vendored
@ -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);
|
||||
@ -55,22 +71,3 @@ export function mapEventToProfile(ev: NostrEvent) {
|
||||
console.error("Failed to parse JSON", ev, e);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CacheStore<T> {
|
||||
preload(): Promise<void>;
|
||||
getFromCache(key?: string): T | undefined;
|
||||
get(key?: string): Promise<T | undefined>;
|
||||
bulkGet(keys: Array<string>): Promise<Array<T>>;
|
||||
set(obj: T): Promise<void>;
|
||||
bulkSet(obj: Array<T>): Promise<void>;
|
||||
update<TCachedWithCreated extends T & { created: number; loaded: number }>(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<string>): Promise<Array<string>>;
|
||||
|
||||
clear(): Promise<void>;
|
||||
}
|
||||
|
@ -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
|
||||
|
53
yarn.lock
53
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==
|
||||
|
Loading…
x
Reference in New Issue
Block a user