2023-04-06 21:37:40 +00:00
|
|
|
import { v4 as uuid } from "uuid";
|
2023-05-24 23:03:54 +00:00
|
|
|
import debug from "debug";
|
2023-03-28 14:34:01 +00:00
|
|
|
import { Connection, RawReqFilter, Nips } from "@snort/nostr";
|
2023-05-24 10:12:23 +00:00
|
|
|
import { unixNowMs, unwrap } from "SnortUtils";
|
2023-04-06 21:37:40 +00:00
|
|
|
import { NoteStore } from "./NoteCollection";
|
2023-05-24 23:03:54 +00:00
|
|
|
|
2023-04-06 21:37:40 +00:00
|
|
|
/**
|
|
|
|
* Tracing for relay query status
|
|
|
|
*/
|
|
|
|
class QueryTrace {
|
|
|
|
readonly id: string;
|
|
|
|
readonly subId: string;
|
|
|
|
readonly relay: string;
|
|
|
|
readonly connId: string;
|
|
|
|
readonly start: number;
|
|
|
|
sent?: number;
|
|
|
|
eose?: number;
|
|
|
|
close?: number;
|
|
|
|
#wasForceClosed = false;
|
|
|
|
readonly #fnClose: (id: string) => void;
|
2023-05-24 16:17:17 +00:00
|
|
|
readonly #fnProgress: () => void;
|
2023-05-25 18:52:03 +00:00
|
|
|
|
|
|
|
constructor(sub: string, relay: string, connId: string, fnClose: (id: string) => void, fnProgress: () => void) {
|
2023-04-06 21:37:40 +00:00
|
|
|
this.id = uuid();
|
|
|
|
this.subId = sub;
|
|
|
|
this.relay = relay;
|
|
|
|
this.connId = connId;
|
|
|
|
this.start = unixNowMs();
|
|
|
|
this.#fnClose = fnClose;
|
2023-05-24 16:17:17 +00:00
|
|
|
this.#fnProgress = fnProgress;
|
2023-04-06 21:37:40 +00:00
|
|
|
}
|
2023-03-28 14:34:01 +00:00
|
|
|
|
2023-04-06 21:37:40 +00:00
|
|
|
sentToRelay() {
|
|
|
|
this.sent = unixNowMs();
|
2023-05-24 16:17:17 +00:00
|
|
|
this.#fnProgress();
|
2023-04-06 21:37:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
gotEose() {
|
|
|
|
this.eose = unixNowMs();
|
2023-05-24 16:17:17 +00:00
|
|
|
this.#fnProgress();
|
2023-04-06 21:37:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
forceEose() {
|
|
|
|
this.eose = unixNowMs();
|
|
|
|
this.#wasForceClosed = true;
|
2023-05-24 16:17:17 +00:00
|
|
|
this.#fnProgress();
|
2023-05-25 10:05:06 +00:00
|
|
|
this.sendClose();
|
2023-04-06 21:37:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
sendClose() {
|
|
|
|
this.close = unixNowMs();
|
|
|
|
this.#fnClose(this.subId);
|
2023-05-24 16:17:17 +00:00
|
|
|
this.#fnProgress();
|
2023-04-06 21:37:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Time spent in queue
|
|
|
|
*/
|
|
|
|
get queued() {
|
2023-05-24 16:17:17 +00:00
|
|
|
return (this.sent === undefined ? unixNowMs() : this.#wasForceClosed ? unwrap(this.eose) : this.sent) - this.start;
|
2023-04-06 21:37:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Total query runtime
|
|
|
|
*/
|
|
|
|
get runtime() {
|
|
|
|
return (this.eose === undefined ? unixNowMs() : this.eose) - this.start;
|
|
|
|
}
|
|
|
|
|
2023-05-18 15:51:21 +00:00
|
|
|
/**
|
|
|
|
* Total time spent waiting for relay to respond
|
|
|
|
*/
|
|
|
|
get responseTime() {
|
|
|
|
return this.finished ? unwrap(this.eose) - unwrap(this.sent) : 0;
|
|
|
|
}
|
|
|
|
|
2023-04-06 21:37:40 +00:00
|
|
|
/**
|
|
|
|
* If tracing is finished, we got EOSE or timeout
|
|
|
|
*/
|
|
|
|
get finished() {
|
|
|
|
return this.eose !== undefined;
|
|
|
|
}
|
2023-03-28 14:34:01 +00:00
|
|
|
}
|
|
|
|
|
2023-05-24 16:17:17 +00:00
|
|
|
export interface QueryBase {
|
2023-03-28 14:34:01 +00:00
|
|
|
/**
|
|
|
|
* Uniquie ID of this query
|
|
|
|
*/
|
|
|
|
id: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The query payload (REQ filters)
|
|
|
|
*/
|
2023-04-06 21:37:40 +00:00
|
|
|
filters: Array<RawReqFilter>;
|
2023-03-28 14:34:01 +00:00
|
|
|
|
|
|
|
/**
|
2023-05-24 16:17:17 +00:00
|
|
|
* List of relays to send this query to
|
2023-03-28 14:34:01 +00:00
|
|
|
*/
|
2023-05-24 16:17:17 +00:00
|
|
|
relays?: Array<string>;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Active or queued query on the system
|
|
|
|
*/
|
2023-05-25 18:52:03 +00:00
|
|
|
export class Query {
|
|
|
|
/**
|
|
|
|
* Uniquie ID of this query
|
|
|
|
*/
|
2023-05-24 16:17:17 +00:00
|
|
|
id: string;
|
2023-05-25 18:52:03 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A merged set of all filters send to relays for this query
|
|
|
|
*/
|
|
|
|
filters: Array<RawReqFilter> = [];
|
2023-03-28 14:34:01 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Which relays this query has already been executed on
|
|
|
|
*/
|
2023-04-06 21:37:40 +00:00
|
|
|
#tracing: Array<QueryTrace> = [];
|
2023-03-28 15:41:57 +00:00
|
|
|
|
2023-03-28 14:34:01 +00:00
|
|
|
/**
|
|
|
|
* Leave the query open until its removed
|
|
|
|
*/
|
|
|
|
leaveOpen = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Time when this query can be removed
|
|
|
|
*/
|
|
|
|
#cancelTimeout?: number;
|
|
|
|
|
2023-04-06 21:37:40 +00:00
|
|
|
/**
|
|
|
|
* Timer used to track tracing status
|
|
|
|
*/
|
|
|
|
#checkTrace?: ReturnType<typeof setInterval>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Feed object which collects events
|
|
|
|
*/
|
2023-04-25 17:01:29 +00:00
|
|
|
#feed: NoteStore;
|
2023-04-06 21:37:40 +00:00
|
|
|
|
2023-05-24 16:17:17 +00:00
|
|
|
subQueryCounter = 0;
|
2023-05-24 23:03:54 +00:00
|
|
|
#log = debug("Query");
|
2023-05-24 16:17:17 +00:00
|
|
|
|
2023-05-25 18:52:03 +00:00
|
|
|
constructor(id: string, feed: NoteStore) {
|
2023-03-28 14:34:01 +00:00
|
|
|
this.id = id;
|
2023-04-06 21:37:40 +00:00
|
|
|
this.#feed = feed;
|
|
|
|
this.#checkTraces();
|
2023-03-28 14:34:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
get closing() {
|
|
|
|
return this.#cancelTimeout !== undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
get closingAt() {
|
|
|
|
return this.#cancelTimeout;
|
|
|
|
}
|
|
|
|
|
2023-04-06 21:37:40 +00:00
|
|
|
get feed() {
|
|
|
|
return this.#feed;
|
|
|
|
}
|
|
|
|
|
2023-03-28 14:34:01 +00:00
|
|
|
cancel() {
|
|
|
|
this.#cancelTimeout = unixNowMs() + 5_000;
|
|
|
|
}
|
|
|
|
|
|
|
|
unCancel() {
|
|
|
|
this.#cancelTimeout = undefined;
|
|
|
|
}
|
|
|
|
|
2023-04-06 21:37:40 +00:00
|
|
|
cleanup() {
|
|
|
|
this.#stopCheckTraces();
|
|
|
|
}
|
|
|
|
|
2023-05-25 18:52:03 +00:00
|
|
|
sendToRelay(c: Connection, subq: QueryBase) {
|
2023-05-24 16:17:17 +00:00
|
|
|
if (!this.#canSendQuery(c, subq)) {
|
2023-03-28 14:34:01 +00:00
|
|
|
return;
|
|
|
|
}
|
2023-05-24 16:17:17 +00:00
|
|
|
this.#sendQueryInternal(c, subq);
|
2023-04-06 21:37:40 +00:00
|
|
|
}
|
|
|
|
|
2023-05-24 23:03:54 +00:00
|
|
|
connectionLost(id: string) {
|
|
|
|
this.#tracing.filter(a => a.connId == id).forEach(a => a.forceEose());
|
2023-03-28 14:34:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
sendClose() {
|
2023-05-24 16:17:17 +00:00
|
|
|
for (const qt of this.#tracing) {
|
|
|
|
qt.sendClose();
|
2023-03-29 14:17:12 +00:00
|
|
|
}
|
2023-04-06 21:37:40 +00:00
|
|
|
this.cleanup();
|
2023-03-28 14:34:01 +00:00
|
|
|
}
|
2023-03-28 15:41:57 +00:00
|
|
|
|
2023-04-06 21:37:40 +00:00
|
|
|
eose(sub: string, conn: Readonly<Connection>) {
|
2023-04-25 17:01:29 +00:00
|
|
|
const qt = this.#tracing.find(a => a.subId === sub && a.connId === conn.Id);
|
|
|
|
qt?.gotEose();
|
2023-05-25 18:52:03 +00:00
|
|
|
if (!this.leaveOpen) {
|
|
|
|
qt?.sendClose();
|
|
|
|
}
|
2023-03-28 15:41:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the progress to EOSE, can be used to determine when we should load more content
|
|
|
|
*/
|
|
|
|
get progress() {
|
2023-05-24 16:17:17 +00:00
|
|
|
const thisProgress = this.#tracing.reduce((acc, v) => (acc += v.finished ? 1 : 0), 0) / this.#tracing.length;
|
2023-03-29 10:40:05 +00:00
|
|
|
if (isNaN(thisProgress)) {
|
2023-05-24 16:17:17 +00:00
|
|
|
return 0;
|
2023-03-28 15:41:57 +00:00
|
|
|
}
|
2023-05-24 16:17:17 +00:00
|
|
|
return thisProgress;
|
|
|
|
}
|
2023-03-28 15:41:57 +00:00
|
|
|
|
2023-05-24 16:17:17 +00:00
|
|
|
#onProgress() {
|
|
|
|
const isFinished = this.progress === 1;
|
|
|
|
if (this.feed.loading !== isFinished) {
|
2023-05-24 23:03:54 +00:00
|
|
|
this.#log("%s loading=%s, progress=%d", this.id, this.feed.loading, this.progress);
|
2023-05-24 16:17:17 +00:00
|
|
|
this.feed.loading = isFinished;
|
2023-03-28 15:41:57 +00:00
|
|
|
}
|
|
|
|
}
|
2023-04-06 21:37:40 +00:00
|
|
|
|
|
|
|
#stopCheckTraces() {
|
|
|
|
if (this.#checkTrace) {
|
|
|
|
clearInterval(this.#checkTrace);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#checkTraces() {
|
|
|
|
this.#stopCheckTraces();
|
|
|
|
this.#checkTrace = setInterval(() => {
|
|
|
|
for (const v of this.#tracing) {
|
|
|
|
if (v.runtime > 5_000 && !v.finished) {
|
|
|
|
v.forceEose();
|
|
|
|
}
|
|
|
|
}
|
2023-05-24 16:17:17 +00:00
|
|
|
}, 500);
|
|
|
|
}
|
|
|
|
|
|
|
|
#canSendQuery(c: Connection, q: QueryBase) {
|
|
|
|
if (q.relays && !q.relays.includes(c.Address)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if ((q.relays?.length ?? 0) === 0 && c.Ephemeral) {
|
2023-05-25 10:51:59 +00:00
|
|
|
this.#log("Cant send non-specific REQ to ephemeral connection %O %O %O", q, q.relays, c);
|
2023-05-24 16:17:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (q.filters.some(a => a.search) && !c.SupportsNip(Nips.Search)) {
|
2023-05-24 23:03:54 +00:00
|
|
|
this.#log("Cant send REQ to non-search relay", c.Address);
|
2023-05-24 16:17:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
#sendQueryInternal(c: Connection, q: QueryBase) {
|
|
|
|
const qt = new QueryTrace(
|
|
|
|
q.id,
|
|
|
|
c.Address,
|
|
|
|
c.Id,
|
|
|
|
x => c.CloseReq(x),
|
|
|
|
() => this.#onProgress()
|
|
|
|
);
|
|
|
|
this.#tracing.push(qt);
|
|
|
|
c.QueueReq(["REQ", q.id, ...q.filters], () => qt.sentToRelay());
|
2023-04-06 21:37:40 +00:00
|
|
|
}
|
2023-03-28 14:34:01 +00:00
|
|
|
}
|