Use Relay class from nostr-tools

upgrade nostr-tools from 2.1.3 to 2.4.0
improve parsing zaps
This commit is contained in:
hzrd149 2024-04-10 17:59:30 -05:00
parent 71b4c4e1eb
commit 781948afd6
40 changed files with 379 additions and 619 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Use Relay class from nostr-tools

View File

@ -62,7 +62,7 @@
"nanoid": "^5.0.4",
"ngeohash": "^0.6.3",
"nostr-idb": "^2.1.1",
"nostr-tools": "^2.1.3",
"nostr-tools": "^2.4.0",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",

View File

@ -1,9 +1,8 @@
import dayjs from "dayjs";
import { Filter, NostrEvent } from "nostr-tools";
import { Filter, NostrEvent, Relay, Subscription } 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/event";
@ -15,20 +14,17 @@ 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;
private subscription: Subscription | null = null;
events = new EventStore();
relay: Relay;
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));
constructor(relay: Relay, log?: Debugger) {
this.relay = relay;
this.log = log || debug("RelayBatchLoader");
}
@ -105,12 +101,17 @@ export default class BatchKindLoader {
.map((kind: string) => `kind ${kind}: ${filters[parseInt(kind)].authors?.length}`)
.join(", "),
);
this.subscription.setFilters(query);
if (this.subscription.state !== NostrSubscription.OPEN) {
this.subscription.open();
if (!this.subscription || this.subscription.closed) {
this.subscription = this.relay.subscribe(query, {
onevent: (event) => this.handleEvent(event),
oneose: () => this.handleEOSE(),
});
} else {
this.subscription.filters = query;
this.subscription.fire();
}
} else if (this.subscription.state === NostrSubscription.OPEN) {
} else if (this.subscription && !this.subscription.closed) {
this.subscription.close();
}
}

View File

