Track expiration in repository
This commit is contained in:
@@ -1,30 +1,20 @@
|
|||||||
import {describe, it, vi, expect, beforeEach} from "vitest"
|
import {describe, it, vi, expect, beforeEach} from "vitest"
|
||||||
import {now} from "@welshman/lib"
|
import {now, randomId} from "@welshman/lib"
|
||||||
import {getAddress, TrustedEvent, DELETE, MUTES} from "@welshman/util"
|
import {getAddress, makeEvent, TrustedEvent, DELETE, MUTES} from "@welshman/util"
|
||||||
import {Repository} from "../src/repository"
|
import {Repository} from "../src/repository"
|
||||||
|
|
||||||
|
const createEvent = (kind: number, extra = {}) => ({
|
||||||
|
...makeEvent(kind),
|
||||||
|
pubkey: randomId(),
|
||||||
|
id: randomId(),
|
||||||
|
...extra,
|
||||||
|
})
|
||||||
|
|
||||||
describe("Repository", () => {
|
describe("Repository", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Realistic Nostr data
|
|
||||||
const pubkey = "ee".repeat(32)
|
|
||||||
const id = "ff".repeat(32)
|
|
||||||
const sig = "00".repeat(64)
|
|
||||||
const currentTime = now()
|
|
||||||
|
|
||||||
const createEvent = (overrides = {}): TrustedEvent => ({
|
|
||||||
id: id,
|
|
||||||
pubkey: pubkey,
|
|
||||||
created_at: currentTime,
|
|
||||||
kind: 1,
|
|
||||||
tags: [],
|
|
||||||
content: "Hello Nostr!",
|
|
||||||
sig: sig,
|
|
||||||
...overrides,
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("basic operations", () => {
|
describe("basic operations", () => {
|
||||||
let repo: Repository
|
let repo: Repository
|
||||||
|
|
||||||
@@ -33,7 +23,7 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should publish and retrieve events", () => {
|
it("should publish and retrieve events", () => {
|
||||||
const event = createEvent()
|
const event = createEvent(1)
|
||||||
expect(repo.publish(event)).toBe(true)
|
expect(repo.publish(event)).toBe(true)
|
||||||
expect(repo.getEvent(event.id)).toEqual(event)
|
expect(repo.getEvent(event.id)).toEqual(event)
|
||||||
})
|
})
|
||||||
@@ -45,13 +35,13 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should handle duplicate events", () => {
|
it("should handle duplicate events", () => {
|
||||||
const event = createEvent()
|
const event = createEvent(1)
|
||||||
expect(repo.publish(event)).toBe(true)
|
expect(repo.publish(event)).toBe(true)
|
||||||
expect(repo.publish(event)).toBe(false)
|
expect(repo.publish(event)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should check if events exist", () => {
|
it("should check if events exist", () => {
|
||||||
const event = createEvent()
|
const event = createEvent(1)
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
expect(repo.hasEvent(event)).toBe(true)
|
expect(repo.hasEvent(event)).toBe(true)
|
||||||
})
|
})
|
||||||
@@ -65,8 +55,9 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should handle replaceable events", () => {
|
it("should handle replaceable events", () => {
|
||||||
const event1 = createEvent({kind: MUTES, created_at: currentTime - 100})
|
const pubkey = randomId()
|
||||||
const event2 = createEvent({kind: MUTES, created_at: currentTime, id: "ee".repeat(32)})
|
const event1 = createEvent(MUTES, {created_at: now() - 100, pubkey})
|
||||||
|
const event2 = createEvent(MUTES, {created_at: now(), pubkey})
|
||||||
|
|
||||||
const address1 = getAddress(event1)
|
const address1 = getAddress(event1)
|
||||||
const address2 = getAddress(event2)
|
const address2 = getAddress(event2)
|
||||||
@@ -79,7 +70,7 @@ describe("Repository", () => {
|
|||||||
expect(repo.getEvent(event2.id)).toEqual(event2)
|
expect(repo.getEvent(event2.id)).toEqual(event2)
|
||||||
expect(repo.getEvent(address2)).toEqual(event2)
|
expect(repo.getEvent(address2)).toEqual(event2)
|
||||||
|
|
||||||
const event3 = createEvent({kind: MUTES, created_at: currentTime - 50, id: "dd".repeat(32)})
|
const event3 = createEvent(MUTES, {created_at: now() - 50, pubkey})
|
||||||
|
|
||||||
repo.publish(event3)
|
repo.publish(event3)
|
||||||
|
|
||||||
@@ -87,8 +78,8 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should not replace with older events", () => {
|
it("should not replace with older events", () => {
|
||||||
const event1 = createEvent({kind: MUTES, created_at: currentTime})
|
const event1 = createEvent(MUTES, {created_at: now()})
|
||||||
const event2 = createEvent({kind: MUTES, created_at: currentTime - 100})
|
const event2 = createEvent(MUTES, {created_at: now() - 100})
|
||||||
|
|
||||||
repo.publish(event1)
|
repo.publish(event1)
|
||||||
repo.publish(event2)
|
repo.publish(event2)
|
||||||
@@ -105,13 +96,8 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should handle delete events", () => {
|
it("should handle delete events", () => {
|
||||||
const event = createEvent()
|
const event = createEvent(1)
|
||||||
const deleteEvent = createEvent({
|
const deleteEvent = createEvent(DELETE, {tags: [["e", event.id]], created_at: now() + 100})
|
||||||
id: "ee".repeat(32),
|
|
||||||
kind: DELETE,
|
|
||||||
tags: [["e", event.id]],
|
|
||||||
created_at: currentTime + 100,
|
|
||||||
})
|
|
||||||
|
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
repo.publish(deleteEvent)
|
repo.publish(deleteEvent)
|
||||||
@@ -120,12 +106,10 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should handle delete by address", () => {
|
it("should handle delete by address", () => {
|
||||||
const event = createEvent({kind: MUTES})
|
const event = createEvent(MUTES)
|
||||||
const deleteEvent = createEvent({
|
const deleteEvent = createEvent(DELETE, {
|
||||||
id: "ee".repeat(32),
|
|
||||||
kind: DELETE,
|
|
||||||
tags: [["a", `10000:${event.pubkey}:`]],
|
tags: [["a", `10000:${event.pubkey}:`]],
|
||||||
created_at: currentTime + 100,
|
created_at: now() + 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
@@ -135,6 +119,28 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("expire events", () => {
|
||||||
|
let repo: Repository
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
repo = new Repository()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle expiring events", () => {
|
||||||
|
const event1 = createEvent(1, {tags: [["expiration", String(now() - 100)]]})
|
||||||
|
const event2 = createEvent(1, {tags: [["expiration", String(now() + 100)]]})
|
||||||
|
const event3 = createEvent(1)
|
||||||
|
|
||||||
|
repo.publish(event1)
|
||||||
|
repo.publish(event2)
|
||||||
|
repo.publish(event3)
|
||||||
|
|
||||||
|
expect(repo.isExpired(event1)).toBe(true)
|
||||||
|
expect(repo.isExpired(event2)).toBe(false)
|
||||||
|
expect(repo.isExpired(event3)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("query operations", () => {
|
describe("query operations", () => {
|
||||||
let repo: Repository
|
let repo: Repository
|
||||||
|
|
||||||
@@ -147,7 +153,7 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should query by ids", () => {
|
it("should query by ids", () => {
|
||||||
const event = createEvent()
|
const event = createEvent(1)
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
|
|
||||||
const results = repo.query([{ids: [event.id]}])
|
const results = repo.query([{ids: [event.id]}])
|
||||||
@@ -155,7 +161,7 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should query by authors", () => {
|
it("should query by authors", () => {
|
||||||
const event = createEvent()
|
const event = createEvent(1)
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
|
|
||||||
const results = repo.query([{authors: [event.pubkey]}])
|
const results = repo.query([{authors: [event.pubkey]}])
|
||||||
@@ -163,7 +169,7 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should query by kinds", () => {
|
it("should query by kinds", () => {
|
||||||
const event = createEvent({kind: 1})
|
const event = createEvent(1)
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
|
|
||||||
const results = repo.query([{kinds: [1]}])
|
const results = repo.query([{kinds: [1]}])
|
||||||
@@ -171,7 +177,9 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should query by tags", () => {
|
it("should query by tags", () => {
|
||||||
const event = createEvent({tags: [["p", pubkey]]})
|
const pubkey = randomId()
|
||||||
|
const event = createEvent(1, {tags: [["p", pubkey]]})
|
||||||
|
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
|
|
||||||
const results = repo.query([{"#p": [pubkey]}])
|
const results = repo.query([{"#p": [pubkey]}])
|
||||||
@@ -179,20 +187,20 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should query by time range", () => {
|
it("should query by time range", () => {
|
||||||
const event = createEvent()
|
const event = createEvent(1)
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
|
|
||||||
const results = repo.query([
|
const results = repo.query([
|
||||||
{
|
{
|
||||||
since: currentTime - 3600,
|
since: now() - 3600,
|
||||||
until: currentTime + 3600,
|
until: now() + 3600,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
expect(results).toContain(event)
|
expect(results).toContain(event)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle multiple filters", () => {
|
it("should handle multiple filters", () => {
|
||||||
const event = createEvent({kind: 1})
|
const event = createEvent(1)
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
|
|
||||||
const results = repo.query([{kinds: [1]}, {authors: [event.pubkey]}])
|
const results = repo.query([{kinds: [1]}, {authors: [event.pubkey]}])
|
||||||
@@ -202,8 +210,8 @@ describe("Repository", () => {
|
|||||||
|
|
||||||
it("should respect limit parameter", () => {
|
it("should respect limit parameter", () => {
|
||||||
const events = [
|
const events = [
|
||||||
createEvent({id: id + "1", created_at: currentTime}),
|
createEvent(1, {created_at: now()}),
|
||||||
createEvent({id: id + "2", created_at: currentTime - 100}),
|
createEvent(1, {created_at: now() - 100}),
|
||||||
]
|
]
|
||||||
|
|
||||||
events.forEach(e => repo.publish(e))
|
events.forEach(e => repo.publish(e))
|
||||||
@@ -214,13 +222,8 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should not return deleted events", () => {
|
it("should not return deleted events", () => {
|
||||||
const event = createEvent()
|
const event = createEvent(1)
|
||||||
const deleteEvent = createEvent({
|
const deleteEvent = createEvent(DELETE, {tags: [["e", event.id]], created_at: now() + 1})
|
||||||
id: "ee".repeat(32),
|
|
||||||
kind: DELETE,
|
|
||||||
tags: [["e", event.id]],
|
|
||||||
created_at: currentTime + 100,
|
|
||||||
})
|
|
||||||
|
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
repo.publish(deleteEvent)
|
repo.publish(deleteEvent)
|
||||||
@@ -238,7 +241,7 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should dump all events", () => {
|
it("should dump all events", () => {
|
||||||
const event = createEvent()
|
const event = createEvent(1)
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
|
|
||||||
const dumped = repo.dump()
|
const dumped = repo.dump()
|
||||||
@@ -246,21 +249,21 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should load events", () => {
|
it("should load events", () => {
|
||||||
const event = createEvent()
|
const event = createEvent(1)
|
||||||
repo.load([event])
|
repo.load([event])
|
||||||
|
|
||||||
expect(repo.getEvent(event.id)).toEqual(event)
|
expect(repo.getEvent(event.id)).toEqual(event)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle chunked loading", () => {
|
it("should handle chunked loading", () => {
|
||||||
const events = Array.from({length: 1500}, (_, i) => createEvent({id: id.slice(0, -1) + i}))
|
const events = Array.from({length: 1500}, (_, i) => createEvent(1))
|
||||||
|
|
||||||
repo.load(events, 500)
|
repo.load(events, 500)
|
||||||
expect(repo.dump()).toHaveLength(1500)
|
expect(repo.dump()).toHaveLength(1500)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should emit update events", () => {
|
it("should emit update events", () => {
|
||||||
const event = createEvent()
|
const event = createEvent(1)
|
||||||
const updateHandler = vi.fn()
|
const updateHandler = vi.fn()
|
||||||
|
|
||||||
repo.on("update", updateHandler)
|
repo.on("update", updateHandler)
|
||||||
@@ -281,13 +284,11 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should handle wrapped events", () => {
|
it("should handle wrapped events", () => {
|
||||||
const wrapped = createEvent()
|
const event: TrustedEvent = createEvent(1, {wrap: createEvent(1)})
|
||||||
const event = createEvent({
|
|
||||||
wrap: wrapped,
|
|
||||||
})
|
|
||||||
|
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
expect(repo.eventsByWrap.get(wrapped.id)).toEqual(event)
|
|
||||||
|
expect(repo.eventsByWrap.get(event.wrap!.id)).toEqual(event)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -299,7 +300,7 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should remove events", () => {
|
it("should remove events", () => {
|
||||||
const event = createEvent()
|
const event = createEvent(1)
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
repo.removeEvent(event.id)
|
repo.removeEvent(event.id)
|
||||||
|
|
||||||
@@ -307,10 +308,8 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should remove wrapped events", () => {
|
it("should remove wrapped events", () => {
|
||||||
const wrapped = createEvent()
|
const wrapped = createEvent(1)
|
||||||
const event = createEvent({
|
const event = createEvent(1, {wrap: wrapped})
|
||||||
wrap: wrapped,
|
|
||||||
})
|
|
||||||
|
|
||||||
repo.publish(event)
|
repo.publish(event)
|
||||||
repo.removeEvent(event.id)
|
repo.removeEvent(event.id)
|
||||||
@@ -319,7 +318,7 @@ describe("Repository", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should emit update on removal", () => {
|
it("should emit update on removal", () => {
|
||||||
const event = createEvent()
|
const event = createEvent(1)
|
||||||
const updateHandler = vi.fn()
|
const updateHandler = vi.fn()
|
||||||
|
|
||||||
repo.on("update", updateHandler)
|
repo.on("update", updateHandler)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
|
|||||||
eventsByAuthor = new Map<string, E[]>()
|
eventsByAuthor = new Map<string, E[]>()
|
||||||
eventsByKind = new Map<number, E[]>()
|
eventsByKind = new Map<number, E[]>()
|
||||||
deletes = new Map<string, number>()
|
deletes = new Map<string, number>()
|
||||||
|
expired = new Map<string, number>()
|
||||||
|
|
||||||
static get() {
|
static get() {
|
||||||
if (!repositorySingleton) {
|
if (!repositorySingleton) {
|
||||||
@@ -75,6 +76,7 @@ export class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
|
|||||||
this.eventsByAuthor.clear()
|
this.eventsByAuthor.clear()
|
||||||
this.eventsByKind.clear()
|
this.eventsByKind.clear()
|
||||||
this.deletes.clear()
|
this.deletes.clear()
|
||||||
|
this.expired.clear()
|
||||||
|
|
||||||
const added = []
|
const added = []
|
||||||
|
|
||||||
@@ -103,6 +105,11 @@ export class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
|
|||||||
removed.add(id)
|
removed.add(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Anything expired has been removed
|
||||||
|
for (const id of this.expired.keys()) {
|
||||||
|
removed.add(id)
|
||||||
|
}
|
||||||
|
|
||||||
this.emit("update", {added, removed})
|
this.emit("update", {added, removed})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +153,10 @@ export class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query = (filters: Filter[], {includeDeleted = false, shouldSort = true} = {}) => {
|
query = (
|
||||||
|
filters: Filter[],
|
||||||
|
{includeDeleted = false, includeExpired = false, shouldSort = true} = {},
|
||||||
|
) => {
|
||||||
const result: E[][] = []
|
const result: E[][] = []
|
||||||
for (const originalFilter of filters) {
|
for (const originalFilter of filters) {
|
||||||
if (originalFilter.limit !== undefined && !shouldSort) {
|
if (originalFilter.limit !== undefined && !shouldSort) {
|
||||||
@@ -169,6 +179,10 @@ export class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!includeExpired && this.isExpired(event)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (matchFilter(filter, event)) {
|
if (matchFilter(filter, event)) {
|
||||||
chunk.push(event)
|
chunk.push(event)
|
||||||
}
|
}
|
||||||
@@ -190,8 +204,8 @@ export class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've already seen this event, or it's been deleted, we're done
|
// If we've already seen this event we're done
|
||||||
if (this.eventsById.get(event.id) || this.isDeleted(event)) {
|
if (this.eventsById.get(event.id)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,6 +261,15 @@ export class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep track of whether this event is expired
|
||||||
|
if (tag[0] === "expiration") {
|
||||||
|
const expiration = parseInt(tag[1] || "")
|
||||||
|
|
||||||
|
if (!isNaN(expiration)) {
|
||||||
|
this.expired.set(event.id, expiration)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldNotify) {
|
if (shouldNotify) {
|
||||||
@@ -262,6 +285,12 @@ export class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
|
|||||||
|
|
||||||
isDeleted = (event: E) => this.isDeletedByAddress(event) || this.isDeletedById(event)
|
isDeleted = (event: E) => this.isDeletedByAddress(event) || this.isDeletedById(event)
|
||||||
|
|
||||||
|
isExpired = (event: E) => {
|
||||||
|
const ts = this.expired.get(event.id)
|
||||||
|
|
||||||
|
return Boolean(ts && ts < now())
|
||||||
|
}
|
||||||
|
|
||||||
// Utilities
|
// Utilities
|
||||||
|
|
||||||
_sortEvents = (shouldSort: boolean, events: E[]) =>
|
_sortEvents = (shouldSort: boolean, events: E[]) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user