cleanup relay class

This commit is contained in:
hzrd149 2024-02-16 10:36:16 +00:00
parent 8f226f822a
commit ef9de96f3f
64 changed files with 414 additions and 457 deletions

View File

@ -0,0 +1,118 @@
import dayjs from "dayjs";
import { Filter, NostrEvent } from "nostr-tools";
import _throttle from "lodash.throttle";
import debug, { Debugger } from "debug";
import NostrSubscription from "./nostr-subscription";
import EventStore from "./event-store";
import { getEventCoordinate } from "../helpers/nostr/events";
export function createCoordinate(kind: number, pubkey: string, d?: string) {
return `${kind}:${pubkey}${d ? ":" + d : ""}`;
}
const RELAY_REQUEST_BATCH_TIME = 500;
/** This class is ued to batch requests by kind to a single relay */
export default class BatchKindLoader {
private subscription: NostrSubscription;
events = new EventStore();
private requestNext = new Set<string>();
private requested = new Map<string, Date>();
log: Debugger;
constructor(relay: string, log?: Debugger) {
this.subscription = new NostrSubscription(relay, undefined, `replaceable-event-loader`);
this.subscription.onEvent.subscribe(this.handleEvent.bind(this));
this.subscription.onEOSE.subscribe(this.handleEOSE.bind(this));
this.log = log || debug("RelayBatchLoader");
}
private handleEvent(event: NostrEvent) {
const key = getEventCoordinate(event);
// remove the key from the waiting list
this.requested.delete(key);
const current = this.events.getEvent(key);
if (!current || event.created_at > current.created_at) {
this.events.addEvent(event);
}
}
private handleEOSE() {
// relays says it has nothing left
this.requested.clear();
}
requestEvent(kind: number, pubkey: string, d?: string) {
const key = createCoordinate(kind, pubkey, d);
const event = this.events.getEvent(key);
if (!event) {
this.requestNext.add(key);
this.updateThrottle();
}
return event;
}
updateThrottle = _throttle(this.update, RELAY_REQUEST_BATCH_TIME);
update() {
let needsUpdate = false;
for (const key of this.requestNext) {
if (!this.requested.has(key)) {
this.requested.set(key, new Date());
needsUpdate = true;
}
}
this.requestNext.clear();
// prune requests
const timeout = dayjs().subtract(1, "minute");
for (const [key, date] of this.requested) {
if (dayjs(date).isBefore(timeout)) {
this.requested.delete(key);
needsUpdate = true;
}
}
// update the subscription
if (needsUpdate) {
if (this.requested.size > 0) {
const filters: Record<number, Filter> = {};
for (const [cord] of this.requested) {
const [kindStr, pubkey, d] = cord.split(":") as [string, string] | [string, string, string];
const kind = parseInt(kindStr);
filters[kind] = filters[kind] || { kinds: [kind] };
const arr = (filters[kind].authors = filters[kind].authors || []);
arr.push(pubkey);
if (d) {
const arr = (filters[kind]["#d"] = filters[kind]["#d"] || []);
arr.push(d);
}
}
const query = Array.from(Object.values(filters));
this.log(
`Updating query`,
Array.from(Object.keys(filters))
.map((kind: string) => `kind ${kind}: ${filters[parseInt(kind)].authors?.length}`)
.join(", "),
);
this.subscription.setFilters(query);
if (this.subscription.state !== NostrSubscription.OPEN) {
this.subscription.open();
}
} else if (this.subscription.state === NostrSubscription.OPEN) {
this.subscription.close();
}
}
}
}

View File

