add local relay cache option

cleanup event relay hint methods
This commit is contained in:
hzrd149 2023-12-02 12:00:58 -06:00
parent 9069932fbe
commit 199f208b11
29 changed files with 294 additions and 107 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add local relay cache option

View File

@ -29,10 +29,14 @@ export default class NostrMultiSubscription {
this.id = nanoid();
this.name = name;
}
private handleEvent(event: IncomingEvent) {
if (this.state === NostrMultiSubscription.OPEN && event.subId === this.id && !this.seenEvents.has(event.body.id)) {
this.onEvent.next(event.body);
this.seenEvents.add(event.body.id);
private handleEvent(incomingEvent: IncomingEvent) {
if (
this.state === NostrMultiSubscription.OPEN &&
incomingEvent.subId === this.id &&
!this.seenEvents.has(incomingEvent.body.id)
) {
this.onEvent.next(incomingEvent.body);
this.seenEvents.add(incomingEvent.body.id);
}
}

View File

@ -1,9 +1,10 @@
import { nanoid } from "nanoid";
import { NostrEvent } from "../types/nostr-event";
import { NostrOutgoingMessage, NostrRequestFilter } from "../types/nostr-query";
import Relay, { IncomingEOSE } from "./relay";
import relayPoolService from "../services/relay-pool";
import { Subject } from "./subject";
import { nanoid } from "nanoid";
export default class NostrSubscription {
static INIT = "initial";
@ -26,7 +27,9 @@ export default class NostrSubscription {
this.relay = relayPoolService.requestRelay(relayUrl);
this.onEvent.connectWithHandler(this.relay.onEvent, (event, next) => {
if (this.state === NostrSubscription.OPEN) next(event.body);
if (this.state === NostrSubscription.OPEN) {
next(event.body);
}
});
this.onEOSE.connectWithHandler(this.relay.onEOSE, (eose, next) => {
if (this.state === NostrSubscription.OPEN) next(eose);

View File

@ -9,7 +9,7 @@ import appSettings from "../../../services/settings/app-settings";
import EventVerificationIcon from "../../event-verification-icon";
import { TrustProvider } from "../../../providers/trust";
import Timestamp from "../../timestamp";
import { getNeventCodeWithRelays } from "../../../helpers/nip19";
import { getNeventForEventId } from "../../../helpers/nip19";
import { CompactNoteContent } from "../../compact-note-content";
import HoverLinkOverlay from "../../hover-link-overlay";
import { getReferences } from "../../../helpers/nostr/events";
@ -26,7 +26,7 @@ export default function EmbeddedTorrentComment({
const { showSignatureVerification } = useSubject(appSettings);
const refs = getReferences(comment);
const torrent = useSingleEvent(refs.rootId, refs.rootRelay ? [refs.rootRelay] : []);
const linkToTorrent = refs.rootId && `/torrents/${getNeventCodeWithRelays(refs.rootId)}`;
const linkToTorrent = refs.rootId && `/torrents/${getNeventForEventId(refs.rootId)}`;
const handleClick = useCallback<MouseEventHandler>(
(e) => {

View File

@ -16,7 +16,7 @@ import {
} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { getNeventCodeWithRelays } from "../../../helpers/nip19";
import { getSharableEventAddress } from "../../../helpers/nip19";
import UserAvatarLink from "../../user-avatar-link";
import { UserLink } from "../../user-link";
import { NostrEvent } from "../../../types/nostr-event";
@ -29,7 +29,7 @@ import HoverLinkOverlay from "../../hover-link-overlay";
export default function EmbeddedTorrent({ torrent, ...props }: Omit<CardProps, "children"> & { torrent: NostrEvent }) {
const navigate = useNavigateInDrawer();
const link = `/torrents/${getNeventCodeWithRelays(torrent.id)}`;
const link = `/torrents/${getSharableEventAddress(torrent)}`;
const handleClick = useCallback<MouseEventHandler>(
(e) => {

View File

@ -29,6 +29,7 @@ import accountService from "../../services/account";
import PayStep from "./pay-step";
import { getInvoiceFromCallbackUrl } from "../../helpers/lnurl";
import { UserLink } from "../user-link";
import relayHintService from "../../services/event-relay-hint";
export type PayRequest = { invoice?: string; pubkey: string; error?: any };
@ -69,7 +70,7 @@ async function getPayRequestForPubkey(
.map((r) => r.url) ?? [],
)
.slice(0, 4);
const eventRelays = event ? relayScoreboardService.getRankedRelays(getEventRelays(event.id).value).slice(0, 4) : [];
const eventRelays = event ? relayHintService.getEventRelayHints(event, 4) : [];
const outbox = relayScoreboardService.getRankedRelays(clientRelaysService.getWriteUrls()).slice(0, 4);
const additional = relayScoreboardService.getRankedRelays(additionalRelays);

View File

@ -3,14 +3,14 @@ import { Link, LinkProps } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { truncatedId } from "../helpers/nostr/events";
import { getNeventCodeWithRelays } from "../helpers/nip19";
import { getNeventForEventId } from "../helpers/nip19";
export type NoteLinkProps = LinkProps & {
noteId: string;
};
export const NoteLink = ({ children, noteId, color = "blue.500", ...props }: NoteLinkProps) => {
const nevent = useMemo(() => getNeventCodeWithRelays(noteId), [noteId]);
const nevent = useMemo(() => getNeventForEventId(noteId), [noteId]);
return (
<Link as={RouterLink} to={`/n/${nevent}`} color={color} {...props}>

View File

@ -13,28 +13,25 @@ import {
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import dayjs from "dayjs";
import type { AddressPointer } from "nostr-tools/lib/types/nip19";
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
import { EmbedEvent } from "../../embed-event";
import { getEventRelays } from "../../../services/event-relays";
import relayScoreboardService from "../../../services/relay-scoreboard";
import { Kind } from "nostr-tools";
import dayjs from "dayjs";
import NostrPublishAction from "../../../classes/nostr-publish-action";
import clientRelaysService from "../../../services/client-relays";
import { useSigningContext } from "../../../providers/signing-provider";
import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from "../../icons";
import useUserCommunitiesList from "../../../hooks/use-user-communities-list";
import useCurrentAccount from "../../../hooks/use-current-account";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import { createCoordinate } from "../../../services/replaceable-event-requester";
import relayHintService from "../../../services/event-relay-hint";
function buildRepost(event: NostrEvent): DraftNostrEvent {
const relays = getEventRelays(event.id).value;
const topRelay = relayScoreboardService.getRankedRelays(relays)[0] ?? "";
const hint = relayHintService.getEventRelayHint(event);
const tags: NostrEvent["tags"] = [];
tags.push(["e", event.id, topRelay]);
tags.push(["e", event.id, hint ?? ""]);
return {
kind: Kind.Repost,
@ -65,6 +62,7 @@ export default function RepostModal({
draftRepost.tags.push([
"a",
createCoordinate(communityPointer.kind, communityPointer.pubkey, communityPointer.identifier),
relayHintService.getAddressPointerRelayHint(communityPointer) ?? "",
]);
}
const signed = await requestSignature(draftRepost);

View File

@ -1,10 +1,9 @@
import { bech32 } from "bech32";
import { getPublicKey, nip19 } from "nostr-tools";
import { getEventRelays } from "../services/event-relays";
import relayScoreboardService from "../services/relay-scoreboard";
import { NostrEvent, Tag, isATag, isDTag, isETag, isPTag } from "../types/nostr-event";
import { getEventUID, isReplaceable } from "./nostr/events";
import { isReplaceable } from "./nostr/events";
import { DecodeResult } from "nostr-tools/lib/types/nip19";
import relayHintService from "../services/event-relay-hint";
export function isHexKey(key?: string) {
if (key?.toLowerCase()?.match(/^[0-9a-f]{64}$/)) return true;
@ -47,7 +46,7 @@ export function safeDecode(str: string) {
} catch (e) {}
}
export function getPubkey(result?: nip19.DecodeResult) {
export function getPubkeyFromDecodeResult(result?: nip19.DecodeResult) {
if (!result) return;
switch (result.type) {
case "naddr":
@ -68,26 +67,21 @@ export function normalizeToHex(hex: string) {
}
export function getSharableEventAddress(event: NostrEvent) {
const relays = getEventRelays(getEventUID(event)).value;
const ranked = relayScoreboardService.getRankedRelays(relays);
const maxTwo = ranked.slice(0, 2);
const relays = relayHintService.getEventRelayHints(event, 2);
if (isReplaceable(event.kind)) {
const d = event.tags.find(isDTag)?.[1];
if (!d) return null;
return nip19.naddrEncode({ kind: event.kind, identifier: d, pubkey: event.pubkey, relays: maxTwo });
return nip19.naddrEncode({ kind: event.kind, identifier: d, pubkey: event.pubkey, relays });
} else {
if (maxTwo.length == 2) {
return nip19.neventEncode({ id: event.id, kind: event.kind, relays: maxTwo });
} else return nip19.neventEncode({ id: event.id, kind: event.kind, relays: maxTwo, author: event.pubkey });
return nip19.neventEncode({ id: event.id, kind: event.kind, relays, author: event.pubkey });
}
}
export function getNeventCodeWithRelays(eventId: string) {
const relays = getEventRelays(eventId).value;
const ranked = relayScoreboardService.getRankedRelays(relays);
const maxTwo = ranked.slice(0, 2);
return nip19.neventEncode({ id: eventId, relays: maxTwo });
/** @deprecated use getSharableEventAddress unless required */
export function getNeventForEventId(eventId: string, maxRelays = 2) {
const relays = relayHintService.getEventPointerRelayHints(eventId).slice(0, maxRelays);
return nip19.neventEncode({ id: eventId, relays });
}
export function encodePointer(pointer: DecodeResult) {

View File

@ -193,30 +193,6 @@ export function parseCoordinate(a: string, requireD = false): CustomEventPointer
};
}
export function draftAddCoordinate(list: NostrEvent | DraftNostrEvent, coordinate: string, relay?: string) {
if (list.tags.some((t) => t[0] === "a" && t[1] === coordinate)) throw new Error("event already in list");
const draft: DraftNostrEvent = {
created_at: dayjs().unix(),
kind: list.kind,
content: list.content,
tags: [...list.tags, relay ? ["a", coordinate, relay] : ["a", coordinate]],
};
return draft;
}
export function draftRemoveCoordinate(list: NostrEvent | DraftNostrEvent, coordinate: string) {
const draft: DraftNostrEvent = {
created_at: dayjs().unix(),
kind: list.kind,
content: list.content,
tags: list.tags.filter((t) => !(t[0] === "a" && t[1] === coordinate)),
};
return draft;
}
export function parseHardcodedNoteContent(event: NostrEvent) {
const json = safeJson(event.content, null);
if (!json) return null;

View File

@ -1,5 +1,6 @@
import stringify from "json-stringify-deterministic";
import { NostrQuery, NostrRequestFilter, RelayQueryMap } from "../../types/nostr-query";
import localCacheRelayService, { LOCAL_CACHE_RELAY } from "../../services/local-cache-relay";
export function addQueryToFilter(filter: NostrRequestFilter, query: NostrQuery) {
if (Array.isArray(filter)) {
@ -20,6 +21,13 @@ export function mapQueryMap(queryMap: RelayQueryMap, fn: (filter: NostrRequestFi
export function createSimpleQueryMap(relays: string[], filter: NostrRequestFilter) {
const map: RelayQueryMap = {};
// if the local cache relay is enabled, also ask it
if (localCacheRelayService.enabled) {
map[LOCAL_CACHE_RELAY] = filter;
}
for (const relay of relays) map[relay] = filter;
return map;
}

View File

@ -149,7 +149,8 @@ export function listAddCoordinate(
coordinate: string,
relay?: string,
): DraftNostrEvent {
if (list.tags.some((t) => t[0] === "a" && t[1] === coordinate)) throw new Error("coordinate already in list");
if (list.tags.some((t) => t[0] === "a" && t[1] === coordinate)) throw new Error("Event already in list");
return {
created_at: dayjs().unix(),
kind: list.kind,

View File

@ -1,12 +1,11 @@
import { DraftNostrEvent, NostrEvent, Tag } from "../../types/nostr-event";
import { getMatchEmoji, getMatchHashtag } from "../regexp";
import { getReferences } from "./events";
import { getEventRelays } from "../../services/event-relays";
import relayScoreboardService from "../../services/relay-scoreboard";
import { getPubkey, safeDecode } from "../nip19";
import { getPubkeyFromDecodeResult, safeDecode } from "../nip19";
import { Emoji } from "../../providers/emoji-provider";
import { EventSplit } from "./zaps";
import { unique } from "../array";
import relayHintService from "../../services/event-relay-hint";
function addTag(tags: Tag[], tag: Tag, overwrite = false) {
if (tags.some((t) => t[0] === tag[0] && t[1] === tag[1])) {
@ -21,10 +20,9 @@ function addTag(tags: Tag[], tag: Tag, overwrite = false) {
return [...tags, tag];
}
function AddEtag(tags: Tag[], eventId: string, type?: string, overwrite = false) {
const relays = getEventRelays(eventId).value ?? [];
const top = relayScoreboardService.getRankedRelays(relays)[0] ?? "";
const hint = relayHintService.getEventPointerRelayHint(eventId) ?? "";
const tag = type ? ["e", eventId, top, type] : ["e", eventId, top];
const tag = type ? ["e", eventId, hint, type] : ["e", eventId, hint];
if (tags.some((t) => t[0] === tag[0] && t[1] === tag[1] && t[3] === tag[3])) {
if (overwrite) {
@ -73,7 +71,7 @@ export function getContentMentions(content: string) {
Array.from(matched)
.map((m) => {
const parsed = safeDecode(m[1]);
return parsed && getPubkey(parsed);
return parsed && getPubkeyFromDecodeResult(parsed);
})
.filter(Boolean) as string[],
);

View File

@ -1,10 +0,0 @@
import { useMemo } from "react";
import { RelayConfig } from "../classes/relay";
import relayScoreboardService from "../services/relay-scoreboard";
export default function useRankedRelayConfigs(relays: RelayConfig[]) {
return useMemo(() => {
const rankedUrls = relayScoreboardService.getRankedRelays(relays.map((r) => r.url));
return rankedUrls.map((u) => relays.find((r) => r.url === u) as RelayConfig);
}, [relays.join("|")]);
}

View File

@ -1,7 +1,8 @@
import { useMemo } from "react";
import relayScoreboardService from "../services/relay-scoreboard";
import { RelayMode } from "../classes/relay";
import { nip19 } from "nostr-tools";
import { RelayMode } from "../classes/relay";
import relayScoreboardService from "../services/relay-scoreboard";
import { useUserRelays } from "./use-user-relays";
export function useSharableProfileId(pubkey: string, relayCount = 2) {

View File

@ -2,8 +2,7 @@ import "./polyfill";
import { createRoot } from "react-dom/client";
import { App } from "./app";
import { GlobalProviders } from "./providers";
import "./services/serial-port";
import "./services/local-cache-relay";
// setup dayjs
import dayjs from "dayjs";

View File

@ -12,6 +12,7 @@ import { logger } from "../helpers/debug";
import db from "./db";
import createDefer, { Deferred } from "../classes/deferred";
import { getChannelPointer } from "../helpers/nostr/channel";
import localCacheRelayService, { LOCAL_CACHE_RELAY } from "./local-cache-relay";
type Pubkey = string;
type Relay = string;
@ -25,6 +26,8 @@ export type RequestOptions = {
// keepAlive?: boolean;
};
const RELAY_REQUEST_BATCH_TIME = 1000;
/** This class is ued to batch requests to a single relay */
class ChannelMetadataRelayLoader {
private subscription: NostrSubscription;
@ -78,7 +81,7 @@ class ChannelMetadataRelayLoader {
return subject;
}
updateThrottle = _throttle(this.update, 1000);
updateThrottle = _throttle(this.update, RELAY_REQUEST_BATCH_TIME);
update() {
let needsUpdate = false;
for (const channelId of this.requestNext) {
@ -119,6 +122,9 @@ class ChannelMetadataRelayLoader {
}
}
const READ_CACHE_BATCH_TIME = 250;
const WRITE_CACHE_BATCH_TIME = 250;
/** This is a clone of ReplaceableEventLoaderService to support channel metadata */
class ChannelMetadataService {
private metadata = new SuperMap<Pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
@ -147,7 +153,7 @@ class ChannelMetadataService {
}
private readFromCachePromises = new Map<string, Deferred<boolean>>();
private readFromCacheThrottle = _throttle(this.readFromCache, 1000);
private readFromCacheThrottle = _throttle(this.readFromCache, READ_CACHE_BATCH_TIME);
private async readFromCache() {
if (this.readFromCachePromises.size === 0) return;
@ -187,7 +193,7 @@ class ChannelMetadataService {
}
private writeCacheQueue = new Map<string, NostrEvent>();
private writeToCacheThrottle = _throttle(this.writeToCache, 1000);
private writeToCacheThrottle = _throttle(this.writeToCache, WRITE_CACHE_BATCH_TIME);
private async writeToCache() {
if (this.writeCacheQueue.size === 0) return;
@ -224,7 +230,11 @@ class ChannelMetadataService {
private requestChannelMetadataFromRelays(relays: string[], channelId: string) {
const sub = this.metadata.get(channelId);
for (const relay of relays) {
const relayUrls = Array.from(relays);
if (localCacheRelayService.enabled) {
relayUrls.unshift(LOCAL_CACHE_RELAY);
}
for (const relay of relayUrls) {
const request = this.loaders.get(relay).requestMetadata(channelId);
sub.connectWithHandler(request, (event, next, current) => {

View File

@ -8,6 +8,7 @@ import relayScoreboardService from "./relay-scoreboard";
import { logger } from "../helpers/debug";
import { matchFilter, matchFilters } from "nostr-tools";
import { NostrEvent } from "../types/nostr-event";
import localCacheRelayService, { LOCAL_CACHE_RELAY } from "./local-cache-relay";
function hashFilter(filter: NostrRequestFilter) {
// const encoder = new TextEncoder();
@ -41,7 +42,10 @@ class EventExistsService {
if (!this.filters.has(key)) this.filters.set(key, filter);
if (sub.value !== true) {
for (const url of relays) {
const relayUrls = Array.from(relays);
if (localCacheRelayService.enabled) relayUrls.unshift(LOCAL_CACHE_RELAY);
for (const url of relayUrls) {
if (!asked.has(url) && !pending.has(url)) {
pending.add(url);
}

View File

@ -0,0 +1,49 @@
import { NostrEvent } from "../types/nostr-event";
import { getEventRelays } from "./event-relays";
import relayScoreboardService from "./relay-scoreboard";
import type { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19";
import { createCoordinate } from "./replaceable-event-requester";
function pickBestRelays(relays: string[]) {
// ignore local relays
relays = relays.filter((url) => !url.includes("://localhost") && !url.includes("://192.168"));
return relayScoreboardService.getRankedRelays(relays);
}
function getAddressPointerRelayHint(pointer: AddressPointer): string | undefined {
let relays = getEventRelays(createCoordinate(pointer.kind, pointer.pubkey, pointer.identifier)).value;
return pickBestRelays(relays)[0];
}
function getEventPointerRelayHints(pointerOrId: string | EventPointer): string[] {
let relays =
typeof pointerOrId === "string" ? getEventRelays(pointerOrId).value : getEventRelays(pointerOrId.id).value;
return pickBestRelays(relays);
}
function getEventPointerRelayHint(pointerOrId: string | EventPointer): string | undefined {
return getEventPointerRelayHints(pointerOrId)[0];
}
function getEventRelayHint(event: NostrEvent): string | undefined {
return getEventRelayHints(event, 1)[0];
}
function getEventRelayHints(event: NostrEvent, count = 2): string[] {
// NOTE: in the future try to use the events authors relays
let relays = getEventRelays(event.id).value;
return pickBestRelays(relays).slice(0, count);
}
const relayHintService = {
getEventRelayHints,
getEventRelayHint,
getEventPointerRelayHint,
getEventPointerRelayHints,
getAddressPointerRelayHint,
pickBestRelays,
};
export default relayHintService;

View File

@ -0,0 +1,62 @@
import { logger } from "../helpers/debug";
import { NostrEvent } from "../types/nostr-event";
import relayPoolService from "./relay-pool";
import _throttle from "lodash.throttle";
const enabled = !!localStorage.getItem("enable-cache-relay");
export const LOCAL_CACHE_RELAY = "ws://localhost:7000";
const wroteEvents = new Set<string>();
const writeQueue: NostrEvent[] = [];
const BATCH_WRITE = 100;
const log = logger.extend(`LocalCacheRelay`);
async function flush() {
for (let i = 0; i < BATCH_WRITE; i++) {
const e = writeQueue.pop();
if (!e) continue;
relayPoolService.requestRelay(LOCAL_CACHE_RELAY).send(["EVENT", e]);
}
}
function report() {
if (writeQueue.length) {
log(`${writeQueue.length} events in write queue`);
}
}
function addToQueue(e: NostrEvent) {
if (!enabled) return;
if (!wroteEvents.has(e.id)) {
wroteEvents.add(e.id);
writeQueue.push(e);
}
}
if (enabled) {
log("Enabled");
relayPoolService.onRelayCreated.subscribe((relay) => {
if (relay.url !== LOCAL_CACHE_RELAY) {
relay.onEvent.subscribe((incomingEvent) => addToQueue(incomingEvent.body));
}
});
}
const localCacheRelayService = {
enabled,
addToQueue,
};
setInterval(() => {
if (enabled) flush();
}, 1000);
setInterval(() => {
if (enabled) report();
}, 1000 * 10);
if (import.meta.env.DEV) {
//@ts-ignore
window.localCacheRelayService = localCacheRelayService;
}
export default localCacheRelayService;

View File

@ -12,6 +12,7 @@ import db from "./db";
import { nameOrPubkey } from "./user-metadata";
import { getEventCoordinate } from "../helpers/nostr/events";
import createDefer, { Deferred } from "../classes/deferred";
import localCacheRelayService, { LOCAL_CACHE_RELAY } from "./local-cache-relay";
type Pubkey = string;
type Relay = string;
@ -32,6 +33,8 @@ export function createCoordinate(kind: number, pubkey: string, d?: string) {
return `${kind}:${pubkey}${d ? ":" + d : ""}`;
}
const RELAY_REQUEST_BATCH_TIME = 1000;
/** This class is ued to batch requests to a single relay */
class ReplaceableEventRelayLoader {
private subscription: NostrSubscription;
@ -85,7 +88,7 @@ class ReplaceableEventRelayLoader {
return event;
}
updateThrottle = _throttle(this.update, 1000);
updateThrottle = _throttle(this.update, RELAY_REQUEST_BATCH_TIME);
update() {
let needsUpdate = false;
for (const cord of this.requestNext) {
@ -144,6 +147,9 @@ class ReplaceableEventRelayLoader {
}
}
const READ_CACHE_BATCH_TIME = 250;
const WRITE_CACHE_BATCH_TIME = 250;
class ReplaceableEventLoaderService {
private events = new SuperMap<Pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
@ -170,7 +176,7 @@ class ReplaceableEventLoaderService {
}
private readFromCachePromises = new Map<string, Deferred<boolean>>();
private readFromCacheThrottle = _throttle(this.readFromCache, 1000);
private readFromCacheThrottle = _throttle(this.readFromCache, READ_CACHE_BATCH_TIME);
private async readFromCache() {
if (this.readFromCachePromises.size === 0) return;
@ -210,7 +216,7 @@ class ReplaceableEventLoaderService {
}
private writeCacheQueue = new Map<string, NostrEvent>();
private writeToCacheThrottle = _throttle(this.writeToCache, 1000);
private writeToCacheThrottle = _throttle(this.writeToCache, WRITE_CACHE_BATCH_TIME);
private async writeToCache() {
if (this.writeCacheQueue.size === 0) return;
@ -248,7 +254,10 @@ class ReplaceableEventLoaderService {
const cord = createCoordinate(kind, pubkey, d);
const sub = this.events.get(cord);
for (const relay of relays) {
const relayUrls = Array.from(relays);
if (localCacheRelayService.enabled) relayUrls.unshift(LOCAL_CACHE_RELAY);
for (const relay of relayUrls) {
const request = this.loaders.get(relay).requestEvent(kind, pubkey, d);
sub.connectWithHandler(request, (event, next, current) => {

View File

@ -5,6 +5,9 @@ import Subject from "../classes/subject";
import SuperMap from "../classes/super-map";
import { safeRelayUrls } from "../helpers/url";
import { NostrEvent } from "../types/nostr-event";
import localCacheRelayService, { LOCAL_CACHE_RELAY } from "./local-cache-relay";
const RELAY_REQUEST_BATCH_TIME = 1000;
class SingleEventService {
private cache = new SuperMap<string, Subject<NostrEvent>>(() => new Subject());
@ -14,7 +17,9 @@ class SingleEventService {
const subject = this.cache.get(id);
if (subject.value) return subject;
this.pending.set(id, this.pending.get(id)?.concat(safeRelayUrls(relays)) ?? safeRelayUrls(relays));
const newUrls = safeRelayUrls(relays);
if (localCacheRelayService.enabled) newUrls.push(LOCAL_CACHE_RELAY);
this.pending.set(id, this.pending.get(id)?.concat(newUrls) ?? newUrls);
this.batchRequestsThrottle();
return subject;
@ -24,7 +29,7 @@ class SingleEventService {
this.cache.get(event.id).next(event);
}
private batchRequestsThrottle = _throttle(this.batchRequests, 1000 * 2);
private batchRequestsThrottle = _throttle(this.batchRequests, RELAY_REQUEST_BATCH_TIME);
batchRequests() {
if (this.pending.size === 0) return;

View File

@ -8,6 +8,8 @@ import SuperMap from "../classes/super-map";
import Subject from "../classes/subject";
import replaceableEventLoaderService, { RequestOptions } from "./replaceable-event-requester";
const WRITE_USER_SEARCH_BATCH_TIME = 500;
class UserMetadataService {
private parsedSubjects = new SuperMap<string, Subject<Kind0ParsedContent>>((pubkey) => {
const sub = new Subject<Kind0ParsedContent>();
@ -34,7 +36,7 @@ class UserMetadataService {
}
private writeSearchQueue = new Set<string>();
private writeSearchDataThrottle = _throttle(this.writeSearchData.bind(this));
private writeSearchDataThrottle = _throttle(this.writeSearchData.bind(this), WRITE_USER_SEARCH_BATCH_TIME);
private async writeSearchData() {
if (this.writeSearchQueue.size === 0) return;

View File

@ -3,7 +3,7 @@ import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import { Spinner } from "@chakra-ui/react";
import CommunityHomePage from "./community-home";
import { getPubkey, isHexKey, safeDecode } from "../../helpers/nip19";
import { getPubkeyFromDecodeResult, isHexKey, safeDecode } from "../../helpers/nip19";
function useCommunityPointer() {
const { community, pubkey } = useParams();
@ -12,7 +12,7 @@ function useCommunityPointer() {
if (decoded) {
if (decoded.type === "naddr" && decoded.data.kind === COMMUNITY_DEFINITION_KIND) return decoded.data;
} else if (community && pubkey) {
const hexPubkey = isHexKey(pubkey) ? pubkey : getPubkey(safeDecode(pubkey));
const hexPubkey = isHexKey(pubkey) ? pubkey : getPubkeyFromDecodeResult(safeDecode(pubkey));
if (!hexPubkey) return;
return { kind: COMMUNITY_DEFINITION_KIND, pubkey: hexPubkey, identifier: community };

View File

@ -4,13 +4,14 @@ import dayjs from "dayjs";
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
import { StarEmptyIcon, StarFullIcon } from "../../../components/icons";
import { draftAddCoordinate, draftRemoveCoordinate, getEventCoordinate } from "../../../helpers/nostr/events";
import { getEventCoordinate } from "../../../helpers/nostr/events";
import { useSigningContext } from "../../../providers/signing-provider";
import NostrPublishAction from "../../../classes/nostr-publish-action";
import clientRelaysService from "../../../services/client-relays";
import replaceableEventLoaderService from "../../../services/replaceable-event-requester";
import { USER_EMOJI_LIST_KIND } from "../../../helpers/nostr/emoji-packs";
import useFavoriteEmojiPacks from "../../../hooks/use-favorite-emoji-packs";
import { listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists";
export default function EmojiPackFavoriteButton({
pack,
@ -33,7 +34,7 @@ export default function EmojiPackFavoriteButton({
try {
setLoading(true);
const draft = isFavorite ? draftRemoveCoordinate(prev, coordinate) : draftAddCoordinate(prev, coordinate);
const draft = isFavorite ? listRemoveCoordinate(prev, coordinate) : listAddCoordinate(prev, coordinate);
const signed = await requestSignature(draft);
const pub = new NostrPublishAction(
isFavorite ? "Unfavorite Emoji pack" : "Favorite emoji pack",

View File

@ -4,13 +4,13 @@ import dayjs from "dayjs";
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
import { StarEmptyIcon, StarFullIcon } from "../../../components/icons";
import { draftAddCoordinate, draftRemoveCoordinate, getEventCoordinate } from "../../../helpers/nostr/events";
import { getEventCoordinate } from "../../../helpers/nostr/events";
import { useSigningContext } from "../../../providers/signing-provider";
import NostrPublishAction from "../../../classes/nostr-publish-action";
import clientRelaysService from "../../../services/client-relays";
import replaceableEventLoaderService from "../../../services/replaceable-event-requester";
import useFavoriteLists, { FAVORITE_LISTS_IDENTIFIER } from "../../../hooks/use-favorite-lists";
import { NOTE_LIST_KIND, isSpecialListKind } from "../../../helpers/nostr/lists";
import { NOTE_LIST_KIND, isSpecialListKind, listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists";
export default function ListFavoriteButton({
list,
@ -38,7 +38,7 @@ export default function ListFavoriteButton({
try {
setLoading(true);
const draft = isFavorite ? draftRemoveCoordinate(prev, coordinate) : draftAddCoordinate(prev, coordinate);
const draft = isFavorite ? listRemoveCoordinate(prev, coordinate) : listAddCoordinate(prev, coordinate);
const signed = await requestSignature(draft);
const pub = new NostrPublishAction("Favorite list", clientRelaysService.getWriteUrls(), signed);
replaceableEventLoaderService.handleEvent(signed);

View File

@ -36,7 +36,7 @@ import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"
import NoteProxyLink from "../../../components/note/components/note-proxy-link";
import { NoteDetailsButton } from "../../../components/note/components/note-details-button";
import EventInteractionDetailsModal from "../../../components/event-interactions-modal";
import { getNeventCodeWithRelays } from "../../../helpers/nip19";
import { getSharableEventAddress } from "../../../helpers/nip19";
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
import useAppSettings from "../../../hooks/use-app-settings";
import useThreadColorLevelProps from "../../../hooks/use-thread-color-level-props";
@ -80,7 +80,7 @@ export const ThreadPost = memo(({ post, initShowReplies, focusId, level = -1 }:
<UserAvatarLink pubkey={post.event.pubkey} size="sm" />
<UserLink pubkey={post.event.pubkey} fontWeight="bold" isTruncated />
<UserDnsIdentityIcon pubkey={post.event.pubkey} onlyIcon />
<Link as={RouterLink} whiteSpace="nowrap" color="current" to={`/n/${getNeventCodeWithRelays(post.event.id)}`}>
<Link as={RouterLink} whiteSpace="nowrap" color="current" to={`/n/${getSharableEventAddress(post.event)}`}>
<Timestamp timestamp={post.event.created_at} />
</Link>
{replies.length > 0 ? (

View File

@ -13,13 +13,28 @@ import {
Input,
Link,
FormErrorMessage,
Code,
Button,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
useDisclosure,
Text,
Heading,
} from "@chakra-ui/react";
import { safeUrl } from "../../helpers/parse";
import { AppSettings } from "../../services/settings/migrations";
import { PerformanceIcon } from "../../components/icons";
import { useLocalStorage } from "react-use";
import { LOCAL_CACHE_RELAY } from "../../services/local-cache-relay";
export default function PerformanceSettings() {
const { register, formState } = useFormContext<AppSettings>();
const [localCacheRelay, setLocalCacheRelay] = useLocalStorage<boolean>("enable-cache-relay");
const cacheDetails = useDisclosure();
return (
<AccordionItem>
@ -95,6 +110,58 @@ export default function PerformanceSettings() {
</Flex>
<FormHelperText>Enabled: show signature verification on notes</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="localCacheRelay" mb="0">
Local Cache Relay
</FormLabel>
<Switch
id="localCacheRelay"
isChecked={localCacheRelay}
onChange={(e) => setLocalCacheRelay(e.target.checked)}
/>
<Button onClick={cacheDetails.onOpen} variant="link" ml="4">
Details
</Button>
</Flex>
<FormHelperText>Enabled: Use a local relay as a caching service</FormHelperText>
<Modal isOpen={cacheDetails.isOpen} onClose={cacheDetails.onClose} size="4xl">
<ModalOverlay />
<ModalContent>
<ModalHeader p="4">Local cache relay</ModalHeader>
<ModalCloseButton />
<ModalBody px="4" pb="4" pt="0">
<Text>
When this option is enabled noStrudel will mirror every event it sees to the relay. It will also try
to load as much data from the relay first before reaching out to other relays.
</Text>
<Text>
For security reasons noStrudel will only use <Code>ws://localhost:7000</Code> as the cache relay.
</Text>
<Heading size="md" mt="2">
Linux setup instructions
</Heading>
<Text>
You can run a local relay using{" "}
<Link href="https://www.docker.com/get-started/" isExternal>
docker
</Link>{" "}
and{" "}
<Link href="https://hub.docker.com/r/scsibug/nostr-rs-relay" isExternal>
nostr-rs-relay
</Link>
</Text>
<Text mt="2">1. Create a folder for the data</Text>
<Code>mkdir ~/.nostr-relay/data -p -m 777</Code>
<Text mt="2">2. Start the relay</Text>
<Code>
docker run --rm -it -p 7000:8080 -v ~/.nostr-relay/data:/usr/src/app/db scsibug/nostr-rs-relay
</Code>
</ModalBody>
</ModalContent>
</Modal>
</FormControl>
</Flex>
</AccordionPanel>
</AccordionItem>

View File

@ -7,7 +7,7 @@ import { NostrEvent } from "../../../types/nostr-event";
import Timestamp from "../../../components/timestamp";
import { UserLink } from "../../../components/user-link";
import Magnet from "../../../components/icons/magnet";
import { getNeventCodeWithRelays } from "../../../helpers/nip19";
import { getNeventForEventId } from "../../../helpers/nip19";
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
import { getEventUID } from "../../../helpers/nostr/events";
import { formatBytes } from "../../../helpers/number";
@ -58,7 +58,7 @@ function TorrentTableRow({ torrent }: { torrent: NostrEvent }) {
))}
</Td>
<Td>
<Link as={RouterLink} to={`/torrents/${getNeventCodeWithRelays(torrent.id)}`} isTruncated maxW="lg">
<Link as={RouterLink} to={`/torrents/${getNeventForEventId(torrent.id)}`} isTruncated maxW="lg">
{getTorrentTitle(torrent)}
</Link>
</Td>