mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
add local relay cache option
cleanup event relay hint methods
This commit is contained in:
parent
9069932fbe
commit
199f208b11
5
.changeset/quick-garlics-work.md
Normal file
5
.changeset/quick-garlics-work.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add local relay cache option
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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}>
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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[],
|
||||
);
|
||||
|
@ -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("|")]);
|
||||
}
|
@ -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) {
|
||||
|
@ -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";
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
}
|
||||
|
49
src/services/event-relay-hint.ts
Normal file
49
src/services/event-relay-hint.ts
Normal 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;
|
62
src/services/local-cache-relay.ts
Normal file
62
src/services/local-cache-relay.ts
Normal 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;
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 };
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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 ? (
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user