@ -1,4 +1,6 @@
import { NostrEvent } from "nostr-tools";
import { nanoid } from "nanoid";
import { getEventUID, sortByDate } from "../helpers/nostr/events";
import ControlledObservable from "./controlled-observable";
import SuperMap from "./super-map";
@ -6,7 +8,9 @@ import deleteEventService from "../services/delete-events";
export type EventFilter = (event: NostrEvent, store: EventStore) => boolean;
/** a class used to store and sort events */
export default class EventStore {
id = nanoid(8);
name?: string;
events = new Map<string, NostrEvent>();
@ -34,16 +38,15 @@ export default class EventStore {
onClear = new ControlledObservable();
private handleEvent(event: NostrEvent) {
const id = getEventUID(event);
const existing = this.events.get(id);
const uid = getEventUID(event);
const existing = this.events.get(uid);
if (!existing || event.created_at > existing.created_at) {
this.events.set(id, event);
this.events.set(uid, event);
this.onEvent.next(event);
}
}
addEvent(event: NostrEvent) {
const id = getEventUID(event);
this.handleEvent(event);
}
getEvent(id: string) {

View File

@ -1,8 +1,8 @@
import { nanoid } from "nanoid";
import { NostrEvent } from "../types/nostr-event";
import { NostrOutgoingRequest, NostrRequestFilter, RelayQueryMap } from "../types/nostr-query";
import Relay, { IncomingEvent } from "./relay";
import { NostrRequestFilter, RelayQueryMap } from "../types/nostr-relay";
import Relay, { IncomingEvent, OutgoingRequest } from "./relay";
import relayPoolService from "../services/relay-pool";
import { isFilterEqual, isQueryMapEqual } from "../helpers/nostr/filter";
import ControlledObservable from "./controlled-observable";
@ -26,14 +26,14 @@ export default class NostrMultiSubscription {
this.id = nanoid();
this.name = name;
}
private handleEvent(incomingEvent: IncomingEvent) {
private handleMessage(incomingEvent: IncomingEvent) {
if (
this.state === NostrMultiSubscription.OPEN &&
incomingEvent.subId === this.id &&
!this.seenEvents.has(incomingEvent.body.id)
incomingEvent[1] === this.id &&
!this.seenEvents.has(incomingEvent[2].id)
) {
this.onEvent.next(incomingEvent.body);
this.seenEvents.add(incomingEvent.body.id);
this.onEvent.next(incomingEvent[2]);
this.seenEvents.add(incomingEvent[2].id);
}
}
@ -41,7 +41,7 @@ export default class NostrMultiSubscription {
/** listen for event and open events from relays */
private connectToRelay(relay: Relay) {
const subs = this.relaySubs.get(relay);
subs.push(relay.onEvent.subscribe(this.handleEvent.bind(this)));
subs.push(relay.onEvent.subscribe(this.handleMessage.bind(this)));
subs.push(relay.onOpen.subscribe(this.handleRelayConnect.bind(this)));
subs.push(relay.onClose.subscribe(this.handleRelayDisconnect.bind(this)));
relayPoolService.addClaim(relay.url, this);
@ -93,9 +93,7 @@ export default class NostrMultiSubscription {
for (const relay of this.relays) {
const filter = this.queryMap[relay.url];
const message: NostrOutgoingRequest = Array.isArray(filter)
? ["REQ", this.id, ...filter]
: ["REQ", this.id, filter];
const message: OutgoingRequest = Array.isArray(filter) ? ["REQ", this.id, ...filter] : ["REQ", this.id, filter];
const currentFilter = this.relayQueries.get(relay);
if (!currentFilter || !isFilterEqual(currentFilter, filter)) {

View File

@ -14,9 +14,10 @@ export default class NostrPublishAction {
relays: string[];
event: NostrEvent;
results = new PersistentSubject<IncomingCommandResult[]>([]);
onResult = new ControlledObservable<IncomingCommandResult>();
onComplete = createDefer<IncomingCommandResult[]>();
results = new PersistentSubject<{ relay: Relay; result: IncomingCommandResult }[]>([]);
onResult = new ControlledObservable<{ relay: Relay; result: IncomingCommandResult }>();
onComplete = createDefer<{ relay: Relay; result: IncomingCommandResult }[]>();
private remaining = new Set<Relay>();
private relayResultSubs = new SuperMap<Relay, ZenObservable.Subscription[]>(() => []);
@ -29,38 +30,31 @@ export default class NostrPublishAction {
for (const url of relays) {
const relay = relayPoolService.requestRelay(url);
this.remaining.add(relay);
this.relayResultSubs.get(relay).push(relay.onCommandResult.subscribe(this.handleResult.bind(this)));
this.relayResultSubs.get(relay).push(
relay.onCommandResult.subscribe((result) => {
if (result[1] === this.event.id) this.handleResult(result, relay);
}),
);
// send event
relay.send(["EVENT", event]);
}
setTimeout(this.handleTimeout.bind(this), timeout);
}
private handleResult(result: IncomingCommandResult) {
if (result.eventId === this.event.id) {
const relay = result.relay;
this.results.next([...this.results.value, result]);
private handleResult(result: IncomingCommandResult, relay: Relay) {
this.results.next([...this.results.value, { relay, result }]);
this.onResult.next({ relay, result });
this.onResult.next(result);
this.relayResultSubs.get(relay).forEach((s) => s.unsubscribe());
this.relayResultSubs.delete(relay);
this.remaining.delete(relay);
if (this.remaining.size === 0) this.onComplete.resolve(this.results.value);
}
this.relayResultSubs.get(relay).forEach((s) => s.unsubscribe());
this.relayResultSubs.delete(relay);
this.remaining.delete(relay);
if (this.remaining.size === 0) this.onComplete.resolve(this.results.value);
}
private handleTimeout() {
for (const relay of this.remaining) {
this.handleResult({
message: "Timeout",
eventId: this.event.id,
status: false,
type: "OK",
relay,
});
this.handleResult(["OK", this.event.id, false, "Timeout"], relay);
}
}
}

View File

@ -1,8 +1,9 @@
import { nanoid } from "nanoid";
import { CountResponse, NostrEvent } from "../types/nostr-event";
import { NostrRequestFilter } from "../types/nostr-query";
import { NostrEvent } from "../types/nostr-event";
import { NostrRequestFilter } from "../types/nostr-relay";
import relayPoolService from "../services/relay-pool";
import Relay, { IncomingCount, IncomingEOSE, IncomingEvent } from "./relay";
import Relay, { CountResponse, IncomingCount, IncomingEOSE, IncomingEvent } from "./relay";
import createDefer from "./deferred";
import ControlledObservable from "./controlled-observable";
import SuperMap from "./super-map";
@ -19,7 +20,6 @@ export default class NostrRequest {
state = NostrRequest.IDLE;
onEvent = new ControlledObservable<NostrEvent>();
onCount = new ControlledObservable<CountResponse>();
/** @deprecated */
onComplete = createDefer<void>();
seenEvents = new Set<string>();
@ -30,7 +30,7 @@ export default class NostrRequest {
for (const relay of this.relays) {
const subs = this.relaySubs.get(relay);
subs.push(relay.onEOSE.subscribe(this.handleEOSE.bind(this)));
subs.push(relay.onEOSE.subscribe((m) => this.handleEOSE(m, relay)));
subs.push(relay.onEvent.subscribe(this.handleEvent.bind(this)));
subs.push(relay.onCount.subscribe(this.handleCount.bind(this)));
}
@ -38,9 +38,8 @@ export default class NostrRequest {
this.timeout = timeout ?? REQUEST_DEFAULT_TIMEOUT;
}
handleEOSE(eose: IncomingEOSE) {
if (eose.subId === this.id) {
const relay = eose.relay;
handleEOSE(message: IncomingEOSE, relay: Relay) {
if (message[1] === this.id) {
this.relays.delete(relay);
relay.send(["CLOSE", this.id]);
@ -53,19 +52,15 @@ export default class NostrRequest {
}
}
}
handleEvent(incomingEvent: IncomingEvent) {
if (
this.state === NostrRequest.RUNNING &&
incomingEvent.subId === this.id &&
!this.seenEvents.has(incomingEvent.body.id)
) {
this.onEvent.next(incomingEvent.body);
this.seenEvents.add(incomingEvent.body.id);
handleEvent(message: IncomingEvent) {
if (this.state === NostrRequest.RUNNING && message[1] === this.id && !this.seenEvents.has(message[2].id)) {
this.onEvent.next(message[2]);
this.seenEvents.add(message[2].id);
}
}
handleCount(incomingCount: IncomingCount) {
if (incomingCount.subId === this.id) {
this.onCount.next({ count: incomingCount.count, approximate: incomingCount.approximate });
if (incomingCount[1] === this.id) {
this.onCount.next(incomingCount[2]);
}
}

View File

@ -1,8 +1,7 @@
import { nanoid } from "nanoid";
import { Filter, NostrEvent } from "nostr-tools";
import { NostrEvent } from "../types/nostr-event";
import { NostrOutgoingMessage, NostrRequestFilter } from "../types/nostr-query";
import Relay, { IncomingEOSE } from "./relay";
import Relay, { IncomingEOSE, OutgoingMessage } from "./relay";
import relayPoolService from "../services/relay-pool";
import ControlledObservable from "./controlled-observable";
@ -13,61 +12,58 @@ export default class NostrSubscription {
id: string;
name?: string;
query?: NostrRequestFilter;
filters?: Filter[];
relay: Relay;
state = NostrSubscription.INIT;
onEvent = new ControlledObservable<NostrEvent>();
onEOSE = new ControlledObservable<IncomingEOSE>();
private subs: ZenObservable.Subscription[] = [];
constructor(relayUrl: string | URL, query?: NostrRequestFilter, name?: string) {
constructor(relayUrl: string | URL, filters?: Filter[], name?: string) {
this.id = nanoid();
this.query = query;
this.filters = filters;
this.name = name;
this.relay = relayPoolService.requestRelay(relayUrl);
this.subs.push(
this.relay.onEvent.subscribe((message) => {
if (this.state === NostrSubscription.OPEN && message.subId === this.id) {
this.onEvent.next(message.body);
if (this.state === NostrSubscription.OPEN && message[1] === this.id) {
this.onEvent.next(message[2]);
}
}),
);
this.subs.push(
this.relay.onEOSE.subscribe((eose) => {
if (this.state === NostrSubscription.OPEN && eose.subId === this.id) this.onEOSE.next(eose);
if (this.state === NostrSubscription.OPEN && eose[1] === this.id) this.onEOSE.next(eose);
}),
);
}
send(message: NostrOutgoingMessage) {
send(message: OutgoingMessage) {
this.relay.send(message);
}
setFilters(filters: Filter[]) {
this.filters = filters;
if (this.state === NostrSubscription.OPEN) {
this.send(["REQ", this.id, ...this.filters]);
}
return this;
}
open() {
if (!this.query) throw new Error("cant open without a query");
if (!this.filters) throw new Error("cant open without a query");
if (this.state === NostrSubscription.OPEN) return this;
this.state = NostrSubscription.OPEN;
if (Array.isArray(this.query)) {
this.send(["REQ", this.id, ...this.query]);
} else this.send(["REQ", this.id, this.query]);
this.send(["REQ", this.id, ...this.filters]);
relayPoolService.addClaim(this.relay.url, this);
return this;
}
setQuery(query: NostrRequestFilter) {
this.query = query;
if (this.state === NostrSubscription.OPEN) {
if (Array.isArray(this.query)) {
this.send(["REQ", this.id, ...this.query]);
} else this.send(["REQ", this.id, this.query]);
}
return this;
}
close() {
if (this.state !== NostrSubscription.OPEN) return this;

90
src/classes/relay-pool.ts Normal file
View File

@ -0,0 +1,90 @@
import { logger } from "../helpers/debug";
import { validateRelayURL } from "../helpers/relay";
import { offlineMode } from "../services/offline-mode";
import Relay from "./relay";
import Subject from "./subject";
export default class RelayPool {
relays = new Map<string, Relay>();
onRelayCreated = new Subject<Relay>();
relayClaims = new Map<string, Set<any>>();
log = logger.extend("RelayPool");
getRelays() {
return Array.from(this.relays.values());
}
getRelayClaims(url: string | URL) {
url = validateRelayURL(url);
const key = url.toString();
if (!this.relayClaims.has(key)) {
this.relayClaims.set(key, new Set());
}
return this.relayClaims.get(key) as Set<any>;
}
requestRelay(url: string | URL, connect = true) {
url = validateRelayURL(url);
const key = url.toString();
if (!this.relays.has(key)) {
const newRelay = new Relay(key);
this.relays.set(key, newRelay);
this.onRelayCreated.next(newRelay);
}
const relay = this.relays.get(key) as Relay;
if (connect && !relay.okay) {
try {
relay.open();
} catch (e) {
this.log(`Failed to connect to ${relay.url}`);
this.log(e);
}
}
return relay;
}
pruneRelays() {
for (const [url, relay] of this.relays.entries()) {
const claims = this.getRelayClaims(url).size;
if (claims === 0) {
relay.close();
}
}
}
reconnectRelays() {
if (offlineMode.value) return;
for (const [url, relay] of this.relays.entries()) {
const claims = this.getRelayClaims(url).size;
if (!relay.okay && claims > 0) {
try {
relay.open();
} catch (e) {
this.log(`Failed to connect to ${relay.url}`);
this.log(e);
}
}
}
}
addClaim(url: string | URL, id: any) {
url = validateRelayURL(url);
const key = url.toString();
this.getRelayClaims(key).add(id);
}
removeClaim(url: string | URL, id: any) {
url = validateRelayURL(url);
const key = url.toString();
this.getRelayClaims(key).delete(id);
}
get connectedCount() {
let count = 0;
for (const [url, relay] of this.relays.entries()) {
if (relay.connected) count++;
}
return count;
}
}

View File

@ -1,40 +1,28 @@
import { Filter, NostrEvent } from "nostr-tools";
import { offlineMode } from "../services/offline-mode";
import relayScoreboardService from "../services/relay-scoreboard";
import { RawIncomingNostrEvent, NostrEvent, CountResponse } from "../types/nostr-event";
import { NostrOutgoingMessage } from "../types/nostr-query";
import ControlledObservable from "./controlled-observable";
import createDefer, { Deferred } from "./deferred";
import { PersistentSubject } from "./subject";
export type IncomingEvent = {
type: "EVENT";
subId: string;
body: NostrEvent;
relay: Relay;
};
export type IncomingNotice = {
type: "NOTICE";
message: string;
relay: Relay;
};
export type IncomingCount = {
type: "COUNT";
subId: string;
relay: Relay;
} & CountResponse;
export type IncomingEOSE = {
type: "EOSE";
subId: string;
relay: Relay;
};
export type IncomingCommandResult = {
type: "OK";
eventId: string;
status: boolean;
message?: string;
relay: Relay;
export type CountResponse = {
count: number;
approximate?: boolean;
};
export type IncomingEvent = ["EVENT", string, NostrEvent];
export type IncomingNotice = ["NOTICE", string];
export type IncomingCount = ["COUNT", string, CountResponse];
export type IncomingEOSE = ["EOSE", string];
export type IncomingCommandResult = ["OK", string, boolean] | ["OK", string, boolean, string];
export type IncomingMessage = IncomingEvent | IncomingNotice | IncomingCount | IncomingEOSE | IncomingCommandResult;
export type OutgoingEvent = ["EVENT", NostrEvent];
export type OutgoingRequest = ["REQ", string, ...Filter[]];
export type OutgoingCount = ["COUNT", string, ...Filter[]];
export type OutgoingClose = ["CLOSE", string];
export type OutgoingMessage = OutgoingEvent | OutgoingRequest | OutgoingClose | OutgoingCount;
export enum RelayMode {
NONE = 0,
READ = 1,
@ -46,15 +34,16 @@ const CONNECTION_TIMEOUT = 1000 * 30;
export default class Relay {
url: string;
ws?: WebSocket;
status = new PersistentSubject<number>(WebSocket.CLOSED);
onOpen = new ControlledObservable<Relay>();
onClose = new ControlledObservable<Relay>();
onEvent = new ControlledObservable<IncomingEvent>();
onNotice = new ControlledObservable<IncomingNotice>();
onCount = new ControlledObservable<IncomingCount>();
onEOSE = new ControlledObservable<IncomingEOSE>();
onCommandResult = new ControlledObservable<IncomingCommandResult>();
ws?: WebSocket;
private connectionPromises: Deferred<void>[] = [];
@ -62,7 +51,7 @@ export default class Relay {
private ejectTimer?: () => void;
private intentionalClose = false;
private subscriptionResTimer = new Map<string, () => void>();
private queue: NostrOutgoingMessage[] = [];
private queue: OutgoingMessage[] = [];
constructor(url: string) {
this.url = url;
@ -123,7 +112,7 @@ export default class Relay {
};
this.ws.onmessage = this.handleMessage.bind(this);
}
send(json: NostrOutgoingMessage) {
send(json: OutgoingMessage) {
if (this.connected) {
this.ws?.send(JSON.stringify(json));
@ -185,35 +174,38 @@ export default class Relay {
return this.ws?.readyState;
}
handleMessage(event: MessageEvent<string>) {
if (!event.data) return;
handleMessage(message: MessageEvent<string>) {
if (!message.data) return;
try {
const data: RawIncomingNostrEvent = JSON.parse(event.data);
const data: IncomingMessage = JSON.parse(message.data);
const type = data[0];
// all messages must have an argument
if (!data[1]) return;
switch (type) {
case "EVENT":
this.onEvent.next({ relay: this, type, subId: data[1], body: data[2] });
this.onEvent.next(data);
this.endSubResTimer(data[1]);
break;
case "NOTICE":
this.onNotice.next({ relay: this, type, message: data[1] });
this.onNotice.next(data);
break;
case "COUNT":
this.onCount.next({ relay: this, type, subId: data[1], ...data[2] });
this.onCount.next(data);
break;
case "EOSE":
this.onEOSE.next({ relay: this, type, subId: data[1] });
this.onEOSE.next(data);
this.endSubResTimer(data[1]);
break;
case "OK":
this.onCommandResult.next({ relay: this, type, eventId: data[1], status: data[2], message: data[3] });
this.onCommandResult.next(data);
break;
}
} catch (e) {
console.log(`Relay: Failed to parse event from ${this.url}`);
console.log(event.data, e);
console.log(`Relay: Failed to parse massage from ${this.url}`);
console.log(message.data, e);
}
}
}

View File

@ -4,14 +4,14 @@ import { Filter, matchFilters } from "nostr-tools";
import _throttle from "lodash.throttle";
import { NostrEvent, isATag, isETag } from "../types/nostr-event";
import { NostrRequestFilter, RelayQueryMap } from "../types/nostr-query";
import { NostrRequestFilter, RelayQueryMap } from "../types/nostr-relay";
import NostrRequest from "./nostr-request";
import NostrMultiSubscription from "./nostr-multi-subscription";
import Subject, { PersistentSubject } from "./subject";
import { logger } from "../helpers/debug";
import EventStore from "./event-store";
import { isReplaceable } from "../helpers/nostr/events";
import replaceableEventLoaderService from "../services/replaceable-event-requester";
import replaceableEventsService from "../services/replaceable-events";
import deleteEventService from "../services/delete-events";
import {
addQueryToFilter,
@ -127,7 +127,7 @@ export default class TimelineLoader {
this.name = name;
this.log = logger.extend("TimelineLoader:" + name);
this.events = new EventStore(name);
this.events.connect(replaceableEventLoaderService.events, false);
this.events.connect(replaceableEventsService.events, false);
this.subscription = new NostrMultiSubscription(name);
this.subscription.onEvent.subscribe(this.handleEvent.bind(this));
@ -147,7 +147,7 @@ export default class TimelineLoader {
}
private handleEvent(event: NostrEvent, cache = true) {
// if this is a replaceable event, mirror it over to the replaceable event service
if (isReplaceable(event.kind)) replaceableEventLoaderService.handleEvent(event);
if (isReplaceable(event.kind)) replaceableEventsService.handleEvent(event);
this.events.addEvent(event);
if (cache) localRelay.publish(event);

View File

@ -7,13 +7,13 @@ import RawValue from "./raw-value";
import RawJson from "./raw-json";
import { useSharableProfileId } from "../../hooks/use-shareable-profile-id";
import useUserLNURLMetadata from "../../hooks/use-user-lnurl-metadata";
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
import replaceableEventsService from "../../services/replaceable-events";
export default function UserDebugModal({ pubkey, ...props }: { pubkey: string } & Omit<ModalProps, "children">) {
const npub = nip19.npubEncode(pubkey);
const metadata = useUserMetadata(pubkey);
const nprofile = useSharableProfileId(pubkey);
const relays = replaceableEventLoaderService.getEvent(kinds.RelayList, pubkey).value;
const relays = replaceableEventsService.getEvent(kinds.RelayList, pubkey).value;
const tipMetadata = useUserLNURLMetadata(pubkey);
return (

View File

@ -3,7 +3,7 @@ import { Link as RouterLink } from "react-router-dom";
import { NostrEvent } from "../../../types/nostr-event";
import { getListDescription, getListName, isSpecialListKind } from "../../../helpers/nostr/lists";
import { createCoordinate } from "../../../services/replaceable-event-requester";
import { createCoordinate } from "../../../services/replaceable-events";
import { getSharableEventAddress } from "../../../helpers/nip19";
import UserAvatarLink from "../../user-avatar-link";
import UserLink from "../../user-link";

View File

@ -30,7 +30,7 @@ import { isValidRelayURL } from "../helpers/relay";
import relayScoreboardService from "../services/relay-scoreboard";
import { RelayFavicon } from "./relay-favicon";
import singleEventService from "../services/single-event";
import replaceableEventLoaderService from "../services/replaceable-event-requester";
import replaceableEventsService from "../services/replaceable-events";
function SearchOnRelaysModal({
isOpen,
@ -53,7 +53,7 @@ function SearchOnRelaysModal({
setLoading(true);
switch (decode.type) {
case "naddr":
replaceableEventLoaderService.requestEvent(
replaceableEventsService.requestEvent(
Array.from(relays),
decode.data.kind,
decode.data.pubkey,

View File

@ -21,7 +21,7 @@ import { EmbedEvent } from "../../embed-event";
import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from "../../icons";
import useUserCommunitiesList from "../../../hooks/use-user-communities-list";
import useCurrentAccount from "../../../hooks/use-current-account";
import { createCoordinate } from "../../../services/replaceable-event-requester";
import { createCoordinate } from "../../../services/replaceable-events";
import relayHintService from "../../../services/event-relay-hint";
import { usePublishEvent } from "../../../providers/global/publish-provider";

View File

@ -17,17 +17,17 @@ export function PublishDetails({ pub }: PostResultsProps & Omit<FlexProps, "chil
<Flex direction="column" gap="2">
<EmbedEvent event={pub.event} />
<Progress value={(results.length / pub.relays.length) * 100} size="lg" hasStripe />
{results.map((result) => (
<Alert key={result.relay.url} status={result.status ? "success" : "warning"}>
{results.map(({ result, relay }) => (
<Alert key={relay.url} status={result[2] ? "success" : "warning"}>
<AlertIcon />
<Box>
<AlertTitle>
<Link as={RouterLink} to={`/r/${encodeURIComponent(result.relay.url)}`}>
{result.relay.url}
<Link as={RouterLink} to={`/r/${encodeURIComponent(relay.url)}`}>
{relay.url}
</Link>
<RelayPaidTag url={result.relay.url} />
<RelayPaidTag url={relay.url} />
</AlertTitle>
{result.message && <AlertDescription>{result.message}</AlertDescription>}
{result[3] && <AlertDescription>{result[3]}</AlertDescription>}
</Box>
</Alert>
))}

View File

@ -25,8 +25,8 @@ import { PublishContext } from "../providers/global/publish-provider";
export function PublishActionStatusTag({ pub, ...props }: { pub: NostrPublishAction } & Omit<TagProps, "children">) {
const results = useSubject(pub.results);
const successful = results.filter((result) => result.status);
const failedWithMessage = results.filter((result) => !result.status && result.message);
const successful = results.filter(({ result }) => result[2]);
const failedWithMessage = results.filter(({ result }) => !result[2] && result[3]);
let statusIcon = <Spinner size="xs" />;
let statusColor: TagProps["colorScheme"] = "blue";

View File

@ -1,14 +1,14 @@
import { EventTemplate, kinds, validateEvent } from "nostr-tools";
import { getEventUID } from "nostr-idb";
import dayjs from "dayjs";
import { nanoid } from "nanoid";
import { ATag, DraftNostrEvent, ETag, isATag, isDTag, isETag, isPTag, NostrEvent, Tag } from "../../types/nostr-event";
import { getMatchNostrLink } from "../regexp";
import { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19";
import { safeJson } from "../parse";
import { safeDecode } from "../nip19";
import { getEventUID } from "nostr-idb";
import { safeRelayUrl, safeRelayUrls } from "../relay";
import dayjs from "dayjs";
import { nanoid } from "nanoid";
import userMailboxesService from "../../services/user-mailboxes";
import RelaySet from "../../classes/relay-set";

View File

@ -1,5 +1,5 @@
import stringify from "json-stringify-deterministic";
import { NostrRequestFilter, RelayQueryMap } from "../../types/nostr-query";
import { NostrRequestFilter, RelayQueryMap } from "../../types/nostr-relay";
import { Filter } from "nostr-tools";
import { safeRelayUrls } from "../relay";

View File

@ -2,7 +2,7 @@ import dayjs from "dayjs";
import { DraftNostrEvent, NostrEvent, isPTag } from "../../types/nostr-event";
import { unique } from "../array";
import { ensureNotifyContentMentions } from "./post";
import { createCoordinate } from "../../services/replaceable-event-requester";
import { createCoordinate } from "../../services/replaceable-events";
export const STREAM_KIND = 30311;
export const STREAM_CHAT_MESSAGE_KIND = 1311;

View File

@ -1,7 +1,7 @@
import { SimpleRelay, SubscriptionOptions } from "nostr-idb";
import { Filter } from "nostr-tools";
import { NostrQuery, NostrRequestFilter } from "../types/nostr-query";
import { NostrQuery, NostrRequestFilter } from "../types/nostr-relay";
import { NostrEvent } from "../types/nostr-event";
// NOTE: only use this for equality checks and querying

View File

@ -1,6 +1,6 @@
import { useMemo } from "react";
import { RequestOptions } from "../services/replaceable-event-requester";
import { RequestOptions } from "../services/replaceable-events";
import useSubject from "./use-subject";
import channelMetadataService from "../services/channel-metadata";
import { ChannelMetadata, safeParseChannelMetadata } from "../helpers/nostr/channel";

View File

@ -1,6 +1,6 @@
import { useMemo } from "react";
import eventCountService from "../services/event-count";
import { NostrRequestFilter } from "../types/nostr-query";
import { NostrRequestFilter } from "../types/nostr-relay";
import useSubject from "./use-subject";
export default function useEventCount(filter?: NostrRequestFilter, alwaysRequest = false) {

View File

@ -1,7 +1,7 @@
import { useMemo } from "react";
import stringify from "json-stringify-deterministic";
import eventExistsService from "../services/event-exists";
import { NostrRequestFilter } from "../types/nostr-query";
import { NostrRequestFilter } from "../types/nostr-relay";
import useSubject from "./use-subject";
export default function useEventExists(filter?: NostrRequestFilter, relays: string[] = [], fallback = true) {

View File

@ -1,7 +1,7 @@
import useReplaceableEvent from "./use-replaceable-event";
import useCurrentAccount from "./use-current-account";
import { USER_EMOJI_LIST_KIND } from "../helpers/nostr/emoji-packs";
import { RequestOptions } from "../services/replaceable-event-requester";
import { RequestOptions } from "../services/replaceable-events";
export const FAVORITE_LISTS_IDENTIFIER = "nostrudel-favorite-lists";

View File

@ -1,7 +1,7 @@
import { useMemo } from "react";
import { useReadRelays } from "./use-client-relays";
import replaceableEventLoaderService, { RequestOptions } from "../services/replaceable-event-requester";
import replaceableEventsService, { RequestOptions } from "../services/replaceable-events";
import { CustomAddressPointer, parseCoordinate } from "../helpers/nostr/events";
import useSubject from "./use-subject";
@ -14,7 +14,7 @@ export default function useReplaceableEvent(
const sub = useMemo(() => {
const parsed = typeof cord === "string" ? parseCoordinate(cord) : cord;
if (!parsed) return;
return replaceableEventLoaderService.requestEvent(
return replaceableEventsService.requestEvent(
parsed.relays ? [...readRelays, ...parsed.relays] : readRelays,
parsed.kind,
parsed.pubkey,

View File

@ -1,7 +1,7 @@
import { useMemo } from "react";
import { useReadRelays } from "./use-client-relays";
import replaceableEventLoaderService, { RequestOptions } from "../services/replaceable-event-requester";
import replaceableEventsService, { RequestOptions } from "../services/replaceable-events";
import { CustomAddressPointer, parseCoordinate } from "../helpers/nostr/events";
import Subject from "../classes/subject";
import { NostrEvent } from "../types/nostr-event";
@ -20,7 +20,7 @@ export default function useReplaceableEvents(
const parsed = typeof cord === "string" ? parseCoordinate(cord) : cord;
if (!parsed) return;
subs.push(
replaceableEventLoaderService.requestEvent(
replaceableEventsService.requestEvent(
parsed.relays ? [...readRelays, ...parsed.relays] : readRelays,
parsed.kind,
parsed.pubkey,

View File

@ -1,7 +1,7 @@
import { useEffect, useMemo } from "react";
import { useUnmount } from "react-use";
import { NostrRequestFilter } from "../types/nostr-query";
import { NostrRequestFilter } from "../types/nostr-relay";
import timelineCacheService from "../services/timeline-cache";
import { EventFilter } from "../classes/timeline-loader";
import { NostrEvent } from "../types/nostr-event";

View File

@ -1,7 +1,7 @@
import { useMemo } from "react";
import { BOOKMARK_LIST_KIND, getAddressPointersFromList, getEventPointersFromList } from "../helpers/nostr/lists";
import { RequestOptions } from "../services/replaceable-event-requester";
import { RequestOptions } from "../services/replaceable-events";
import useCurrentAccount from "./use-current-account";
import useReplaceableEvent from "./use-replaceable-event";

View File

@ -1,5 +1,5 @@
import { CHANNELS_LIST_KIND, getEventPointersFromList } from "../helpers/nostr/lists";
import { RequestOptions } from "../services/replaceable-event-requester";
import { RequestOptions } from "../services/replaceable-events";
import useCurrentAccount from "./use-current-account";
import useReplaceableEvent from "./use-replaceable-event";

View File

@ -1,6 +1,6 @@
import { COMMUNITY_DEFINITION_KIND, SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER } from "../helpers/nostr/communities";
import { COMMUNITIES_LIST_KIND, NOTE_LIST_KIND, getAddressPointersFromList } from "../helpers/nostr/lists";
import { RequestOptions } from "../services/replaceable-event-requester";
import { RequestOptions } from "../services/replaceable-events";
import useCurrentAccount from "./use-current-account";
import useReplaceableEvent from "./use-replaceable-event";

View File

@ -1,6 +1,6 @@
import { kinds } from "nostr-tools";
import useReplaceableEvent from "./use-replaceable-event";
import { RequestOptions } from "../services/replaceable-event-requester";
import { RequestOptions } from "../services/replaceable-events";
export default function useUserContactList(
pubkey?: string,

View File

@ -1,5 +1,5 @@
import { useMemo } from "react";
import { RequestOptions } from "../services/replaceable-event-requester";
import { RequestOptions } from "../services/replaceable-events";
import RelaySet from "../classes/relay-set";
import useUserContactList from "./use-user-contact-list";
import { RelayMode } from "../classes/relay";

View File

@ -1,5 +1,5 @@
import RelaySet from "../classes/relay-set";
import { RequestOptions } from "../services/replaceable-event-requester";
import { RequestOptions } from "../services/replaceable-events";
import userMailboxesService from "../services/user-mailboxes";
import { useReadRelays } from "./use-client-relays";
import useSubject from "./use-subject";

View File

@ -2,7 +2,7 @@ import { useMemo } from "react";
import userMetadataService from "../services/user-metadata";
import { useReadRelays } from "./use-client-relays";
import useSubject from "./use-subject";
import { RequestOptions } from "../services/replaceable-event-requester";
import { RequestOptions } from "../services/replaceable-events";
import { COMMON_CONTACT_RELAY } from "../const";
export function useUserMetadata(pubkey?: string, additionalRelays: Iterable<string> = [], opts: RequestOptions = {}) {

View File

@ -5,7 +5,7 @@ import useUserMuteList from "./use-user-mute-list";
import { getPubkeysFromList } from "../helpers/nostr/lists";
import { NostrEvent } from "../types/nostr-event";
import { STREAM_KIND, getStreamHost } from "../helpers/nostr/stream";
import { RequestOptions } from "../services/replaceable-event-requester";
import { RequestOptions } from "../services/replaceable-events";
export default function useUserMuteFilter(pubkey?: string, additionalRelays?: string[], opts?: RequestOptions) {
const account = useCurrentAccount();

View File

@ -1,6 +1,6 @@
import useReplaceableEvent from "./use-replaceable-event";
import { MUTE_LIST_KIND } from "../helpers/nostr/lists";
import { RequestOptions } from "../services/replaceable-event-requester";
import { RequestOptions } from "../services/replaceable-events";
export default function useUserMuteList(
pubkey?: string,

View File

@ -3,7 +3,7 @@ import { useMemo } from "react";
import useReplaceableEvent from "./use-replaceable-event";
import { PEOPLE_LIST_KIND, getPubkeysFromList } from "../helpers/nostr/lists";
import useUserMuteList from "./use-user-mute-list";
import { RequestOptions } from "../services/replaceable-event-requester";
import { RequestOptions } from "../services/replaceable-events";
export default function useUserMuteLists(
pubkey?: string,

View File

@ -3,7 +3,7 @@ import { kinds } from "nostr-tools";
import { getPubkeysFromList } from "../helpers/nostr/lists";
import useUserContactList from "./use-user-contact-list";
import replaceableEventLoaderService from "../services/replaceable-event-requester";
import replaceableEventsService from "../services/replaceable-events";
import { useReadRelays } from "./use-client-relays";
import useSubjects from "./use-subjects";
import userMetadataService from "../services/user-metadata";
@ -34,7 +34,7 @@ export default function useUserNetwork(pubkey: string, additionalRelays?: Iterab
const subjects = useMemo(() => {
return contactsPubkeys.map((person) =>
replaceableEventLoaderService.requestEvent(readRelays, kinds.Contacts, person.pubkey),
replaceableEventsService.requestEvent(readRelays, kinds.Contacts, person.pubkey),
);
}, [contactsPubkeys, readRelays.urls.join("|")]);

View File

@ -1,5 +1,5 @@
import { PIN_LIST_KIND, getEventPointersFromList } from "../helpers/nostr/lists";
import { RequestOptions } from "../services/replaceable-event-requester";
import { RequestOptions } from "../services/replaceable-events";
import useCurrentAccount from "./use-current-account";
import useReplaceableEvent from "./use-replaceable-event";

View File

@ -8,7 +8,7 @@ import NostrPublishAction from "../../classes/nostr-publish-action";
import clientRelaysService from "../../services/client-relays";
import RelaySet from "../../classes/relay-set";
import { addPubkeyRelayHints, getAllRelayHints, isReplaceable } from "../../helpers/nostr/events";
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
import replaceableEventsService from "../../services/replaceable-events";
import eventExistsService from "../../services/event-exists";
import eventReactionsService from "../../services/event-reactions";
import { localRelay } from "../../services/local-relay";
@ -66,8 +66,8 @@ export default function PublishProvider({ children }: PropsWithChildren) {
const pub = new NostrPublishAction(label, relays, signed);
setLog((arr) => arr.concat(pub));
pub.onResult.subscribe((result) => {
if (result.status) handleEventFromRelay(result.relay, signed);
pub.onResult.subscribe(({ relay, result }) => {
if (result[2]) handleEventFromRelay(relay, signed);
});
// send it to the local relay
@ -75,7 +75,7 @@ export default function PublishProvider({ children }: PropsWithChildren) {
// pass it to other services
eventExistsService.handleEvent(signed);
if (isReplaceable(signed.kind)) replaceableEventLoaderService.handleEvent(signed);
if (isReplaceable(signed.kind)) replaceableEventsService.handleEvent(signed);
if (signed.kind === kinds.Reaction) eventReactionsService.handleEvent(signed);
if (signed.kind === kinds.EventDeletion) deleteEventService.handleEvent(signed);
return pub;

View File

@ -5,7 +5,7 @@ import useCurrentAccount from "../../hooks/use-current-account";
import { getPubkeysFromList } from "../../helpers/nostr/lists";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import { NostrEvent } from "../../types/nostr-event";
import { NostrQuery } from "../../types/nostr-query";
import { NostrQuery } from "../../types/nostr-relay";
import useRouteSearchValue from "../../hooks/use-route-search-value";
export type ListId = "following" | "global" | string;

View File

@ -7,7 +7,7 @@ import NostrSubscription from "../classes/nostr-subscription";
import SuperMap from "../classes/super-map";
import { NostrEvent } from "../types/nostr-event";
import Subject from "../classes/subject";
import { NostrQuery } from "../types/nostr-query";
import { NostrQuery } from "../types/nostr-relay";
import { logger } from "../helpers/debug";
import db from "./db";
import createDefer, { Deferred } from "../classes/deferred";
@ -109,7 +109,7 @@ class ChannelMetadataRelayLoader {
};
if (query["#e"] && query["#e"].length > 0) this.log(`Updating query`, query["#e"].length);
this.subscription.setQuery(query);
this.subscription.setFilters([query]);
if (this.subscription.state !== NostrSubscription.OPEN) {
this.subscription.open();

View File

@ -2,7 +2,7 @@ import stringify from "json-stringify-deterministic";
import Subject from "../classes/subject";
import SuperMap from "../classes/super-map";
import { NostrRequestFilter } from "../types/nostr-query";
import { NostrRequestFilter } from "../types/nostr-relay";
import NostrRequest from "../classes/nostr-request";
import relayPoolService from "./relay-pool";

View File

@ -1,7 +1,7 @@
import stringify from "json-stringify-deterministic";
import Subject from "../classes/subject";
import { NostrRequestFilter } from "../types/nostr-query";
import { NostrRequestFilter } from "../types/nostr-relay";
import SuperMap from "../classes/super-map";
import NostrRequest from "../classes/nostr-request";
import relayScoreboardService from "./relay-scoreboard";

View File

@ -2,7 +2,7 @@ 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";
import { createCoordinate } from "./replaceable-events";
function pickBestRelays(relays: string[]) {
// ignore local relays

View File

@ -31,8 +31,8 @@ export function handleEventFromRelay(relay: Relay, event: NostrEvent) {
}
relayPoolService.onRelayCreated.subscribe((relay) => {
relay.onEvent.subscribe(({ body: event }) => {
handleEventFromRelay(relay, event);
relay.onEvent.subscribe((message) => {
handleEventFromRelay(relay, message[2]);
});
});

View File

@ -1,95 +1,7 @@
import Relay from "../classes/relay";
import Subject from "../classes/subject";
import { logger } from "../helpers/debug";
import { safeRelayUrl, validateRelayURL } from "../helpers/relay";
import RelayPool from "../classes/relay-pool";
import { offlineMode } from "./offline-mode";
export class RelayPoolService {
relays = new Map<string, Relay>();
relayClaims = new Map<string, Set<any>>();
onRelayCreated = new Subject<Relay>();
log = logger.extend("RelayPool");
getRelays() {
return Array.from(this.relays.values());
}
getRelayClaims(url: string | URL) {
url = validateRelayURL(url);
const key = url.toString();
if (!this.relayClaims.has(key)) {
this.relayClaims.set(key, new Set());
}
return this.relayClaims.get(key) as Set<any>;
}
requestRelay(url: string | URL, connect = true) {
url = validateRelayURL(url);
const key = url.toString();
if (!this.relays.has(key)) {
const newRelay = new Relay(key);
this.relays.set(key, newRelay);
this.onRelayCreated.next(newRelay);
}
const relay = this.relays.get(key) as Relay;
if (connect && !relay.okay) {
try {
relay.open();
} catch (e) {
this.log(`Failed to connect to ${relay.url}`);
this.log(e);
}
}
return relay;
}
pruneRelays() {
for (const [url, relay] of this.relays.entries()) {
const claims = this.getRelayClaims(url).size;
if (claims === 0) {
relay.close();
}
}
}
reconnectRelays() {
if (offlineMode.value) return;
for (const [url, relay] of this.relays.entries()) {
const claims = this.getRelayClaims(url).size;
if (!relay.okay && claims > 0) {
try {
relay.open();
} catch (e) {
this.log(`Failed to connect to ${relay.url}`);
this.log(e);
}
}
}
}
// id can be anything
addClaim(url: string | URL, id: any) {
url = validateRelayURL(url);
const key = url.toString();
this.getRelayClaims(key).add(id);
}
removeClaim(url: string | URL, id: any) {
url = validateRelayURL(url);
const key = url.toString();
this.getRelayClaims(key).delete(id);
}
get connectedCount() {
let count = 0;
for (const [url, relay] of this.relays.entries()) {
if (relay.connected) count++;
}
return count;
}
}
const relayPoolService = new RelayPoolService();
const relayPoolService = new RelayPool();
setInterval(() => {
if (document.visibilityState === "visible") {

View File

@ -1,12 +1,8 @@
import dayjs from "dayjs";
import debug, { Debugger } from "debug";
import _throttle from "lodash/throttle";
import { Filter } from "nostr-tools";
import NostrSubscription from "../classes/nostr-subscription";
import SuperMap from "../classes/super-map";
import { NostrEvent } from "../types/nostr-event";
import { NostrQuery } from "../types/nostr-query";
import { logger } from "../helpers/debug";
import { nameOrPubkey } from "./user-metadata";
import { getEventCoordinate } from "../helpers/nostr/events";
@ -15,9 +11,7 @@ import { localRelay } from "./local-relay";
import { relayRequest } from "../helpers/relay";
import EventStore from "../classes/event-store";
import Subject from "../classes/subject";
type Pubkey = string;
type Relay = string;
import BatchKindLoader, { createCoordinate } from "../classes/batch-kind-loader";
export type RequestOptions = {
/** Always request the event from the relays */
@ -31,123 +25,14 @@ export type RequestOptions = {
export function getHumanReadableCoordinate(kind: number, pubkey: string, d?: string) {
return `${kind}:${nameOrPubkey(pubkey)}${d ? ":" + d : ""}`;
}
export function createCoordinate(kind: number, pubkey: string, d?: string) {
return `${kind}:${pubkey}${d ? ":" + d : ""}`;
}
const RELAY_REQUEST_BATCH_TIME = 500;
/** This class is ued to batch requests to a single relay */
class ReplaceableEventRelayLoader {
private subscription: NostrSubscription;
events = new EventStore();
private requestNext = new Set<string>();
private requested = new Map<string, Date>();
log: Debugger;
constructor(relay: string, log?: Debugger) {
this.subscription = new NostrSubscription(relay, undefined, `replaceable-event-loader`);
this.subscription.onEvent.subscribe(this.handleEvent.bind(this));
this.subscription.onEOSE.subscribe(this.handleEOSE.bind(this));
this.log = log || debug("misc");
}
private handleEvent(event: NostrEvent) {
const cord = getEventCoordinate(event);
// remove the cord from the waiting list
this.requested.delete(cord);
const current = this.events.getEvent(cord);
if (!current || event.created_at > current.created_at) {
this.events.addEvent(event);
}
}
private handleEOSE() {
// relays says it has nothing left
this.requested.clear();
}
requestEvent(kind: number, pubkey: string, d?: string) {
const cord = createCoordinate(kind, pubkey, d);
const event = this.events.getEvent(cord);
if (!event) {
this.requestNext.add(cord);
this.updateThrottle();
}
return event;
}
updateThrottle = _throttle(this.update, RELAY_REQUEST_BATCH_TIME);
update() {
let needsUpdate = false;
for (const cord of this.requestNext) {
if (!this.requested.has(cord)) {
this.requested.set(cord, new Date());
needsUpdate = true;
}
}
this.requestNext.clear();
// prune requests
const timeout = dayjs().subtract(1, "minute");
for (const [cord, date] of this.requested) {
if (dayjs(date).isBefore(timeout)) {
this.requested.delete(cord);
needsUpdate = true;
}
}
// update the subscription
if (needsUpdate) {
if (this.requested.size > 0) {
const filters: Record<number, NostrQuery> = {};
for (const [cord] of this.requested) {
const [kindStr, pubkey, d] = cord.split(":") as [string, string] | [string, string, string];
const kind = parseInt(kindStr);
filters[kind] = filters[kind] || { kinds: [kind] };
const arr = (filters[kind].authors = filters[kind].authors || []);
arr.push(pubkey);
if (d) {
const arr = (filters[kind]["#d"] = filters[kind]["#d"] || []);
arr.push(d);
}
}
const query = Array.from(Object.values(filters));
this.log(
`Updating query`,
Array.from(Object.keys(filters))
.map((kind: string) => `kind ${kind}: ${filters[parseInt(kind)].authors?.length}`)
.join(", "),
);
this.subscription.setQuery(query);
if (this.subscription.state !== NostrSubscription.OPEN) {
this.subscription.open();
}
} else if (this.subscription.state === NostrSubscription.OPEN) {
this.subscription.close();
}
}
}
}
const READ_CACHE_BATCH_TIME = 250;
const WRITE_CACHE_BATCH_TIME = 250;
class ReplaceableEventLoaderService {
private subjects = new SuperMap<Pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
private loaders = new SuperMap<Relay, ReplaceableEventRelayLoader>((relay) => {
const loader = new ReplaceableEventRelayLoader(relay, this.log.extend(relay));
class ReplaceableEventsService {
private subjects = new SuperMap<string, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
private loaders = new SuperMap<string, BatchKindLoader>((relay) => {
const loader = new BatchKindLoader(relay, this.log.extend(relay));
loader.events.onEvent.subscribe((e) => this.handleEvent(e));
return loader;
});
@ -251,11 +136,11 @@ class ReplaceableEventLoaderService {
}
requestEvent(relays: Iterable<string>, kind: number, pubkey: string, d?: string, opts: RequestOptions = {}) {
const cord = createCoordinate(kind, pubkey, d);
const sub = this.subjects.get(cord);
const key = createCoordinate(kind, pubkey, d);
const sub = this.subjects.get(key);
if (!sub.value) {
this.loadFromCache(cord).then((loaded) => {
this.loadFromCache(key).then((loaded) => {
if (!loaded && !sub.value) this.requestEventFromRelays(relays, kind, pubkey, d);
});
}
@ -268,11 +153,11 @@ class ReplaceableEventLoaderService {
}
}
const replaceableEventLoaderService = new ReplaceableEventLoaderService();
const replaceableEventsService = new ReplaceableEventsService();
if (import.meta.env.DEV) {
//@ts-ignore
window.replaceableEventLoaderService = replaceableEventLoaderService;
window.replaceableEventsService = replaceableEventsService;
}
export default replaceableEventLoaderService;
export default replaceableEventsService;

View File

@ -5,7 +5,7 @@ import { DraftNostrEvent } from "../../types/nostr-event";
import SuperMap from "../../classes/super-map";
import { PersistentSubject } from "../../classes/subject";
import { AppSettings, defaultSettings, parseAppSettings } from "./migrations";
import replaceableEventLoaderService, { RequestOptions } from "../replaceable-event-requester";
import replaceableEventsService, { RequestOptions } from "../replaceable-events";
export const APP_SETTINGS_KIND = 30078;
export const SETTING_EVENT_IDENTIFIER = "nostrudel-settings";
@ -19,7 +19,7 @@ class UserAppSettings {
}
requestAppSettings(pubkey: string, relays: Iterable<string>, opts?: RequestOptions) {
const sub = this.parsedSubjects.get(pubkey);
const requestSub = replaceableEventLoaderService.requestEvent(
const requestSub = replaceableEventsService.requestEvent(
relays,
APP_SETTINGS_KIND,
pubkey,

View File

@ -4,7 +4,7 @@ import { logger } from "../helpers/debug";
import accountService from "./account";
import clientRelaysService from "./client-relays";
import { offlineMode } from "./offline-mode";
import replaceableEventLoaderService from "./replaceable-event-requester";
import replaceableEventsService from "./replaceable-events";
import userAppSettings from "./settings/user-app-settings";
import userMailboxesService from "./user-mailboxes";
import userMetadataService from "./user-metadata";
@ -15,7 +15,7 @@ function loadContactsList() {
const account = accountService.current.value!;
log("Loading contacts list");
replaceableEventLoaderService.requestEvent(
replaceableEventsService.requestEvent(
[...clientRelaysService.readRelays.value, COMMON_CONTACT_RELAY],
kinds.Contacts,
account.pubkey,

View File

@ -3,7 +3,7 @@ import { kinds } from "nostr-tools";
import { NostrEvent } from "../types/nostr-event";
import SuperMap from "../classes/super-map";
import Subject from "../classes/subject";
import replaceableEventLoaderService, { createCoordinate, RequestOptions } from "./replaceable-event-requester";
import replaceableEventsService, { createCoordinate, RequestOptions } from "./replaceable-events";
import RelaySet from "../classes/relay-set";
import { RelayMode } from "../classes/relay";
import { relaysFromContactsEvent } from "../helpers/nostr/contacts";
@ -30,17 +30,17 @@ function nip65ToUserMailboxes(event: NostrEvent): UserMailboxes {
class UserMailboxesService {
private subjects = new SuperMap<string, Subject<UserMailboxes>>((pubkey) =>
replaceableEventLoaderService.getEvent(kinds.RelayList, pubkey).map(nip65ToUserMailboxes),
replaceableEventsService.getEvent(kinds.RelayList, pubkey).map(nip65ToUserMailboxes),
);
getMailboxes(pubkey: string) {
return this.subjects.get(pubkey);
}
requestMailboxes(pubkey: string, relays: Iterable<string>, opts: RequestOptions = {}) {
const sub = this.subjects.get(pubkey);
replaceableEventLoaderService.requestEvent(relays, kinds.RelayList, pubkey, undefined, opts);
replaceableEventsService.requestEvent(relays, kinds.RelayList, pubkey, undefined, opts);
// also fetch the relays from the users contacts
const contactsSub = replaceableEventLoaderService.requestEvent(relays, kinds.Contacts, pubkey, undefined, opts);
const contactsSub = replaceableEventsService.requestEvent(relays, kinds.Contacts, pubkey, undefined, opts);
sub.connectWithMapper(contactsSub, (event, next, value) => {
// NOTE: only use relays from contact list if the user dose not have a NIP-65 relay list
const relays = relaysFromContactsEvent(event);
@ -61,12 +61,12 @@ class UserMailboxesService {
async loadFromCache(pubkey: string) {
const sub = this.subjects.get(pubkey);
await replaceableEventLoaderService.loadFromCache(createCoordinate(kinds.RelayList, pubkey));
await replaceableEventsService.loadFromCache(createCoordinate(kinds.RelayList, pubkey));
return sub;
}
receiveEvent(event: NostrEvent) {
replaceableEventLoaderService.handleEvent(event);
replaceableEventsService.handleEvent(event);
}
}

View File

@ -4,18 +4,18 @@ import _throttle from "lodash.throttle";
import { Kind0ParsedContent, parseKind0Event } from "../helpers/user-metadata";
import SuperMap from "../classes/super-map";
import Subject from "../classes/subject";
import replaceableEventLoaderService, { RequestOptions } from "./replaceable-event-requester";
import replaceableEventsService, { RequestOptions } from "./replaceable-events";
class UserMetadataService {
private metadata = new SuperMap<string, Subject<Kind0ParsedContent>>((pubkey) => {
return replaceableEventLoaderService.getEvent(0, pubkey).map(parseKind0Event);
return replaceableEventsService.getEvent(0, pubkey).map(parseKind0Event);
});
getSubject(pubkey: string) {
return this.metadata.get(pubkey);
}
requestMetadata(pubkey: string, relays: Iterable<string>, opts: RequestOptions = {}) {
const subject = this.metadata.get(pubkey);
replaceableEventLoaderService.requestEvent(relays, kinds.Metadata, pubkey, undefined, opts);
replaceableEventsService.requestEvent(relays, kinds.Metadata, pubkey, undefined, opts);
return subject;
}
}

View File

@ -1,7 +1,7 @@
import _throttle from "lodash.throttle";
import { getSearchNames } from "../helpers/user-metadata";
import db from "./db";
import replaceableEventLoaderService from "./replaceable-event-requester";
import replaceableEventsService from "./replaceable-events";
import userMetadataService from "./user-metadata";
const WRITE_USER_SEARCH_BATCH_TIME = 500;
@ -25,7 +25,7 @@ const writeSearchData = _throttle(async () => {
await transaction.done;
}, WRITE_USER_SEARCH_BATCH_TIME);
replaceableEventLoaderService.events.onEvent.subscribe((event) => {
replaceableEventsService.events.onEvent.subscribe((event) => {
if (event.kind === 0) {
writeSearchQueue.add(event.pubkey);
writeSearchData();

View File

@ -12,25 +12,9 @@ export type Tag = string[] | ETag | PTag | RTag | DTag | ATag | ExpirationTag;
export type NostrEvent = Omit<NostrToolsNostrEvent, "tags"> & {
tags: Tag[];
};
export type CountResponse = {
count: number;
approximate?: boolean;
};
export type DraftNostrEvent = Omit<NostrEvent, "pubkey" | "id" | "sig"> & { pubkey?: string; id?: string };
export type RawIncomingEvent = ["EVENT", string, NostrEvent];
export type RawIncomingNotice = ["NOTICE", string];
export type RawIncomingCount = ["COUNT", string, CountResponse];
export type RawIncomingEOSE = ["EOSE", string];
export type RawIncomingCommandResult = ["OK", string, boolean, string];
export type RawIncomingNostrEvent =
| RawIncomingEvent
| RawIncomingNotice
| RawIncomingCount
| RawIncomingEOSE
| RawIncomingCommandResult;
export function isETag(tag: Tag): tag is ETag {
return tag[0] === "e" && tag[1] !== undefined;
}

View File

@ -1,16 +0,0 @@
import { Filter } from "nostr-tools";
import { NostrEvent } from "./nostr-event";
export type NostrOutgoingEvent = ["EVENT", NostrEvent];
export type NostrOutgoingRequest = ["REQ", string, ...Filter[]];
export type NostrOutgoingCount = ["COUNT", string, ...Filter[]];
export type NostrOutgoingClose = ["CLOSE", string];
export type NostrOutgoingMessage = NostrOutgoingEvent | NostrOutgoingRequest | NostrOutgoingClose | NostrOutgoingCount;
/** @deprecated use Filter instead */
export type NostrQuery = Filter;
export type NostrRequestFilter = Filter | Filter[];
export type RelayQueryMap = Record<string, NostrRequestFilter>;

8
src/types/nostr-relay.ts Normal file
View File

@ -0,0 +1,8 @@
import { Filter } from "nostr-tools";
/** @deprecated use Filter instead */
export type NostrQuery = Filter;
export type NostrRequestFilter = Filter | Filter[];
export type RelayQueryMap = Record<string, NostrRequestFilter>;

View File

@ -9,7 +9,7 @@ import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities";
import { ErrorBoundary } from "../../components/error-boundary";
import { useReadRelays } from "../../hooks/use-client-relays";
import useSubjects from "../../hooks/use-subjects";
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
import replaceableEventsService from "../../services/replaceable-events";
import { COMMUNITIES_LIST_KIND, getCoordinatesFromList } from "../../helpers/nostr/lists";
import { useNavigate } from "react-router-dom";
import { ChevronLeftIcon } from "../../components/icons";
@ -20,9 +20,7 @@ import { AddressPointer } from "nostr-tools/lib/types/nip19";
export function useUsersJoinedCommunitiesLists(pubkeys: string[], additionalRelays?: Iterable<string>) {
const readRelays = useReadRelays(additionalRelays);
const communityListsSubjects = useMemo(() => {
return pubkeys.map((pubkey) =>
replaceableEventLoaderService.requestEvent(readRelays, COMMUNITIES_LIST_KIND, pubkey),
);
return pubkeys.map((pubkey) => replaceableEventsService.requestEvent(readRelays, COMMUNITIES_LIST_KIND, pubkey));
}, [pubkeys]);
return useSubjects(communityListsSubjects);
}

View File

@ -35,7 +35,7 @@ import {
getCommunityMods,
getCommunityName,
} from "../../helpers/nostr/communities";
import { createCoordinate } from "../../services/replaceable-event-requester";
import { createCoordinate } from "../../services/replaceable-events";
import { getImageSize } from "../../helpers/image";
import { useReadRelays } from "../../hooks/use-client-relays";
import useTimelineLoader from "../../hooks/use-timeline-loader";

View File

@ -29,7 +29,7 @@ import {
import { getSharableEventAddress } from "../../../helpers/nip19";
import { NostrEvent } from "../../../types/nostr-event";
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
import { createCoordinate } from "../../../services/replaceable-event-requester";
import { createCoordinate } from "../../../services/replaceable-events";
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
import ListFavoriteButton from "./list-favorite-button";
import { getEventUID } from "../../../helpers/nostr/events";

View File

@ -39,7 +39,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import EventStore from "../../../classes/event-store";
import NostrRequest from "../../../classes/nostr-request";
import { sortByDate } from "../../../helpers/nostr/events";
import { NostrQuery } from "../../../types/nostr-query";
import { NostrQuery } from "../../../types/nostr-relay";
ChartJS.register(
ArcElement,

View File

@ -2,7 +2,7 @@ import { useMemo } from "react";
import { Card, CardBody, CardHeader, CardProps, Heading, Image, LinkBox, LinkOverlay } from "@chakra-ui/react";
import { useReadRelays } from "../../../hooks/use-client-relays";
import replaceableEventLoaderService from "../../../services/replaceable-event-requester";
import replaceableEventsService from "../../../services/replaceable-events";
import useSubject from "../../../hooks/use-subject";
import { NoteContents } from "../../../components/note/text-note-contents";
import { isATag } from "../../../types/nostr-event";
@ -15,7 +15,7 @@ export const STREAMER_CARD_TYPE = 37777;
function useStreamerCardsCords(pubkey: string, relays: Iterable<string>) {
const sub = useMemo(
() => replaceableEventLoaderService.requestEvent(relays, STREAMER_CARDS_TYPE, pubkey),
() => replaceableEventsService.requestEvent(relays, STREAMER_CARDS_TYPE, pubkey),
[pubkey, relays],
);
const streamerCards = useSubject(sub);

View File

@ -12,7 +12,7 @@ import PeopleListSelection from "../../components/people-list-selection/people-l
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
import useParsedStreams from "../../hooks/use-parsed-streams";
import { NostrRequestFilter } from "../../types/nostr-query";
import { NostrRequestFilter } from "../../types/nostr-relay";
import { useAppTitle } from "../../hooks/use-app-title";
import { NostrEvent } from "../../types/nostr-event";
import VerticalPageLayout from "../../components/vertical-page-layout";

View File

@ -31,7 +31,7 @@ import StreamSummaryContent from "../components/stream-summary-content";
import { ChevronLeftIcon, ExternalLinkIcon } from "../../../components/icons";
import useSetColorMode from "../../../hooks/use-set-color-mode";
import { CopyIconButton } from "../../../components/copy-icon-button";
import replaceableEventLoaderService from "../../../services/replaceable-event-requester";
import replaceableEventsService from "../../../services/replaceable-events";
import useSubject from "../../../hooks/use-subject";
import StreamerCards from "../components/streamer-cards";
import { useAppTitle } from "../../../hooks/use-app-title";
@ -251,7 +251,7 @@ export default function StreamView() {
if (parsed.data.kind !== STREAM_KIND) throw new Error("Invalid stream kind");
const addrRelays = parsed.data.relays ?? [];
return replaceableEventLoaderService.requestEvent(
return replaceableEventsService.requestEvent(
unique([...readRelays, ...streamRelays, ...addrRelays]),
parsed.data.kind,
parsed.data.pubkey,

View File

@ -6,7 +6,7 @@ import { ParsedStream, STREAM_CHAT_MESSAGE_KIND, getATag } from "../../../../hel
import useTimelineLoader from "../../../../hooks/use-timeline-loader";
import { NostrEvent } from "../../../../types/nostr-event";
import useStreamGoal from "../../../../hooks/use-stream-goal";
import { NostrQuery } from "../../../../types/nostr-query";
import { NostrQuery } from "../../../../types/nostr-relay";
import useUserMuteFilter from "../../../../hooks/use-user-mute-filter";
import useClientSideMuteFilter from "../../../../hooks/use-client-side-mute-filter";
import { useReadRelays } from "../../../../hooks/use-client-relays";

View File

@ -19,7 +19,7 @@ import { useUsersMetadata } from "../../hooks/use-user-network";
import { MUTE_LIST_KIND, getPubkeysFromList, isPubkeyInList } from "../../helpers/nostr/lists";
import useUserContactList from "../../hooks/use-user-contact-list";
import { useReadRelays } from "../../hooks/use-client-relays";
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
import replaceableEventsService from "../../services/replaceable-events";
import useSubjects from "../../hooks/use-subjects";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import { useNavigate } from "react-router-dom";
@ -28,7 +28,7 @@ import { ChevronLeftIcon } from "../../components/icons";
export function useUsersMuteLists(pubkeys: string[], additionalRelays?: Iterable<string>) {
const readRelays = useReadRelays(additionalRelays);
const muteListSubjects = useMemo(() => {
return pubkeys.map((pubkey) => replaceableEventLoaderService.requestEvent(readRelays, MUTE_LIST_KIND, pubkey));
return pubkeys.map((pubkey) => replaceableEventsService.requestEvent(readRelays, MUTE_LIST_KIND, pubkey));
}, [pubkeys]);
return useSubjects(muteListSubjects);
}