handle delete events in timeline loader

properly cleanup timelines and set max 10 in cache
This commit is contained in:
hzrd149 2023-09-01 11:08:37 -05:00
parent 7c797ff05b
commit 11f98c8967
6 changed files with 91 additions and 13 deletions

View File

@ -16,8 +16,9 @@ export default class EventStore {
return Array.from(this.events.values()).sort((a, b) => b.created_at - a.created_at);
}
onEvent = new Subject<NostrEvent>();
onClear = new Subject();
onEvent = new Subject<NostrEvent>(undefined, false);
onDelete = new Subject<string>(undefined, false);
onClear = new Subject(undefined, false);
addEvent(event: NostrEvent) {
const id = getEventUID(event);
@ -30,17 +31,25 @@ export default class EventStore {
getEvent(id: string) {
return this.events.get(id);
}
deleteEvent(id: string) {
if (this.events.has(id)) {
this.events.delete(id);
this.onDelete.next(id);
}
}
clear() {
this.events.clear();
this.onClear.next(null);
this.onClear.next(undefined);
}
connect(other: EventStore) {
other.onEvent.subscribe(this.addEvent, this);
other.onDelete.subscribe(this.deleteEvent, this);
}
disconnect(other: EventStore) {
other.onEvent.unsubscribe(this.addEvent, this);
other.onDelete.unsubscribe(this.deleteEvent, this);
}
getFirstEvent(nth = 0, filter?: EventFilter) {

View File

@ -1,6 +1,6 @@
import dayjs from "dayjs";
import { Debugger } from "debug";
import { NostrEvent } from "../types/nostr-event";
import { NostrEvent, isATag, isETag } from "../types/nostr-event";
import { NostrQuery, NostrRequestFilter } from "../types/nostr-query";
import { NostrRequest } from "./nostr-request";
import { NostrMultiSubscription } from "./nostr-multi-subscription";
@ -9,6 +9,7 @@ import { logger } from "../helpers/debug";
import EventStore from "./event-store";
import { isReplaceable } from "../helpers/nostr/events";
import replaceableEventLoaderService from "../services/replaceable-event-requester";
import deleteEventService from "../services/delete-events";
function addToQuery(filter: NostrRequestFilter, query: NostrQuery) {
if (Array.isArray(filter)) {
@ -40,6 +41,8 @@ export class RelayTimelineLoader {
this.log = log || logger.extend(relay);
this.events = new EventStore(relay);
deleteEventService.stream.subscribe(this.handleDeleteEvent, this);
}
loadNextBlock() {
@ -70,10 +73,22 @@ export class RelayTimelineLoader {
request.start(query);
}
private handleDeleteEvent(deleteEvent: NostrEvent) {
const cord = deleteEvent.tags.find(isATag)?.[1];
const eventId = deleteEvent.tags.find(isETag)?.[1];
if (cord) this.events.deleteEvent(cord);
if (eventId) this.events.deleteEvent(eventId);
}
private handleEvent(event: NostrEvent) {
return this.events.addEvent(event);
}
cleanup() {
deleteEventService.stream.unsubscribe(this.handleDeleteEvent, this);
}
getFirstEvent(nth = 0, filter?: EventFilter) {
return this.events.getFirstEvent(nth, filter);
}
@ -111,7 +126,10 @@ export class TimelineLoader {
// update the timeline when there are new events
this.events.onEvent.subscribe(this.updateTimeline, this);
this.events.onDelete.subscribe(this.updateTimeline, this);
this.events.onClear.subscribe(this.updateTimeline, this);
deleteEventService.stream.subscribe(this.handleDeleteEvent, this);
}
private updateTimeline() {
@ -126,6 +144,13 @@ export class TimelineLoader {
}
this.events.addEvent(event);
}
private handleDeleteEvent(deleteEvent: NostrEvent) {
const cord = deleteEvent.tags.find(isATag)?.[1];
const eventId = deleteEvent.tags.find(isETag)?.[1];
if (cord) this.events.deleteEvent(cord);
if (eventId) this.events.deleteEvent(eventId);
}
private createLoaders() {
if (!this.query) return;
@ -143,6 +168,7 @@ export class TimelineLoader {
private removeLoaders(filter?: (loader: RelayTimelineLoader) => boolean) {
for (const [relay, loader] of this.relayTimelineLoaders) {
if (!filter || filter(loader)) {
loader.cleanup();
this.events.disconnect(loader.events);
loader.onBlockFinish.unsubscribe(this.updateLoading, this);
loader.onBlockFinish.unsubscribe(this.updateComplete, this);
@ -166,18 +192,19 @@ export class TimelineLoader {
setQuery(query: NostrRequestFilter) {
if (JSON.stringify(this.query) === JSON.stringify(query)) return;
// remove all loaders
this.removeLoaders();
this.log("set query", query);
this.query = query;
this.events.clear();
this.timeline.next([]);
// forget all events
this.forgetEvents();
// create any missing loaders
this.createLoaders();
// update the complete flag
this.updateComplete();
// update the subscription
this.subscription.forgetEvents();
// update the subscription with the new query
this.subscription.setQuery(addToQuery(query, { limit: BLOCK_SIZE / 2 }));
}
setFilter(filter?: (event: NostrEvent) => boolean) {
@ -242,10 +269,17 @@ export class TimelineLoader {
reset() {
this.cursor = dayjs().unix();
this.relayTimelineLoaders.clear();
this.removeLoaders();
this.forgetEvents();
}
/** close the subscription and remove any event listeners for this timeline */
cleanup() {
this.close();
this.removeLoaders();
deleteEventService.stream.unsubscribe(this.handleDeleteEvent, this);
}
// TODO: this is only needed because the current logic dose not remove events when the relay they where fetched from is removed
/** @deprecated */
forgetEvents() {

View File

@ -35,6 +35,7 @@ import { ExternalLinkIcon } from "../components/icons";
import { getEventCoordinate, getEventUID, isReplaceable } from "../helpers/nostr/events";
import NostrPublishAction from "../classes/nostr-publish-action";
import { Tag } from "../types/nostr-event";
import deleteEventService from "../services/delete-events";
type DeleteEventContextType = {
isLoading: boolean;
@ -94,6 +95,7 @@ export default function DeleteEventProvider({ children }: PropsWithChildren) {
};
const signed = await signingService.requestSignature(draft, account);
const pub = new NostrPublishAction("Delete", writeRelays, signed);
deleteEventService.handleEvent(signed);
defer?.resolve();
} catch (e) {
if (e instanceof Error) toast({ status: "error", description: e.message });

View File

@ -0,0 +1,24 @@
import { Kind } from "nostr-tools";
import Subject from "../classes/subject";
import { getEventUID } from "../helpers/nostr/events";
import { NostrEvent } from "../types/nostr-event";
const deleteEventStream = new Subject<NostrEvent>();
function handleEvent(deleteEvent: NostrEvent) {
if (deleteEvent.kind !== Kind.EventDeletion) return;
deleteEventStream.next(deleteEvent);
}
function doseMatch(deleteEvent: NostrEvent, event: NostrEvent) {
const id = getEventUID(event);
return deleteEvent.tags.some((t) => (t[0] === "a" || t[0] === "e") && t[1] === id);
}
const deleteEventService = {
stream: deleteEventStream,
handleEvent,
doseMatch,
};
export default deleteEventService;

View File

@ -13,7 +13,7 @@ import { getEventCoordinate } from "../helpers/nostr/events";
type Pubkey = string;
type Relay = string;
export function getReadableCoordinate(kind: number, pubkey: string, d?: string) {
export function getHumanReadableCoordinate(kind: number, pubkey: string, d?: string) {
return `${kind}:${nameOrPubkey(pubkey)}${d ? ":" + d : ""}`;
}
export function createCoordinate(kind: number, pubkey: string, d?: string) {

View File

@ -1,6 +1,6 @@
import { TimelineLoader } from "../classes/timeline-loader";
const MAX_CACHE = 4;
const MAX_CACHE = 10;
class TimelineCacheService {
private timelines = new Map<string, TimelineLoader>();
@ -13,9 +13,18 @@ class TimelineCacheService {
this.timelines.set(key, timeline);
}
// add or move the timelines key to the top of the queue
this.cacheQueue = this.cacheQueue.filter((p) => p !== key).concat(key);
// remove any timelines at the end of the queue
while (this.cacheQueue.length > MAX_CACHE) {
this.cacheQueue.shift();
const deleteKey = this.cacheQueue.shift();
if (!deleteKey) break;
const deadTimeline = this.timelines.get(deleteKey);
if (deadTimeline) {
this.timelines.delete(deleteKey);
deadTimeline.cleanup();
}
}
return timeline;