@ -1,21 +1,21 @@
import { Debugger } from "debug";
import { Filter, NostrEvent, matchFilters } from "nostr-tools";
import { Filter, NostrEvent, Relay, matchFilters } from "nostr-tools";
import _throttle from "lodash.throttle";
import NostrRequest from "./nostr-request";
import Subject from "./subject";
import { logger } from "../helpers/debug";
import EventStore from "./event-store";
import deleteEventService from "../services/delete-events";
import { mergeFilter } from "../helpers/nostr/filter";
import { isATag, isETag } from "../types/nostr-event";
import relayPoolService from "../services/relay-pool";
const DEFAULT_CHUNK_SIZE = 100;
export type EventFilter = (event: NostrEvent, store: EventStore) => boolean;
export default class ChunkedRequest {
relay: string;
relay: Relay;
filters: Filter[];
chunkSize = DEFAULT_CHUNK_SIZE;
private log: Debugger;
@ -28,12 +28,12 @@ export default class ChunkedRequest {
onChunkFinish = new Subject<number>();
constructor(relay: string, filters: Filter[], log?: Debugger) {
constructor(relay: Relay, filters: Filter[], log?: Debugger) {
this.relay = relay;
this.filters = filters;
this.log = log || logger.extend(relay);
this.events = new EventStore(relay);
this.log = log || logger.extend(relay.url);
this.events = new EventStore(relay.url);
// TODO: find a better place for this
this.subs.push(deleteEventService.stream.subscribe((e) => this.handleDeleteEvent(e)));
@ -47,23 +47,25 @@ export default class ChunkedRequest {
filters = mergeFilter(filters, { until: oldestEvent.created_at - 1 });
}
const request = new NostrRequest([this.relay]);
relayPoolService.addClaim(this.relay, this);
let gotEvents = 0;
request.onEvent.subscribe((e) => {
this.handleEvent(e);
gotEvents++;
const sub = this.relay.subscribe(filters, {
onevent: (event) => {
this.handleEvent(event);
gotEvents++;
},
oneose: () => {
this.loading = false;
if (gotEvents === 0) {
this.complete = true;
this.log("Complete");
} else this.log(`Got ${gotEvents} events`);
this.onChunkFinish.next(gotEvents);
sub.close();
relayPoolService.removeClaim(this.relay, this);
},
});
request.onComplete.then(() => {
this.loading = false;
if (gotEvents === 0) {
this.complete = true;
this.log("Complete");
} else this.log(`Got ${gotEvents} events`);
this.onChunkFinish.next(gotEvents);
});
request.start(filters);
}
private handleEvent(event: NostrEvent) {

View File

@ -1,12 +1,11 @@
import { nanoid } from "nanoid";
import { NostrEvent } from "../types/nostr-event";
import { NostrRequestFilter, RelayQueryMap } from "../types/nostr-relay";
import Relay, { IncomingEvent, OutgoingRequest } from "./relay";
import { RelayQueryMap } from "../types/nostr-relay";
import relayPoolService from "../services/relay-pool";
import { isFilterEqual, isQueryMapEqual } from "../helpers/nostr/filter";
import ControlledObservable from "./controlled-observable";
import SuperMap from "./super-map";
import { Relay, Subscription } from "nostr-tools";
export default class NostrMultiSubscription {
static INIT = "initial";
@ -18,6 +17,8 @@ export default class NostrMultiSubscription {
queryMap: RelayQueryMap = {};
relays: Relay[] = [];
subscriptions = new Map<Relay, Subscription>();
state = NostrMultiSubscription.INIT;
onEvent = new ControlledObservable<NostrEvent>();
seenEvents = new Set<string>();
@ -26,38 +27,17 @@ export default class NostrMultiSubscription {
this.id = nanoid();
this.name = name;
}
private handleMessage(incomingEvent: IncomingEvent) {
if (
this.state === NostrMultiSubscription.OPEN &&
incomingEvent[1] === this.id &&
!this.seenEvents.has(incomingEvent[2].id)
) {
this.onEvent.next(incomingEvent[2]);
this.seenEvents.add(incomingEvent[2].id);
}
private handleEvent(event: NostrEvent) {
if (this.seenEvents.has(event.id)) return;
this.onEvent.next(event);
this.seenEvents.add(event.id);
}
private relaySubs = new SuperMap<Relay, ZenObservable.Subscription[]>(() => []);
/** listen for event and open events from relays */
private connectToRelay(relay: Relay) {
const subs = this.relaySubs.get(relay);
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)));
private handleAddRelay(relay: Relay) {
relayPoolService.addClaim(relay.url, this);
}
/** stop listing to events from relays */
private disconnectFromRelay(relay: Relay) {
const subs = this.relaySubs.get(relay);
for (const sub of subs) sub.unsubscribe();
this.relaySubs.delete(relay);
private handleRemoveRelay(relay: Relay) {
relayPoolService.removeClaim(relay.url, this);
// if the subscription is open and had sent a request to the relay
if (this.state === NostrMultiSubscription.OPEN && this.relayQueries.has(relay)) {
relay.send(["CLOSE", this.id]);
}
this.relayQueries.delete(relay);
}
setQueryMap(queryMap: RelayQueryMap) {
@ -70,7 +50,7 @@ export default class NostrMultiSubscription {
// add relay
const relay = relayPoolService.requestRelay(url);
this.relays.push(relay);
this.connectToRelay(relay);
this.handleAddRelay(relay);
}
}
for (const url of Object.keys(this.queryMap)) {
@ -78,41 +58,51 @@ export default class NostrMultiSubscription {
const relay = this.relays.find((r) => r.url === url);
if (!relay) continue;
this.relays = this.relays.filter((r) => r !== relay);
this.disconnectFromRelay(relay);
this.handleRemoveRelay(relay);
}
}
this.queryMap = queryMap;
this.updateRelayQueries();
this.updateSubscriptions();
}
private relayQueries = new WeakMap<Relay, NostrRequestFilter>();
private updateRelayQueries() {
if (this.state !== NostrMultiSubscription.OPEN) return;
private updateSubscriptions() {
// close all subscriptions if not open
if (this.state !== NostrMultiSubscription.OPEN) {
for (const [relay, subscription] of this.subscriptions) {
subscription.close();
}
this.subscriptions.clear();
return;
}
// else open and update subscriptions
for (const relay of this.relays) {
const filter = this.queryMap[relay.url];
const message: OutgoingRequest = Array.isArray(filter) ? ["REQ", this.id, ...filter] : ["REQ", this.id, filter];
const filters = this.queryMap[relay.url];
const currentFilter = this.relayQueries.get(relay);
if (!currentFilter || !isFilterEqual(currentFilter, filter)) {
this.relayQueries.set(relay, filter);
relay.send(message);
let subscription = this.subscriptions.get(relay);
if (!subscription || !isFilterEqual(subscription.filters, filters)) {
if (subscription) {
subscription.filters = filters;
subscription.fire();
} else {
subscription = relay.subscribe(filters, {
onevent: (event) => this.handleEvent(event),
onclose: () => {
if (this.subscriptions.get(relay) === subscription) {
this.subscriptions.delete(relay);
}
},
});
this.subscriptions.set(relay, subscription);
}
}
}
}
private handleRelayConnect(relay: Relay) {
this.updateRelayQueries();
}
private handleRelayDisconnect(relay: Relay) {
this.relayQueries.delete(relay);
}
sendAll(event: NostrEvent) {
for (const relay of this.relays) {
relay.send(["EVENT", event]);
}
publish(event: NostrEvent) {
return Promise.all(this.relays.map((r) => r.publish(event)));
}
open() {
@ -120,14 +110,14 @@ export default class NostrMultiSubscription {
this.state = NostrMultiSubscription.OPEN;
// reconnect to all relays
for (const relay of this.relays) this.connectToRelay(relay);
for (const relay of this.relays) this.handleAddRelay(relay);
// send queries
this.updateRelayQueries();
this.updateSubscriptions();
return this;
}
waitForConnection(): Promise<void> {
return Promise.all(this.relays.map((r) => r.waitForConnection())).then((v) => void 0);
waitForAllConnection(): Promise<void> {
return Promise.all(this.relays.filter((r) => !r.connected).map((r) => r.connect())).then((v) => void 0);
}
close() {
if (this.state !== NostrMultiSubscription.OPEN) return this;
@ -135,7 +125,7 @@ export default class NostrMultiSubscription {
// forget all seen events
this.forgetEvents();
// unsubscribe from relay messages
for (const relay of this.relays) this.disconnectFromRelay(relay);
for (const relay of this.relays) this.handleRemoveRelay(relay);
// set state
this.state = NostrMultiSubscription.CLOSED;

View File

@ -1,12 +1,12 @@
import { nanoid } from "nanoid";
import { NostrEvent } from "nostr-tools";
import { NostrEvent, Relay } from "nostr-tools";
import relayPoolService from "../services/relay-pool";
import createDefer from "./deferred";
import Relay, { IncomingCommandResult } from "./relay";
import { PersistentSubject } from "./subject";
import ControlledObservable from "./controlled-observable";
import SuperMap from "./super-map";
type Result = { relay: Relay; success: boolean; message: string };
export default class NostrPublishAction {
id = nanoid();
@ -14,13 +14,12 @@ export default class NostrPublishAction {
relays: string[];
event: NostrEvent;
results = new PersistentSubject<{ relay: Relay; result: IncomingCommandResult }[]>([]);
results = new PersistentSubject<Result[]>([]);
onResult = new ControlledObservable<{ relay: Relay; result: IncomingCommandResult }>();
onComplete = createDefer<{ relay: Relay; result: IncomingCommandResult }[]>();
onResult = new ControlledObservable<Result>();
onComplete = createDefer<Result[]>();
private remaining = new Set<Relay>();
private relayResultSubs = new SuperMap<Relay, ZenObservable.Subscription[]>(() => []);
constructor(label: string, relays: Iterable<string>, event: NostrEvent, timeout: number = 5000) {
this.label = label;
@ -30,31 +29,30 @@ 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((result) => {
if (result[1] === this.event.id) this.handleResult(result, relay);
}),
);
relay.send(["EVENT", event]);
relay
.publish(event)
.then((result) => this.handleResult(event.id, true, result, relay))
.catch((err) => {
if (err instanceof Error) this.handleResult(event.id, false, err.message, relay);
});
}
setTimeout(this.handleTimeout.bind(this), timeout);
}
private handleResult(result: IncomingCommandResult, relay: Relay) {
this.results.next([...this.results.value, { relay, result }]);
this.onResult.next({ relay, result });
private handleResult(id: string, success: boolean, message: string, relay: Relay) {
const result: Result = { relay, success, message };
this.results.next([...this.results.value, 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);
}
private handleTimeout() {
for (const relay of this.remaining) {
this.handleResult(["OK", this.event.id, false, "Timeout"], relay);
this.handleResult(this.event.id, false, "Timeout", relay);
}
}
}

View File

@ -1,95 +0,0 @@
import { nanoid } from "nanoid";
import { Filter, NostrEvent } from "nostr-tools";
import relayPoolService from "../services/relay-pool";
import Relay, { CountResponse, IncomingCount, IncomingEOSE, IncomingEvent } from "./relay";
import createDefer from "./deferred";
import ControlledObservable from "./controlled-observable";
import SuperMap from "./super-map";
const REQUEST_DEFAULT_TIMEOUT = 1000 * 5;
export default class NostrRequest {
static IDLE = "idle";
static RUNNING = "running";
static COMPLETE = "complete";
id = nanoid();
timeout: number;
relays: Set<Relay>;
state = NostrRequest.IDLE;
onEvent = new ControlledObservable<NostrEvent>();
onCount = new ControlledObservable<CountResponse>();
onComplete = createDefer<void>();
seenEvents = new Set<string>();
private relaySubs: SuperMap<Relay, ZenObservable.Subscription[]> = new SuperMap(() => []);
constructor(relayUrls: Iterable<string>, timeout?: number) {
this.relays = new Set(Array.from(relayUrls).map((url) => relayPoolService.requestRelay(url)));
for (const relay of this.relays) {
const subs = this.relaySubs.get(relay);
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)));
}
this.timeout = timeout ?? REQUEST_DEFAULT_TIMEOUT;
}
handleEOSE(message: IncomingEOSE, relay: Relay) {
if (message[1] === this.id) {
this.relays.delete(relay);
relay.send(["CLOSE", this.id]);
this.relaySubs.get(relay).forEach((sub) => sub.unsubscribe());
this.relaySubs.delete(relay);
if (this.relays.size === 0) {
this.state = NostrRequest.COMPLETE;
this.onComplete.resolve();
}
}
}
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[1] === this.id) {
this.onCount.next(incomingCount[2]);
}
}
start(filter: Filter | Filter[], type: "REQ" | "COUNT" = "REQ") {
if (this.state !== NostrRequest.IDLE) {
throw new Error("cant restart a nostr request");
}
this.state = NostrRequest.RUNNING;
for (const relay of this.relays) {
if (Array.isArray(filter)) {
relay.send([type, this.id, ...filter]);
} else relay.send([type, this.id, filter]);
}
setTimeout(() => this.complete(), this.timeout);
return this;
}
complete() {
if (this.state === NostrRequest.COMPLETE) return this;
this.state = NostrRequest.COMPLETE;
for (const relay of this.relays) {
relay.send(["CLOSE", this.id]);
this.relaySubs.get(relay).forEach((sub) => sub.unsubscribe());
}
this.relaySubs.clear();
this.onComplete.resolve();
return this;
}
}

View File

@ -1,10 +1,10 @@
import { nanoid } from "nanoid";
import { Filter, NostrEvent } from "nostr-tools";
import { Filter, NostrEvent, Relay, Subscription } from "nostr-tools";
import Relay, { IncomingEOSE, OutgoingMessage } from "./relay";
import relayPoolService from "../services/relay-pool";
import ControlledObservable from "./controlled-observable";
/** @deprecated use relay.subscribe instead */
export default class NostrSubscription {
static INIT = "initial";
static OPEN = "open";
@ -16,10 +16,10 @@ export default class NostrSubscription {
relay: Relay;
state = NostrSubscription.INIT;
onEvent = new ControlledObservable<NostrEvent>();
onEOSE = new ControlledObservable<IncomingEOSE>();
subscription: Subscription | null = null;
private subs: ZenObservable.Subscription[] = [];
onEvent = new ControlledObservable<NostrEvent>();
onEOSE = new ControlledObservable<number>();
constructor(relayUrl: string | URL, filters?: Filter[], name?: string) {
this.id = nanoid();
@ -27,28 +27,13 @@ export default class NostrSubscription {
this.name = name;
this.relay = relayPoolService.requestRelay(relayUrl);
this.subs.push(
this.relay.onEvent.subscribe((message) => {
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[1] === this.id) this.onEOSE.next(eose);
}),
);
}
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]);
if (this.state === NostrSubscription.OPEN && this.subscription) {
this.subscription.filters = this.filters;
this.subscription.fire();
}
return this;
}
@ -58,7 +43,10 @@ export default class NostrSubscription {
if (this.state === NostrSubscription.OPEN) return this;
this.state = NostrSubscription.OPEN;
this.send(["REQ", this.id, ...this.filters]);
this.subscription = this.relay.subscribe(this.filters, {
onevent: (event) => this.onEvent.next(event),
oneose: () => this.onEOSE.next(Math.random()),
});
relayPoolService.addClaim(this.relay.url, this);
@ -70,13 +58,10 @@ export default class NostrSubscription {
// set state
this.state = NostrSubscription.CLOSED;
// send close message
this.send(["CLOSE", this.id]);
this.subscription?.close();
// unsubscribe from relay messages
relayPoolService.removeClaim(this.relay.url, this);
for (const sub of this.subs) sub.unsubscribe();
this.subs = [];
return this;
}
}

View File

@ -134,7 +134,7 @@ export default class AccountNotifications {
break;
}
case NotificationType.Zap:
const parsed = getParsedZap(e);
const parsed = getParsedZap(e, true, true);
if (parsed instanceof Error) break;
if (!parsed.payment.amount) break;
timeline.push(e);

View File

@ -1,7 +1,7 @@
import { Relay } from "nostr-tools";
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 {
@ -34,9 +34,9 @@ export default class RelayPool {
}
const relay = this.relays.get(key) as Relay;
if (connect && !relay.okay) {
if (connect && !relay.connected) {
try {
relay.open();
relay.connect();
} catch (e) {
this.log(`Failed to connect to ${relay.url}`);
this.log(e);
@ -58,9 +58,9 @@ export default class RelayPool {
for (const [url, relay] of this.relays.entries()) {
const claims = this.getRelayClaims(url).size;
if (!relay.okay && claims > 0) {
if (!relay.connected && claims > 0) {
try {
relay.open();
relay.connect();
} catch (e) {
this.log(`Failed to connect to ${relay.url}`);
this.log(e);
@ -69,14 +69,12 @@ export default class RelayPool {
}
}
addClaim(url: string | URL, id: any) {
url = validateRelayURL(url);
const key = url.toString();
addClaim(relay: string | URL | Relay, id: any) {
const key = relay instanceof Relay ? relay.url : validateRelayURL(relay).toString();
this.getRelayClaims(key).add(id);
}
removeClaim(url: string | URL, id: any) {
url = validateRelayURL(url);
const key = url.toString();
removeClaim(relay: string | URL | Relay, id: any) {
const key = relay instanceof Relay ? relay.url : validateRelayURL(relay).toString();
this.getRelayClaims(key).delete(id);
}

View File

@ -1,211 +1,6 @@
import { Filter, NostrEvent } from "nostr-tools";
import { offlineMode } from "../services/offline-mode";
import relayScoreboardService from "../services/relay-scoreboard";
import ControlledObservable from "./controlled-observable";
import createDefer, { Deferred } from "./deferred";
import { PersistentSubject } from "./subject";
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,
WRITE = 2,
ALL = 1 | 2,
}
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>();
private connectionPromises: Deferred<void>[] = [];
private connectionTimer?: () => void;
private ejectTimer?: () => void;
private intentionalClose = false;
private subscriptionResTimer = new Map<string, () => void>();
private queue: OutgoingMessage[] = [];
constructor(url: string) {
this.url = url;
}
open() {
if (offlineMode.value) return;
if (this.okay) return;
this.intentionalClose = false;
this.ws = new WebSocket(this.url);
this.connectionTimer = relayScoreboardService.relayConnectionTime.get(this.url).createTimer();
const connectionTimeout: number = window.setTimeout(() => {
// end the connection timer after CONNECTION_TIMEOUT
if (this.connectionTimer) {
this.connectionTimer();
this.connectionTimer = undefined;
for (const p of this.connectionPromises) p.reject();
this.connectionPromises = [];
}
// relayScoreboardService.relayTimeouts.get(this.url).addIncident();
}, CONNECTION_TIMEOUT);
// for local dev, cancel timeout if module reloads
if (import.meta.hot) {
import.meta.hot.prune(() => {
window.clearTimeout(connectionTimeout);
this.ws?.close();
});
}
this.ws.onopen = () => {
window.clearTimeout(connectionTimeout);
this.onOpen.next(this);
this.status.next(this.ws!.readyState);
this.ejectTimer = relayScoreboardService.relayEjectTime.get(this.url).createTimer();
if (this.connectionTimer) {
this.connectionTimer();
this.connectionTimer = undefined;
}
this.sendQueued();
for (const p of this.connectionPromises) p.resolve();
this.connectionPromises = [];
};
this.ws.onclose = () => {
this.onClose.next(this);
this.status.next(this.ws!.readyState);
if (!this.intentionalClose && this.ejectTimer) {
this.ejectTimer();
this.ejectTimer = undefined;
}
};
this.ws.onmessage = this.handleMessage.bind(this);
}
send(json: OutgoingMessage) {
if (this.connected) {
this.ws?.send(JSON.stringify(json));
// record start time
if (json[0] === "REQ" || json[0] === "COUNT") {
this.startSubResTimer(json[1]);
}
} else this.queue.push(json);
}
close() {
this.ws?.close();
this.intentionalClose = true;
this.subscriptionResTimer.clear();
}
waitForConnection(): Promise<void> {
if (this.connected) return Promise.resolve();
const p = createDefer<void>();
this.connectionPromises.push(p);
return p;
}
private startSubResTimer(sub: string) {
this.subscriptionResTimer.set(sub, relayScoreboardService.relayResponseTimes.get(this.url).createTimer());
}
private endSubResTimer(sub: string) {
const endTimer = this.subscriptionResTimer.get(sub);
if (endTimer) {
endTimer();
this.subscriptionResTimer.delete(sub);
}
}
private sendQueued() {
if (this.connected) {
for (const message of this.queue) {
this.send(message);
}
this.queue = [];
}
}
get okay() {
return this.connected || this.connecting;
}
get connected() {
return this.ws?.readyState === WebSocket.OPEN;
}
get connecting() {
return this.ws?.readyState === WebSocket.CONNECTING;
}
get closing() {
return this.ws?.readyState === WebSocket.CLOSING;
}
get closed() {
return this.ws?.readyState === WebSocket.CLOSED;
}
get state() {
return this.ws?.readyState;
}
handleMessage(message: MessageEvent<string>) {
if (!message.data) return;
try {
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(data);
this.endSubResTimer(data[1]);
break;
case "NOTICE":
this.onNotice.next(data);
break;
case "COUNT":
this.onCount.next(data);
break;
case "EOSE":
this.onEOSE.next(data);
this.endSubResTimer(data[1]);
break;
case "OK":
this.onCommandResult.next(data);
break;
}
} catch (e) {
console.log(`Relay: Failed to parse massage from ${this.url}`);
console.log(message.data, e);
}
}
}

View File

@ -15,6 +15,7 @@ import { localRelay } from "../services/local-relay";
import { relayRequest } from "../helpers/relay";
import SuperMap from "./super-map";
import ChunkedRequest from "./chunked-request";
import relayPoolService from "../services/relay-pool";
const BLOCK_SIZE = 100;
@ -124,7 +125,11 @@ export default class TimelineLoader {
}
if (!this.chunkLoaders.has(relay)) {
const loader = new ChunkedRequest(relay, Array.isArray(filter) ? filter : [filter], this.log.extend(relay));
const loader = new ChunkedRequest(
relayPoolService.requestRelay(relay),
Array.isArray(filter) ? filter : [filter],
this.log.extend(relay),
);
this.chunkLoaders.set(relay, loader);
this.connectToChunkLoader(loader);
}

View File

@ -1,7 +1,6 @@
import { MouseEventHandler, MutableRefObject, forwardRef, useCallback, useMemo, useRef } from "react";
import { Image, ImageProps, Link, LinkProps } from "@chakra-ui/react";
import { useTrustContext } from "../../providers/local/trust";
import { EmbedableContent, defaultGetLocation } from "../../helpers/embeds";
import { getMatchLink } from "../../helpers/regexp";
import { useRegisterSlide } from "../lightbox-provider";
@ -100,7 +99,7 @@ export const GalleryImage = forwardRef<HTMLImageElement, EmbeddedImageProps>(
export function ImageGallery({ images, event }: { images: string[]; event?: NostrEvent }) {
const photos = useMemo(() => {
return images.map((img) => {
const photo: PhotoWithoutSize = { src: img };
const photo: PhotoWithoutSize = { src: img, key: img };
return photo;
});
}, [images]);

View File

@ -17,8 +17,8 @@ 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, relay }) => (
<Alert key={relay.url} status={result[2] ? "success" : "warning"}>
{results.map(({ success, message, relay }) => (
<Alert key={relay.url} status={success ? "success" : "warning"}>
<AlertIcon />
<Box>
<AlertTitle>
@ -27,7 +27,7 @@ export function PublishDetails({ pub }: PostResultsProps & Omit<FlexProps, "chil
</Link>
<RelayPaidTag url={relay.url} />
</AlertTitle>
{result[3] && <AlertDescription>{result[3]}</AlertDescription>}
{message && <AlertDescription>{message}</AlertDescription>}
</Box>
</Alert>
))}

View File

@ -1,3 +1,4 @@
import { useContext } from "react";
import {
Flex,
FlexProps,
@ -19,14 +20,13 @@ import NostrPublishAction from "../classes/nostr-publish-action";
import useSubject from "../hooks/use-subject";
import { CheckIcon, ErrorIcon } from "./icons";
import { PublishDetails } from "./publish-details";
import { useContext } from "react";
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[2]);
const failedWithMessage = results.filter(({ result }) => !result[2] && result[3]);
const successful = results.filter(({ success }) => success);
const failedWithMessage = results.filter(({ success, message }) => !success && !!message);
let statusIcon = <Spinner size="xs" />;
let statusColor: TagProps["colorScheme"] = "blue";

View File

@ -9,7 +9,6 @@ import {
DrawerOverlay,
DrawerProps,
Flex,
Heading,
IconButton,
Link,
Select,
@ -36,21 +35,9 @@ import { SaveRelaySetForm } from "./save-relay-set-form";
function RelayControl({ url }: { url: string }) {
const relay = useMemo(() => relayPoolService.requestRelay(url, false), [url]);
const status = useSubject(relay.status);
const writeRelays = useSubject(clientRelaysService.writeRelays);
let color = "gray";
switch (status) {
case WebSocket.OPEN:
color = "green";
break;
case WebSocket.CONNECTING:
color = "yellow";
break;
case WebSocket.CLOSED:
color = "red";
break;
}
const color = relay.connected ? "green" : "red";
const onChange = () => {
if (writeRelays.has(url)) clientRelaysService.removeRelay(url, RelayMode.WRITE);
@ -117,7 +104,7 @@ export default function RelayManagementDrawer({ isOpen, onClose, ...props }: Omi
const sorted = useMemo(() => RelaySet.from(readRelays, writeRelays).urls.sort(), [readRelays, writeRelays]);
const others = Array.from(relayPoolService.relays.values())
.filter((r) => !r.closed && !sorted.includes(r.url))
.filter((r) => !r.connected && !sorted.includes(r.url))
.map((r) => r.url)
.sort();

View File

@ -1,22 +1,24 @@
import { Badge, useForceUpdate } from "@chakra-ui/react";
import { useInterval } from "react-use";
import Relay from "../classes/relay";
import relayPoolService from "../services/relay-pool";
import { Relay } from "nostr-tools";
const getStatusText = (relay: Relay) => {
if (relay.connecting) return "Connecting...";
// if (relay.connecting) return "Connecting...";
if (relay.connected) return "Connected";
if (relay.closing) return "Disconnecting...";
if (relay.closed) return "Disconnected";
return "Unused";
// if (relay.closing) return "Disconnecting...";
// if (relay.closed) return "Disconnected";
return "Disconnected";
// return "Unused";
};
const getStatusColor = (relay: Relay) => {
if (relay.connecting) return "yellow";
// if (relay.connecting) return "yellow";
if (relay.connected) return "green";
if (relay.closing) return "yellow";
if (relay.closed) return "red";
return "gray";
// if (relay.closing) return "yellow";
// if (relay.closed) return "red";
// return "gray";
return "red";
};
export const RelayStatus = ({ url }: { url: string }) => {

View File

@ -54,18 +54,38 @@ export type ParsedZap = {
const parsedZapSymbol = Symbol("parsedZap");
type ParsedZapEvent = NostrEvent & { [parsedZapSymbol]: ParsedZap | Error };
export function getParsedZap(event: NostrEvent) {
export function getParsedZap(event: NostrEvent, quite: false, returnError?: boolean): ParsedZap;
export function getParsedZap(event: NostrEvent, quite: true, returnError: true): ParsedZap | Error;
export function getParsedZap(event: NostrEvent, quite: true, returnError: false): ParsedZap | undefined;
export function getParsedZap(event: NostrEvent, quite?: boolean, returnError?: boolean): ParsedZap | undefined;
export function getParsedZap(event: NostrEvent, quite: boolean = true, returnError?: boolean) {
const e = event as ParsedZapEvent;
if (Object.hasOwn(e, parsedZapSymbol)) return e[parsedZapSymbol];
try {
return (e[parsedZapSymbol] = parseZapEvent(e));
} catch (error) {
if (error instanceof Error) return (e[parsedZapSymbol] = error);
else throw error;
if (error instanceof Error) {
e[parsedZapSymbol] = error;
if (quite) return returnError ? error : undefined;
else throw error;
} else throw error;
}
}
export function parseZapEvents(events: NostrEvent[]) {
const parsed: ParsedZap[] = [];
for (const event of events) {
const p = getParsedZap(event);
if (p) parsed.push(p);
}
return parsed;
}
/** @deprecated use getParsedZap instead */
export function parseZapEvent(event: NostrEvent): ParsedZap {
const zapRequestStr = event.tags.find(([t, v]) => t === "description")?.[1];
if (!zapRequestStr) throw new Error("no description tag");

View File

@ -1,8 +1,9 @@
import { SimpleRelay, SubscriptionOptions } from "nostr-idb";
import { Filter } from "nostr-tools";
import { AbstractRelay, Filter, SubCloser, SubscribeManyParams, Subscription } from "nostr-tools";
import { NostrQuery, NostrRequestFilter } from "../types/nostr-relay";
import { NostrEvent } from "../types/nostr-event";
import relayPoolService from "../services/relay-pool";
// NOTE: only use this for equality checks and querying
export function getRelayVariations(relay: string) {
@ -110,3 +111,81 @@ export function relayRequest(relay: SimpleRelay, filters: Filter[], opts: Subscr
});
});
}
// copied from nostr-tools, SimplePool#subscribeMany
export function subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser {
const _knownIds = new Set<string>();
const subs: Subscription[] = [];
// batch all EOSEs into a single
const eosesReceived: boolean[] = [];
let handleEose = (i: number) => {
eosesReceived[i] = true;
if (eosesReceived.filter((a) => a).length === relays.length) {
params.oneose?.();
handleEose = () => {};
}
};
// batch all closes into a single
const closesReceived: string[] = [];
let handleClose = (i: number, reason: string) => {
handleEose(i);
closesReceived[i] = reason;
if (closesReceived.filter((a) => a).length === relays.length) {
params.onclose?.(closesReceived);
handleClose = () => {};
}
};
const localAlreadyHaveEventHandler = (id: string) => {
if (params.alreadyHaveEvent?.(id)) {
return true;
}
const have = _knownIds.has(id);
_knownIds.add(id);
return have;
};
// open a subscription in all given relays
const allOpened = Promise.all(
relays.map(validateRelayURL).map(async (url, i, arr) => {
if (arr.indexOf(url) !== i) {
// duplicate
handleClose(i, "duplicate url");
return;
}
let relay: AbstractRelay;
try {
relay = relayPoolService.requestRelay(url);
await relay.connect();
// changed from nostr-tools
// relay = await this.ensureRelay(url, {
// connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1000) : undefined,
// });
} catch (err) {
handleClose(i, (err as any)?.message || String(err));
return;
}
let subscription = relay.subscribe(filters, {
...params,
oneose: () => handleEose(i),
onclose: (reason) => handleClose(i, reason),
alreadyHaveEvent: localAlreadyHaveEventHandler,
eoseTimeout: params.maxWait,
});
subs.push(subscription);
}),
);
return {
async close() {
await allOpened;
subs.forEach((sub) => {
sub.close();
});
},
};
}

View File

@ -3,7 +3,7 @@ import { useMemo } from "react";
import eventZapsService from "../services/event-zaps";
import { useReadRelays } from "./use-client-relays";
import useSubject from "./use-subject";
import { parseZapEvent } from "../helpers/nostr/zaps";
import { getParsedZap } from "../helpers/nostr/zaps";
export default function useEventZaps(eventUID: string, additionalRelays?: Iterable<string>, alwaysRequest = true) {
const readRelays = useReadRelays(additionalRelays);
@ -18,9 +18,8 @@ export default function useEventZaps(eventUID: string, additionalRelays?: Iterab
const zaps = useMemo(() => {
const parsed = [];
for (const zap of events) {
try {
parsed.push(parseZapEvent(zap));
} catch (e) {}
const p = getParsedZap(zap);
if (p) parsed.push(p);
}
return parsed;
}, [events]);

View File

@ -4,8 +4,9 @@ import { GOAL_KIND } from "../helpers/nostr/goal";
import { ParsedStream, getATag } from "../helpers/nostr/stream";
import { NostrEvent } from "../types/nostr-event";
import { useReadRelays } from "./use-client-relays";
import NostrRequest from "../classes/nostr-request";
import useSingleEvent from "./use-single-event";
import { subscribeMany } from "../helpers/relay";
import { Filter } from "nostr-tools";
export default function useStreamGoal(stream: ParsedStream) {
const [goal, setGoal] = useState<NostrEvent>();
@ -15,11 +16,11 @@ export default function useStreamGoal(stream: ParsedStream) {
useEffect(() => {
if (!stream.goal) {
const request = new NostrRequest(readRelays);
request.onEvent.subscribe((event) => {
setGoal(event);
const filter: Filter = { "#a": [getATag(stream)], kinds: [GOAL_KIND] };
const sub = subscribeMany(Array.from(readRelays), [filter], {
onevent: (event) => setGoal((c) => (!c || event.created_at > c.created_at ? event : c)),
oneose: () => sub.close(),
});
request.start({ "#a": [getATag(stream)], kinds: [GOAL_KIND] });
}
}, [stream.identifier, stream.goal, readRelays.urls.join("|")]);

View File

@ -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(({ relay, result }) => {
if (result[2]) handleEventFromRelay(relay, signed);
pub.onResult.subscribe(({ relay, success }) => {
if (success) handleEventFromRelay(relay, signed);
});
// send it to the local relay

View File

@ -40,7 +40,7 @@ class DnsIdentityService {
async fetchIdentity(address: string) {
const { name, domain } = parseAddress(address);
if (!name || !domain) throw new Error("invalid address");
if (!name || !domain) throw new Error("invalid address " + address);
const json = await fetchWithCorsFallback(`https://${domain}/.well-known/nostr.json?name=${name}`)
.then((res) => res.json() as Promise<IdentityJson>)

View File

@ -3,13 +3,13 @@ import stringify from "json-stringify-deterministic";
import Subject from "../classes/subject";
import { NostrRequestFilter } from "../types/nostr-relay";
import SuperMap from "../classes/super-map";
import NostrRequest from "../classes/nostr-request";
import relayScoreboardService from "./relay-scoreboard";
import { logger } from "../helpers/debug";
import { matchFilter, matchFilters } from "nostr-tools";
import { NostrEvent } from "../types/nostr-event";
import { relayRequest } from "../helpers/relay";
import { localRelay } from "./local-relay";
import relayPoolService from "./relay-pool";
function hashFilter(filter: NostrRequestFilter) {
return stringify(filter);
@ -62,20 +62,23 @@ class EventExistsService {
relays.delete(nextRelay);
(async () => {
const sub = this.answers.get(key);
const request = new NostrRequest([nextRelay], 500);
const limitFilter = Array.isArray(filter) ? filter.map((f) => ({ ...f, limit: 1 })) : { ...filter, limit: 1 };
request.start(limitFilter);
request.onEvent.subscribe(() => {
this.log("Found event for", filter);
sub.next(true);
this.pending.delete(key);
const subject = this.answers.get(key);
const limitFilter = Array.isArray(filter) ? filter.map((f) => ({ ...f, limit: 1 })) : [{ ...filter, limit: 1 }];
const subscription = relayPoolService.requestRelay(nextRelay).subscribe(limitFilter, {
eoseTimeout: 500,
onevent: () => {
this.log("Found event for", filter);
subject.next(true);
this.pending.delete(key);
},
oneose: () => {
if (subject.value === undefined && this.asked.get(key).size > this.pending.get(key).size) {
this.log("Could not find event for", filter);
subject.next(false);
}
subscription.close();
},
});
await request.onComplete;
if (sub.value === undefined && this.asked.get(key).size > this.pending.get(key).size) {
this.log("Could not find event for", filter);
sub.next(false);
}
})();
}
}

View File

@ -1,12 +1,12 @@
import { Filter, kinds, nip25 } from "nostr-tools";
import _throttle from "lodash.throttle";
import NostrRequest from "../classes/nostr-request";
import Subject from "../classes/subject";
import SuperMap from "../classes/super-map";
import { NostrEvent } from "../types/nostr-event";
import { localRelay } from "./local-relay";
import { relayRequest } from "../helpers/relay";
import relayPoolService from "./relay-pool";
type eventId = string;
type relay = string;
@ -73,9 +73,9 @@ class EventReactionsService {
if (coordinates.length > 0) filters.push({ "#a": coordinates, kinds: [kinds.Reaction] });
if (filters.length > 0) {
const request = new NostrRequest([relay]);
request.onEvent.subscribe((e) => this.handleEvent(e));
request.start(filters);
const subscription = relayPoolService
.requestRelay(relay)
.subscribe(filters, { onevent: (event) => this.handleEvent(event), oneose: () => subscription.close() });
}
}
this.pending.clear();

View File

@ -1,8 +1,7 @@
import Relay from "../classes/relay";
import { Relay } from "nostr-tools";
import { PersistentSubject } from "../classes/subject";
import { getEventUID } from "../helpers/nostr/event";
import { NostrEvent } from "../types/nostr-event";
import relayPoolService from "./relay-pool";
const eventRelays = new Map<string, PersistentSubject<string[]>>();
@ -30,11 +29,12 @@ export function handleEventFromRelay(relay: Relay, event: NostrEvent) {
if (event.id !== uid) addRelay(event.id, relay.url);
}
relayPoolService.onRelayCreated.subscribe((relay) => {
relay.onEvent.subscribe((message) => {
handleEventFromRelay(relay, message[2]);
});
});
// TODO: track events from relays
// relayPoolService.onRelayCreated.subscribe((relay) => {
// relay.onEvent.subscribe((message) => {
// handleEventFromRelay(relay, message[2]);
// });
// });
const eventRelaysService = {
getEventRelays,

View File

@ -1,12 +1,12 @@
import { Filter, kinds } from "nostr-tools";
import _throttle from "lodash.throttle";
import NostrRequest from "../classes/nostr-request";
import Subject from "../classes/subject";
import SuperMap from "../classes/super-map";
import { NostrEvent, isATag, isETag } from "../types/nostr-event";
import { relayRequest } from "../helpers/relay";
import { localRelay } from "./local-relay";
import relayPoolService from "./relay-pool";
type eventUID = string;
type relay = string;
@ -73,9 +73,9 @@ class EventZapsService {
if (coordinates.length > 0) filter.push({ "#a": coordinates, kinds: [kinds.Zap] });
if (filter.length > 0) {
const request = new NostrRequest([relay]);
request.onEvent.subscribe((e) => this.handleEvent(e));
request.start(filter);
const sub = relayPoolService
.requestRelay(relay)
.subscribe(filter, { onevent: (event) => this.handleEvent(event), oneose: () => sub.close() });
}
}
this.pending.clear();

View File

@ -57,7 +57,8 @@ export type NostrConnectErrorResponse = {
};
// FIXME list all requested perms
const Perms = "nip04_encrypt,nip04_decrypt,sign_event:0,sign_event:1,sign_event:3,sign_event:4,sign_event:6,sign_event:7"
const Perms =
"nip04_encrypt,nip04_decrypt,sign_event:0,sign_event:1,sign_event:3,sign_event:4,sign_event:6,sign_event:7";
export class NostrConnectClient {
sub: NostrMultiSubscription;
@ -93,7 +94,7 @@ export class NostrConnectClient {
async open() {
this.sub.open();
await this.sub.waitForConnection();
await this.sub.waitForAllConnection();
this.log("Connected to relays", this.relays);
}
close() {
@ -126,7 +127,7 @@ export class NostrConnectClient {
this.log("Got Error", response.id, response.result, response.error);
if (response.result === "auth_url") {
if (!this.auths.has(response.id)) {
this.auths.add(response.id)
this.auths.add(response.id);
try {
await this.handleAuthURL(response.error);
} catch (e) {
@ -163,7 +164,7 @@ export class NostrConnectClient {
const encrypted = await nip04.encrypt(this.secretKey, this.pubkey, JSON.stringify(request));
const event = this.createEvent(encrypted, this.pubkey, kind);
this.log(`Sending request ${id} (${method}) ${JSON.stringify(params)}`, event);
this.sub.sendAll(event);
this.sub.publish(event);
const p = createDefer<ResponseResults[T]>();
this.requests.set(id, p);
@ -180,7 +181,7 @@ export class NostrConnectClient {
const encrypted = await nip04.encrypt(this.secretKey, this.provider, JSON.stringify(request));
const event = this.createEvent(encrypted, this.provider, kind);
this.log(`Sending admin request ${id} (${method}) ${JSON.stringify(params)}`, event);
this.sub.sendAll(event);
this.sub.publish(event);
const p = createDefer<ResponseResults[T]>();
this.requests.set(id, p);
@ -190,10 +191,7 @@ export class NostrConnectClient {
async connect(token?: string) {
await this.open();
try {
const result = await this.makeRequest(
NostrConnectMethod.Connect,
[this.pubkey, token || '', Perms],
);
const result = await this.makeRequest(NostrConnectMethod.Connect, [this.pubkey, token || "", Perms]);
this.isConnected = true;
return result;
} catch (e) {
@ -207,10 +205,12 @@ export class NostrConnectClient {
await this.open();
try {
const newPubkey = await this.makeAdminRequest(
NostrConnectMethod.CreateAccount,
[name, domain, email || '', Perms],
);
const newPubkey = await this.makeAdminRequest(NostrConnectMethod.CreateAccount, [
name,
domain,
email || "",
Perms,
]);
this.pubkey = newPubkey;
this.isConnected = true;
return newPubkey;

View File

@ -1,12 +1,13 @@
import _throttle from "lodash/throttle";
import NostrRequest from "../classes/nostr-request";
import Subject from "../classes/subject";
import SuperMap from "../classes/super-map";
import { NostrEvent } from "../types/nostr-event";
import relayInfoService from "./relay-info";
import { localRelay } from "./local-relay";
import { MONITOR_STATS_KIND, SELF_REPORTED_KIND, getRelayURL } from "../helpers/nostr/relay-stats";
import relayPoolService from "./relay-pool";
import { Filter } from "nostr-tools";
const MONITOR_PUBKEY = "151c17c9d234320cf0f189af7b761f63419fd6c38c6041587a008b7682e4640f";
const MONITOR_RELAY = "wss://history.nostr.watch";
@ -50,9 +51,10 @@ class RelayStatsService {
relayInfoService.getInfo(relay).then((info) => {
if (!info.pubkey) return sub.next(null);
const request = new NostrRequest([relay, MONITOR_RELAY]);
request.onEvent.subscribe((e) => this.handleEvent(e));
request.start({ kinds: [SELF_REPORTED_KIND], authors: [info.pubkey] });
const filter: Filter = { kinds: [SELF_REPORTED_KIND], authors: [info.pubkey] };
const subscription = relayPoolService
.requestRelay(MONITOR_RELAY)
.subscribe([filter], { onevent: (event) => this.handleEvent(event), oneose: () => subscription.close() });
});
}
@ -74,9 +76,10 @@ class RelayStatsService {
private batchRequestMonitorStats() {
const relays = Array.from(this.pendingMonitorStats);
const request = new NostrRequest([MONITOR_RELAY]);
request.onEvent.subscribe((e) => this.handleEvent(e));
request.start({ since: 1704196800, kinds: [MONITOR_STATS_KIND], "#d": relays, authors: [MONITOR_PUBKEY] });
const filter: Filter = { since: 1704196800, kinds: [MONITOR_STATS_KIND], "#d": relays, authors: [MONITOR_PUBKEY] };
const sub = relayPoolService
.requestRelay(MONITOR_RELAY)
.subscribe([filter], { onevent: (event) => this.handleEvent(event), oneose: () => sub.close() });
this.pendingMonitorStats.clear();
}

View File

@ -11,6 +11,7 @@ import { relayRequest } from "../helpers/relay";
import EventStore from "../classes/event-store";
import Subject from "../classes/subject";
import BatchKindLoader, { createCoordinate } from "../classes/batch-kind-loader";
import relayPoolService from "./relay-pool";
export type RequestOptions = {
/** Always request the event from the relays */
@ -31,7 +32,7 @@ const WRITE_CACHE_BATCH_TIME = 250;
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));
const loader = new BatchKindLoader(relayPoolService.requestRelay(relay), this.log.extend(relay));
loader.events.onEvent.subscribe((e) => this.handleEvent(e));
return loader;
});

View File

@ -1,12 +1,12 @@
import _throttle from "lodash.throttle";
import NostrRequest from "../classes/nostr-request";
import SuperMap from "../classes/super-map";
import { NostrEvent } from "../types/nostr-event";
import { localRelay } from "./local-relay";
import { relayRequest, safeRelayUrls } from "../helpers/relay";
import { logger } from "../helpers/debug";
import Subject from "../classes/subject";
import relayPoolService from "./relay-pool";
const RELAY_REQUEST_BATCH_TIME = 500;
@ -64,9 +64,9 @@ class SingleEventService {
}
for (const [relay, ids] of Object.entries(idsFromRelays)) {
const request = new NostrRequest([relay]);
request.onEvent.subscribe((event) => this.handleEvent(event));
request.start({ ids });
const sub = relayPoolService
.requestRelay(relay)
.subscribe([{ ids }], { onevent: (event) => this.handleEvent(event), oneose: () => sub.close() });
}
this.pending.clear();
}

View File

@ -5,7 +5,7 @@ import { kinds, nip18, nip25 } from "nostr-tools";
import useCurrentAccount from "../../../hooks/use-current-account";
import { NostrEvent, isATag, isETag } from "../../../types/nostr-event";
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
import { parseZapEvent } from "../../../helpers/nostr/zaps";
import { getParsedZap } from "../../../helpers/nostr/zaps";
import { readablizeSats } from "../../../helpers/bolt11";
import { getEventUID, parseCoordinate } from "../../../helpers/nostr/event";
import { EmbedEvent, EmbedEventPointer } from "../../../components/embed-event";
@ -93,11 +93,7 @@ const ReactionNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>((
});
const ZapNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
const zap = useMemo(() => {
try {
return parseZapEvent(event);
} catch (e) {}
}, [event]);
const zap = useMemo(() => getParsedZap(event), [event]);
if (!zap || !zap.payment.amount) return null;

View File

@ -12,21 +12,9 @@ import UploadCloud01 from "../../../components/icons/upload-cloud-01";
export default function RelayControl({ url }: { url: string }) {
const relay = useMemo(() => relayPoolService.requestRelay(url, false), [url]);
const status = useSubject(relay.status);
const writeRelays = useSubject(clientRelaysService.writeRelays);
let color = "gray";
switch (status) {
case WebSocket.OPEN:
color = "green";
break;
case WebSocket.CONNECTING:
color = "yellow";
break;
case WebSocket.CLOSED:
color = "red";
break;
}
const color = relay.connected ? "green" : "red";
const onChange = () => {
if (writeRelays.has(url)) clientRelaysService.removeRelay(url, RelayMode.WRITE);

View File

@ -1,3 +1,4 @@
import { useCallback } from "react";
import { Flex, Heading, IconButton, Link, Text } from "@chakra-ui/react";
import { CloseIcon } from "@chakra-ui/icons";
import { Link as RouterLink } from "react-router-dom";
@ -8,7 +9,6 @@ import useCurrentAccount from "../../../hooks/use-current-account";
import { InboxIcon, OutboxIcon } from "../../../components/icons";
import MediaServerFavicon from "../../../components/media-server/media-server-favicon";
import { RelayMode } from "../../../classes/relay";
import { useCallback } from "react";
import { NostrEvent } from "../../../types/nostr-event";
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
import { usePublishEvent } from "../../../providers/global/publish-provider";

View File

@ -37,9 +37,9 @@ import { NostrEvent } from "../../../types/nostr-event";
import { groupByTime } from "../../../helpers/notification";
import { useCallback, useEffect, useMemo, useState } from "react";
import EventStore from "../../../classes/event-store";
import NostrRequest from "../../../classes/nostr-request";
import { sortByDate } from "../../../helpers/nostr/event";
import { NostrQuery } from "../../../types/nostr-relay";
import relayPoolService from "../../../services/relay-pool";
ChartJS.register(
ArcElement,
@ -154,18 +154,21 @@ export default function RelayDetailsTab({ relay }: { relay: string }) {
const [loading, setLoading] = useState(false);
const loadMore = useCallback(() => {
setLoading(true);
const request = new NostrRequest([relay]);
const throttle = _throttle(() => update({}), 100);
request.onEvent.subscribe((e) => {
store.addEvent(e);
throttle();
});
request.onComplete.then(() => setLoading(false));
const query: NostrQuery = { limit: 500 };
const last = store.getLastEvent();
if (last) query.until = last.created_at;
request.start(query);
const throttleUpdate = _throttle(() => update({}), 100);
const sub = relayPoolService.requestRelay(relay).subscribe([query], {
onevent: (event) => {
store.addEvent(event);
throttleUpdate();
},
oneose: () => sub.close(),
onclose: () => {
setLoading(false);
},
});
}, [relay, update, store]);
useEffect(() => loadMore(), [relay, loadMore]);

View File

@ -1,7 +1,7 @@
import { useMemo } from "react";
import { Flex, FlexProps, Text } from "@chakra-ui/react";
import { parseZapEvent } from "../../../helpers/nostr/zaps";
import { parseZapEvents } from "../../../helpers/nostr/zaps";
import UserLink from "../../../components/user/user-link";
import { LightningIcon } from "../../../components/icons";
import { readablizeSats } from "../../../helpers/bolt11";
@ -13,15 +13,7 @@ import UserAvatarLink from "../../../components/user/user-avatar-link";
export default function TopZappers({ stream, ...props }: FlexProps & { stream: ParsedStream }) {
const timeline = useStreamChatTimeline(stream);
const events = useSubject(timeline.timeline);
const zaps = useMemo(() => {
const parsed = [];
for (const event of events) {
try {
parsed.push(parseZapEvent(event));
} catch (e) {}
}
return parsed;
}, [events]);
const zaps = useMemo(() => parseZapEvents(events), [events]);
const totals: Record<string, number> = {};
for (const zap of zaps) {

View File

@ -7,7 +7,7 @@ import UserLink from "../../../../components/user/user-link";
import { NostrEvent } from "../../../../types/nostr-event";
import { useRegisterIntersectionEntity } from "../../../../providers/local/intersection-observer";
import { LightningIcon } from "../../../../components/icons";
import { parseZapEvent } from "../../../../helpers/nostr/zaps";
import { getParsedZap } from "../../../../helpers/nostr/zaps";
import { readablizeSats } from "../../../../helpers/bolt11";
import { TrustProvider } from "../../../../providers/local/trust";
import ChatMessageContent from "./chat-message-content";
@ -17,11 +17,7 @@ function ZapMessage({ zap, stream }: { zap: NostrEvent; stream: ParsedStream })
const ref = useRef<HTMLDivElement | null>(null);
useRegisterIntersectionEntity(ref, zap.id);
const parsed = useMemo(() => {
try {
return parseZapEvent(zap);
} catch (e) {}
}, [zap]);
const parsed = useMemo(() => getParsedZap(zap), [zap]);
const clientMuteFilter = useClientSideMuteFilter();
if (!parsed || !parsed.payment.amount) return null;

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react";
import { Box, Button, Flex, Input, Text } from "@chakra-ui/react";
import AutoSizer from "react-virtualized-auto-sizer";
import ForceGraph, { LinkObject, NodeObject } from "react-force-graph-3d";
import { kinds } from "nostr-tools";
import { Filter, kinds } from "nostr-tools";
import dayjs from "dayjs";
import { useNavigate } from "react-router-dom";
import { useDebounce, useObservable } from "react-use";
@ -24,10 +24,10 @@ import { getPubkeysFromList } from "../../helpers/nostr/lists";
import useUserContactList from "../../hooks/use-user-contact-list";
import useUserMetadata from "../../hooks/use-user-metadata";
import EventStore from "../../classes/event-store";
import NostrRequest from "../../classes/nostr-request";
import { isPTag } from "../../types/nostr-event";
import { ChevronLeftIcon } from "../../components/icons";
import { useReadRelays } from "../../hooks/use-client-relays";
import { subscribeMany } from "../../helpers/relay";
type NodeType = { id: string; image?: string; name?: string };
@ -51,13 +51,15 @@ function NetworkDMGraphPage() {
if (!contacts) return;
store.clear();
const request = new NostrRequest(relays);
request.onEvent.subscribe((e) => store.addEvent(e));
request.start({
const filter: Filter = {
authors: contactsPubkeys,
kinds: [kinds.EncryptedDirectMessage],
since,
until,
};
const sub = subscribeMany(Array.from(relays), [filter], {
onevent: (event) => store.addEvent(event),
oneose: () => sub.close(),
});
},
2 * 1000,

View File

@ -3,15 +3,14 @@ import dayjs from "dayjs";
import { ReactNode, useCallback, useMemo, useRef, useState } from "react";
import { useOutletContext } from "react-router-dom";
import { ErrorBoundary, ErrorFallback } from "../../components/error-boundary";
import { ErrorBoundary } from "../../components/error-boundary";
import { LightningIcon } from "../../components/icons";
import { NoteLink } from "../../components/note/note-link";
import UserAvatarLink from "../../components/user/user-avatar-link";
import UserLink from "../../components/user/user-link";
import { readablizeSats } from "../../helpers/bolt11";
import { isProfileZap, isNoteZap, parseZapEvent, totalZaps } from "../../helpers/nostr/zaps";
import { isProfileZap, isNoteZap, totalZaps, parseZapEvents, getParsedZap } from "../../helpers/nostr/zaps";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { NostrEvent, isATag, isETag, isPTag } from "../../types/nostr-event";
import { NostrEvent, isATag, isETag } from "../../types/nostr-event";
import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
import { useReadRelays } from "../../hooks/use-client-relays";
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
@ -23,7 +22,7 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types";
import Timestamp from "../../components/timestamp";
import { EmbedEventNostrLink, EmbedEventPointer } from "../../components/embed-event";
import { EmbedEventPointer } from "../../components/embed-event";
import { parseCoordinate } from "../../helpers/nostr/event";
import VerticalPageLayout from "../../components/vertical-page-layout";
@ -31,7 +30,7 @@ const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => {
const ref = useRef<HTMLDivElement | null>(null);
useRegisterIntersectionEntity(ref, zapEvent.id);
const { request, payment } = parseZapEvent(zapEvent);
const { request, payment } = getParsedZap(zapEvent, false);
const eventId = request.tags.find(isETag)?.[1];
const coordinate = request.tags.find(isATag)?.[1];
@ -101,15 +100,7 @@ const UserZapsTab = () => {
const timeline = useTimelineLoader(`${pubkey}-zaps`, relays, { "#p": [pubkey], kinds: [9735] }, { eventFilter });
const events = useSubject(timeline.timeline);
const zaps = useMemo(() => {
const parsed = [];
for (const zap of events) {
try {
parsed.push(parseZapEvent(zap));
} catch (e) {}
}
return parsed;
}, [events]);
const zaps = useMemo(() => parseZapEvents(events), [events]);
const callback = useTimelineCurserIntersectionCallback(timeline);

View File

@ -5435,6 +5435,20 @@ nostr-tools@^2.3.2:
optionalDependencies:
nostr-wasm v0.1.0
nostr-tools@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.4.0.tgz#bc2140a95ce0be8d4900bd852f652d811562753e"
integrity sha512-xQC7XdGeh0gLyprcKhvx5lwr7OQ+ZOiQ9C6GpzlVAj+EBv+AiN8kySb57t3uJoG1HK15oT9jf++MmQLwhp1xNQ==
dependencies:
"@noble/ciphers" "^0.5.1"
"@noble/curves" "1.2.0"
"@noble/hashes" "1.3.1"
"@scure/base" "1.1.1"
"@scure/bip32" "1.3.1"
"@scure/bip39" "1.2.1"
optionalDependencies:
nostr-wasm v0.1.0
nostr-wasm@v0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94"