From a21448c0dffd3ac553ce54c0f54441fa9542c384 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Mon, 23 Mar 2026 10:03:52 -0700 Subject: [PATCH] Make sure deletes are by the same author --- packages/lib/src/Tools.ts | 26 +++++++++---- packages/net/__tests__/repository.test.ts | 32 ++++++++++++--- packages/net/src/repository.ts | 47 ++++++++++++++++++----- 3 files changed, 82 insertions(+), 23 deletions(-) diff --git a/packages/lib/src/Tools.ts b/packages/lib/src/Tools.ts index fb0977d..949f7d8 100644 --- a/packages/lib/src/Tools.ts +++ b/packages/lib/src/Tools.ts @@ -1150,9 +1150,9 @@ export const randomId = (): string => Math.random().toString().slice(2) */ export const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t)) -export type PollOptions = { +export type PollOptions = { signal: AbortSignal - condition: () => boolean + condition: () => T interval?: number } @@ -1161,17 +1161,21 @@ export type PollOptions = { * @param options - PollOptions * @returns void Promise */ -export const poll = ({interval = 300, condition, signal}: PollOptions) => - new Promise(resolve => { +export const poll = ({interval = 300, condition, signal}: PollOptions) => + new Promise(resolve => { const int = setInterval(() => { - if (condition()) { - resolve() + const value = condition() + + if (value !== undefined) { + resolve(value) clearInterval(int) } }, interval) - if (condition()) { - resolve() + const value = condition() + + if (value !== undefined) { + resolve(value) clearInterval(int) } @@ -1610,6 +1614,12 @@ export const prop = (x: Record) => x[k] as T +/** Returns a function that checks whether the object's property value is in a list */ +export const propIn = + (k: string, xs: T[]) => + (x: Record) => + xs.includes(x[k] as T) + /** Returns a function that adds/updates a property on object */ export const assoc = (k: K, v: T) => diff --git a/packages/net/__tests__/repository.test.ts b/packages/net/__tests__/repository.test.ts index 364253e..4a0b67f 100644 --- a/packages/net/__tests__/repository.test.ts +++ b/packages/net/__tests__/repository.test.ts @@ -102,8 +102,13 @@ describe("Repository", () => { }) it("should handle delete events", () => { - const event = createEvent(1) - const deleteEvent = createEvent(DELETE, {tags: [["e", event.id]], created_at: now() + 100}) + const pubkey = randomHex() + const event = createEvent(1, {pubkey}) + const deleteEvent = createEvent(DELETE, { + pubkey, + tags: [["e", event.id]], + created_at: now() + 100, + }) repo.publish(event) repo.publish(deleteEvent) @@ -112,8 +117,10 @@ describe("Repository", () => { }) it("should handle delete by address", () => { - const event = createEvent(MUTES) + const pubkey = randomHex() + const event = createEvent(MUTES, {pubkey}) const deleteEvent = createEvent(DELETE, { + pubkey, tags: [["a", `10000:${event.pubkey}:`]], created_at: now() + 100, }) @@ -123,6 +130,16 @@ describe("Repository", () => { expect(repo.isDeletedByAddress(event)).toBe(true) }) + + it("should not delete events with mismatched pubkeys", () => { + const event = createEvent(1) + const deleteEvent = createEvent(DELETE, {tags: [["e", event.id]], created_at: now() + 1}) + + repo.publish(event) + repo.publish(deleteEvent) + + expect(repo.isDeleted(event)).toBe(false) + }) }) describe("expire events", () => { @@ -224,8 +241,13 @@ describe("Repository", () => { }) it("should not return deleted events", () => { - const event = createEvent(1) - const deleteEvent = createEvent(DELETE, {tags: [["e", event.id]], created_at: now() + 1}) + const pubkey = randomHex() + const event = createEvent(1, {pubkey}) + const deleteEvent = createEvent(DELETE, { + pubkey, + tags: [["e", event.id]], + created_at: now() + 1, + }) repo.publish(event) repo.publish(deleteEvent) diff --git a/packages/net/src/repository.ts b/packages/net/src/repository.ts index 8888b48..8ba613d 100644 --- a/packages/net/src/repository.ts +++ b/packages/net/src/repository.ts @@ -1,4 +1,17 @@ -import {DAY, Emitter, flatten, pluck, sortBy, inc, uniq, omit, now, range} from "@welshman/lib" +import { + DAY, + Emitter, + flatten, + pick, + pushToMapKey, + pluck, + sortBy, + inc, + uniq, + omit, + now, + range, +} from "@welshman/lib" import { DELETE, EPOCH, @@ -45,7 +58,7 @@ export class Repository extends Emitter { eventsByDay = new Map() eventsByAuthor = new Map() eventsByKind = new Map() - deletes = new Map() + deletes = new Map() expired = new Map() static get() { @@ -101,8 +114,12 @@ export class Repository extends Emitter { } // Anything removed via delete or replace has been removed - for (const id of this.deletes.keys()) { - removed.add(id) + for (const idOrAddress of this.deletes.keys()) { + const event = this.getEvent(idOrAddress) + + if (event && this.isDeleted(event)) { + removed.add(event.id) + } } // Anything expired has been removed @@ -211,7 +228,7 @@ export class Repository extends Emitter { } // If our event is newer than what it's replacing, delete the old version - this.deletes.set(duplicate.id, event.created_at) + pushToMapKey(this.deletes, duplicate.id, pick(["pubkey", "created_at"], event)) // Notify listeners that it's been removed removed.add(duplicate.id) @@ -238,7 +255,7 @@ export class Repository extends Emitter { // If this is a delete event, the tag value is an id or address. Track when it was // deleted so that replaceables can be restored. if (event.kind === DELETE && ["a", "e"].includes(tag[0]) && tag[1]) { - this.deletes.set(tag[1], Math.max(event.created_at, this.deletes.get(tag[1]) || 0)) + pushToMapKey(this.deletes, tag[1], pick(["pubkey", "created_at"], event)) const deletedEvent = this.getEvent(tag[1]) @@ -266,12 +283,22 @@ export class Repository extends Emitter { return true } - isDeletedByAddress = (event: TrustedEvent) => - (this.deletes.get(getAddress(event)) || 0) > event.created_at + _isDeleted = (key: string, event: TrustedEvent) => { + for (const {pubkey, created_at} of this.deletes.get(key) || []) { + if (pubkey === event.pubkey && created_at > event.created_at) { + return true + } + } - isDeletedById = (event: TrustedEvent) => (this.deletes.get(event.id) || 0) > event.created_at + return false + } - isDeleted = (event: TrustedEvent) => this.isDeletedByAddress(event) || this.isDeletedById(event) + isDeletedByAddress = (event: TrustedEvent) => this._isDeleted(getAddress(event), event) + + isDeletedById = (event: TrustedEvent) => this._isDeleted(event.id, event) + + isDeleted = (event: TrustedEvent) => + this._isDeleted(event.id, event) || this._isDeleted(getAddress(event), event) isExpired = (event: TrustedEvent) => { const ts = this.expired.get(event.id)