applesauce-core
Advanced tools
Comparing version 0.9.0 to 0.10.0
@@ -6,2 +6,3 @@ import { Filter, NostrEvent } from "nostr-tools"; | ||
* An in-memory database for nostr events | ||
* NOTE: does not handle replaceable events | ||
*/ | ||
@@ -17,2 +18,3 @@ export declare class Database { | ||
events: LRU<import("nostr-tools").Event>; | ||
protected replaceable: Map<string, import("nostr-tools").Event[]>; | ||
/** A stream of events inserted into the database */ | ||
@@ -32,14 +34,16 @@ inserted: Subject<import("nostr-tools").Event>; | ||
touch(event: NostrEvent): void; | ||
hasEvent(uid: string): import("nostr-tools").Event | undefined; | ||
getEvent(uid: string): import("nostr-tools").Event | undefined; | ||
/** Checks if the database contains an event without touching it */ | ||
hasEvent(id: string): boolean; | ||
/** Gets a single event based on id */ | ||
getEvent(id: string): NostrEvent | undefined; | ||
/** Checks if the database contains a replaceable event without touching it */ | ||
hasReplaceable(kind: number, pubkey: string, d?: string): boolean; | ||
/** Gets a replaceable event and touches it */ | ||
getReplaceable(kind: number, pubkey: string, d?: string): import("nostr-tools").Event | undefined; | ||
/** Gets an array of replaceable events */ | ||
getReplaceable(kind: number, pubkey: string, d?: string): NostrEvent[] | undefined; | ||
/** Inserts an event into the database and notifies all subscriptions */ | ||
addEvent(event: NostrEvent): import("nostr-tools").Event; | ||
addEvent(event: NostrEvent): NostrEvent; | ||
/** Inserts and event into the database and notifies all subscriptions that the event has updated */ | ||
updateEvent(event: NostrEvent): import("nostr-tools").Event; | ||
updateEvent(event: NostrEvent): NostrEvent; | ||
/** Deletes an event from the database and notifies all subscriptions */ | ||
deleteEvent(eventOrUID: string | NostrEvent): boolean; | ||
deleteEvent(eventOrId: string | NostrEvent): boolean; | ||
/** Sets the claim on the event and touches it */ | ||
@@ -53,12 +57,12 @@ claimEvent(event: NostrEvent, claim: any): void; | ||
clearClaim(event: NostrEvent): void; | ||
iterateAuthors(authors: Iterable<string>): Generator<import("nostr-tools").Event, void, unknown>; | ||
iterateTag(tag: string, values: Iterable<string>): Generator<import("nostr-tools").Event, void, unknown>; | ||
iterateKinds(kinds: Iterable<number>): Generator<import("nostr-tools").Event, void, unknown>; | ||
iterateTime(since: number | undefined, until: number | undefined): Generator<never, Set<import("nostr-tools").Event>, unknown>; | ||
iterateIds(ids: Iterable<string>): Generator<import("nostr-tools").Event, void, unknown>; | ||
iterateAuthors(authors: Iterable<string>): Generator<NostrEvent>; | ||
iterateTag(tag: string, values: Iterable<string>): Generator<NostrEvent>; | ||
iterateKinds(kinds: Iterable<number>): Generator<NostrEvent>; | ||
iterateTime(since: number | undefined, until: number | undefined): Generator<NostrEvent>; | ||
iterateIds(ids: Iterable<string>): Generator<NostrEvent>; | ||
/** Returns all events that match the filter */ | ||
getEventsForFilter(filter: Filter): Set<NostrEvent>; | ||
getForFilters(filters: Filter[]): Set<import("nostr-tools").Event>; | ||
getForFilters(filters: Filter[]): Set<NostrEvent>; | ||
/** Remove the oldest events that are not claimed */ | ||
prune(limit?: number): number; | ||
} |
import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils"; | ||
import { Subject } from "rxjs"; | ||
import { FromCacheSymbol, getEventUID, getIndexableTags, getReplaceableUID } from "../helpers/event.js"; | ||
import { FromCacheSymbol, getEventUID, getIndexableTags, getReplaceableUID, isReplaceable } from "../helpers/event.js"; | ||
import { INDEXABLE_TAGS } from "./common.js"; | ||
@@ -9,2 +9,3 @@ import { logger } from "../logger.js"; | ||
* An in-memory database for nostr events | ||
* NOTE: does not handle replaceable events | ||
*/ | ||
@@ -20,2 +21,3 @@ export class Database { | ||
events = new LRU(); | ||
replaceable = new Map(); | ||
/** A stream of events inserted into the database */ | ||
@@ -61,31 +63,32 @@ inserted = new Subject(); | ||
touch(event) { | ||
this.events.set(getEventUID(event), event); | ||
this.events.set(event.id, event); | ||
} | ||
hasEvent(uid) { | ||
return this.events.get(uid); | ||
/** Checks if the database contains an event without touching it */ | ||
hasEvent(id) { | ||
return this.events.has(id); | ||
} | ||
getEvent(uid) { | ||
return this.events.get(uid); | ||
/** Gets a single event based on id */ | ||
getEvent(id) { | ||
return this.events.get(id); | ||
} | ||
/** Checks if the database contains a replaceable event without touching it */ | ||
hasReplaceable(kind, pubkey, d) { | ||
return this.events.has(getReplaceableUID(kind, pubkey, d)); | ||
const events = this.replaceable.get(getReplaceableUID(kind, pubkey, d)); | ||
return !!events && events.length > 0; | ||
} | ||
/** Gets a replaceable event and touches it */ | ||
/** Gets an array of replaceable events */ | ||
getReplaceable(kind, pubkey, d) { | ||
return this.events.get(getReplaceableUID(kind, pubkey, d)); | ||
return this.replaceable.get(getReplaceableUID(kind, pubkey, d)); | ||
} | ||
/** Inserts an event into the database and notifies all subscriptions */ | ||
addEvent(event) { | ||
const uid = getEventUID(event); | ||
const current = this.events.get(uid); | ||
if (current && event.created_at <= current.created_at) { | ||
const id = event.id; | ||
const current = this.events.get(id); | ||
if (current) { | ||
// if this is a duplicate event, transfer some import symbols | ||
if (current.id === event.id) { | ||
if (event[FromCacheSymbol]) | ||
current[FromCacheSymbol] = event[FromCacheSymbol]; | ||
} | ||
if (event[FromCacheSymbol]) | ||
current[FromCacheSymbol] = event[FromCacheSymbol]; | ||
return current; | ||
} | ||
this.events.set(uid, event); | ||
this.events.set(id, event); | ||
this.getKindIndex(event.kind).add(event); | ||
@@ -98,3 +101,14 @@ this.getAuthorsIndex(event.pubkey).add(event); | ||
} | ||
// insert into time index | ||
insertEventIntoDescendingList(this.created_at, event); | ||
// insert into replaceable index | ||
if (isReplaceable(event.kind)) { | ||
const uid = getEventUID(event); | ||
let array = this.replaceable.get(uid); | ||
if (!this.replaceable.has(uid)) { | ||
array = []; | ||
this.replaceable.set(uid, array); | ||
} | ||
insertEventIntoDescendingList(array, event); | ||
} | ||
this.inserted.next(event); | ||
@@ -110,9 +124,9 @@ return event; | ||
/** Deletes an event from the database and notifies all subscriptions */ | ||
deleteEvent(eventOrUID) { | ||
let event = typeof eventOrUID === "string" ? this.events.get(eventOrUID) : eventOrUID; | ||
deleteEvent(eventOrId) { | ||
let event = typeof eventOrId === "string" ? this.events.get(eventOrId) : eventOrId; | ||
if (!event) | ||
throw new Error("Missing event"); | ||
const uid = getEventUID(event); | ||
const id = event.id; | ||
// only remove events that are known | ||
if (!this.events.has(uid)) | ||
if (!this.events.has(id)) | ||
return false; | ||
@@ -129,3 +143,12 @@ this.getAuthorsIndex(event.pubkey).delete(event); | ||
this.created_at.splice(i, 1); | ||
this.events.delete(uid); | ||
this.events.delete(id); | ||
// remove from replaceable index | ||
if (isReplaceable(event.kind)) { | ||
const uid = getEventUID(event); | ||
const array = this.replaceable.get(uid); | ||
if (array && array.includes(event)) { | ||
const idx = array.indexOf(event); | ||
array.splice(idx, 1); | ||
} | ||
} | ||
this.deleted.next(event); | ||
@@ -188,23 +211,17 @@ return true; | ||
? binarySearch(this.created_at, (mid) => { | ||
if (mid.created_at === until) | ||
return -1; | ||
return mid.created_at - until; | ||
}) | ||
: undefined; | ||
if (start && start[1]) | ||
if (start) | ||
untilIndex = start[0]; | ||
const end = since | ||
? binarySearch(this.created_at, (mid) => { | ||
if (mid.created_at === since) | ||
return 1; | ||
return since - mid.created_at; | ||
return mid.created_at - since; | ||
}) | ||
: undefined; | ||
if (end && end[1]) | ||
if (end) | ||
sinceIndex = end[0]; | ||
const events = new Set(); | ||
for (let i = untilIndex; i <= sinceIndex; i++) { | ||
events.add(this.created_at[i]); | ||
for (let i = untilIndex; i < sinceIndex; i++) { | ||
yield this.created_at[i]; | ||
} | ||
return events; | ||
} | ||
@@ -211,0 +228,0 @@ *iterateIds(ids) { |
@@ -6,22 +6,45 @@ import { Filter, NostrEvent } from "nostr-tools"; | ||
database: Database; | ||
/** Enable this to keep old versions of replaceable events */ | ||
keepOldVersions: boolean; | ||
constructor(); | ||
/** Adds an event to the database */ | ||
add(event: NostrEvent, fromRelay?: string): import("nostr-tools").Event; | ||
/** Adds an event to the database and update subscriptions */ | ||
add(event: NostrEvent, fromRelay?: string): NostrEvent; | ||
/** Removes an event from the database and updates subscriptions */ | ||
remove(event: string | NostrEvent): boolean; | ||
protected deletedIds: Set<string>; | ||
protected deletedCoords: Map<string, number>; | ||
protected handleDeleteEvent(deleteEvent: NostrEvent): void; | ||
protected checkDeleted(event: NostrEvent): boolean; | ||
/** Removes any event that is not being used by a subscription */ | ||
prune(max?: number): number; | ||
/** Add an event to the store and notifies all subscribes it has updated */ | ||
update(event: NostrEvent): import("nostr-tools").Event; | ||
getAll(filters: Filter[]): Set<import("nostr-tools").Event>; | ||
hasEvent(uid: string): import("nostr-tools").Event | undefined; | ||
getEvent(uid: string): import("nostr-tools").Event | undefined; | ||
update(event: NostrEvent): NostrEvent; | ||
getAll(filters: Filter[]): Set<NostrEvent>; | ||
hasEvent(uid: string): boolean; | ||
getEvent(uid: string): NostrEvent | undefined; | ||
hasReplaceable(kind: number, pubkey: string, d?: string): boolean; | ||
getReplaceable(kind: number, pubkey: string, d?: string): import("nostr-tools").Event | undefined; | ||
/** Gets the latest version of a replaceable event */ | ||
getReplaceable(kind: number, pubkey: string, d?: string): NostrEvent | undefined; | ||
/** Returns all versions of a replaceable event */ | ||
getReplaceableHistory(kind: number, pubkey: string, d?: string): NostrEvent[] | undefined; | ||
/** Creates an observable that updates a single event */ | ||
event(uid: string): Observable<import("nostr-tools").Event | undefined>; | ||
event(id: string): Observable<NostrEvent | undefined>; | ||
/** Creates an observable that subscribes to multiple events */ | ||
events(uids: string[]): Observable<Map<string, import("nostr-tools").Event>>; | ||
/** Creates an observable that updates a single replaceable event */ | ||
replaceable(kind: number, pubkey: string, d?: string): Observable<import("nostr-tools").Event | undefined>; | ||
/** Creates an observable that streams all events that match the filter */ | ||
stream(filters: Filter[]): Observable<import("nostr-tools").Event>; | ||
events(ids: string[]): Observable<Map<string, NostrEvent>>; | ||
/** Creates an observable with the latest version of a replaceable event */ | ||
replaceable(kind: number, pubkey: string, d?: string): Observable<NostrEvent | undefined>; | ||
/** Creates an observable with the latest versions of replaceable events */ | ||
replaceableSet(pointers: { | ||
kind: number; | ||
pubkey: string; | ||
identifier?: string; | ||
}[]): Observable<Map<string, NostrEvent>>; | ||
/** | ||
* Creates an observable that streams all events that match the filter | ||
* @param filters | ||
* @param [onlyNew=false] Only subscribe to new events | ||
*/ | ||
stream(filters: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent>; | ||
/** Creates an observable that updates with an array of sorted events */ | ||
timeline(filters: Filter[]): Observable<import("nostr-tools").Event[]>; | ||
timeline(filters: Filter | Filter[], keepOldVersions?: boolean): Observable<NostrEvent[]>; | ||
} |
@@ -0,15 +1,39 @@ | ||
import { kinds } from "nostr-tools"; | ||
import { insertEventIntoDescendingList } from "nostr-tools/utils"; | ||
import { isParameterizedReplaceableKind } from "nostr-tools/kinds"; | ||
import { Observable } from "rxjs"; | ||
import { Database } from "./database.js"; | ||
import { getEventUID, getReplaceableUID } from "../helpers/event.js"; | ||
import { getEventUID, getReplaceableUID, getTagValue, isReplaceable } from "../helpers/event.js"; | ||
import { matchFilters } from "../helpers/filter.js"; | ||
import { addSeenRelay } from "../helpers/relays.js"; | ||
import { getDeleteCoordinates, getDeleteIds } from "../helpers/delete.js"; | ||
export class EventStore { | ||
database; | ||
/** Enable this to keep old versions of replaceable events */ | ||
keepOldVersions = false; | ||
constructor() { | ||
this.database = new Database(); | ||
} | ||
/** Adds an event to the database */ | ||
/** Adds an event to the database and update subscriptions */ | ||
add(event, fromRelay) { | ||
if (event.kind === kinds.EventDeletion) | ||
this.handleDeleteEvent(event); | ||
// ignore if the event was deleted | ||
if (this.checkDeleted(event)) | ||
return event; | ||
// insert event into database | ||
const inserted = this.database.addEvent(event); | ||
// remove all old version of the replaceable event | ||
if (!this.keepOldVersions && isReplaceable(event.kind)) { | ||
const current = this.database.getReplaceable(event.kind, event.pubkey, getTagValue(event, "d")); | ||
if (current) { | ||
const older = Array.from(current).filter((e) => e.created_at < event.created_at); | ||
for (const old of older) | ||
this.database.deleteEvent(old); | ||
// skip inserting this event because its not the newest | ||
if (current.length !== older.length) | ||
return current[0]; | ||
} | ||
} | ||
// attach relay this event was from | ||
if (fromRelay) | ||
@@ -19,2 +43,46 @@ addSeenRelay(inserted, fromRelay); | ||
} | ||
/** Removes an event from the database and updates subscriptions */ | ||
remove(event) { | ||
if (typeof event === "string") | ||
return this.database.deleteEvent(event); | ||
else if (this.database.hasEvent(event.id)) { | ||
return this.database.deleteEvent(event.id); | ||
} | ||
else | ||
return false; | ||
} | ||
deletedIds = new Set(); | ||
deletedCoords = new Map(); | ||
handleDeleteEvent(deleteEvent) { | ||
const ids = getDeleteIds(deleteEvent); | ||
for (const id of ids) { | ||
this.deletedIds.add(id); | ||
// remove deleted events in the database | ||
const event = this.database.getEvent(id); | ||
if (event) | ||
this.database.deleteEvent(event); | ||
} | ||
const coords = getDeleteCoordinates(deleteEvent); | ||
for (const coord of coords) { | ||
this.deletedCoords.set(coord, Math.max(this.deletedCoords.get(coord) ?? 0, deleteEvent.created_at)); | ||
// remove deleted events in the database | ||
const event = this.database.getEvent(coord); | ||
if (event && event.created_at < deleteEvent.created_at) | ||
this.database.deleteEvent(event); | ||
} | ||
} | ||
checkDeleted(event) { | ||
if (this.deletedIds.has(event.id)) | ||
return true; | ||
if (isParameterizedReplaceableKind(event.kind)) { | ||
const deleted = this.deletedCoords.get(getEventUID(event)); | ||
if (deleted) | ||
return deleted > event.created_at; | ||
} | ||
return false; | ||
} | ||
/** Removes any event that is not being used by a subscription */ | ||
prune(max) { | ||
return this.database.prune(max); | ||
} | ||
/** Add an event to the store and notifies all subscribes it has updated */ | ||
@@ -36,9 +104,14 @@ update(event) { | ||
} | ||
/** Gets the latest version of a replaceable event */ | ||
getReplaceable(kind, pubkey, d) { | ||
return this.database.getReplaceable(kind, pubkey, d)?.[0]; | ||
} | ||
/** Returns all versions of a replaceable event */ | ||
getReplaceableHistory(kind, pubkey, d) { | ||
return this.database.getReplaceable(kind, pubkey, d); | ||
} | ||
/** Creates an observable that updates a single event */ | ||
event(uid) { | ||
event(id) { | ||
return new Observable((observer) => { | ||
let current = this.database.getEvent(uid); | ||
let current = this.database.getEvent(id); | ||
if (current) { | ||
@@ -50,20 +123,16 @@ observer.next(current); | ||
const inserted = this.database.inserted.subscribe((event) => { | ||
if (getEventUID(event) === uid && (!current || event.created_at > current.created_at)) { | ||
// remove old claim | ||
if (current) | ||
this.database.removeClaim(current, observer); | ||
if (event.id === id) { | ||
current = event; | ||
observer.next(event); | ||
// claim new event | ||
this.database.claimEvent(current, observer); | ||
this.database.claimEvent(event, observer); | ||
} | ||
}); | ||
// subscribe to updates | ||
// subscribe to updated events | ||
const updated = this.database.updated.subscribe((event) => { | ||
if (event === current) | ||
observer.next(event); | ||
if (event.id === id) | ||
observer.next(current); | ||
}); | ||
// subscribe to deleted events | ||
const deleted = this.database.deleted.subscribe((event) => { | ||
if (getEventUID(event) === uid && current) { | ||
if (current?.id === event.id) { | ||
this.database.removeClaim(current, observer); | ||
@@ -75,5 +144,5 @@ current = undefined; | ||
return () => { | ||
inserted.unsubscribe(); | ||
deleted.unsubscribe(); | ||
updated.unsubscribe(); | ||
inserted.unsubscribe(); | ||
if (current) | ||
@@ -85,10 +154,10 @@ this.database.removeClaim(current, observer); | ||
/** Creates an observable that subscribes to multiple events */ | ||
events(uids) { | ||
events(ids) { | ||
return new Observable((observer) => { | ||
const events = new Map(); | ||
for (const uid of uids) { | ||
const e = this.getEvent(uid); | ||
if (e) { | ||
events.set(uid, e); | ||
this.database.claimEvent(e, observer); | ||
for (const id of ids) { | ||
const event = this.getEvent(id); | ||
if (event) { | ||
events.set(id, event); | ||
this.database.claimEvent(event, observer); | ||
} | ||
@@ -99,20 +168,13 @@ } | ||
const inserted = this.database.inserted.subscribe((event) => { | ||
const uid = getEventUID(event); | ||
if (uids.includes(uid)) { | ||
const current = events.get(uid); | ||
// remove old claim | ||
if (!current || event.created_at > current.created_at) { | ||
if (current) | ||
this.database.removeClaim(current, observer); | ||
events.set(uid, event); | ||
observer.next(events); | ||
// claim new event | ||
this.database.claimEvent(event, observer); | ||
} | ||
const id = event.id; | ||
if (ids.includes(id) && !events.has(id)) { | ||
events.set(id, event); | ||
observer.next(events); | ||
// claim new event | ||
this.database.claimEvent(event, observer); | ||
} | ||
}); | ||
// subscribe to updates | ||
// subscribe to updated events | ||
const updated = this.database.updated.subscribe((event) => { | ||
const uid = getEventUID(event); | ||
if (uids.includes(uid)) | ||
if (ids.includes(event.id)) | ||
observer.next(events); | ||
@@ -122,8 +184,8 @@ }); | ||
const deleted = this.database.deleted.subscribe((event) => { | ||
const uid = getEventUID(event); | ||
if (uids.includes(uid)) { | ||
const current = events.get(uid); | ||
const id = event.id; | ||
if (ids.includes(id)) { | ||
const current = events.get(id); | ||
if (current) { | ||
this.database.removeClaim(current, observer); | ||
events.delete(uid); | ||
events.delete(id); | ||
observer.next(events); | ||
@@ -143,45 +205,159 @@ } | ||
} | ||
/** Creates an observable that updates a single replaceable event */ | ||
/** Creates an observable with the latest version of a replaceable event */ | ||
replaceable(kind, pubkey, d) { | ||
return this.event(getReplaceableUID(kind, pubkey, d)); | ||
return new Observable((observer) => { | ||
const uid = getReplaceableUID(kind, pubkey, d); | ||
// get latest version | ||
let current = this.database.getReplaceable(kind, pubkey, d)?.[0]; | ||
if (current) { | ||
observer.next(current); | ||
this.database.claimEvent(current, observer); | ||
} | ||
// subscribe to future events | ||
const inserted = this.database.inserted.subscribe((event) => { | ||
if (getEventUID(event) === uid && (!current || event.created_at > current.created_at)) { | ||
// remove old claim | ||
if (current) | ||
this.database.removeClaim(current, observer); | ||
current = event; | ||
observer.next(event); | ||
// claim new event | ||
this.database.claimEvent(current, observer); | ||
} | ||
}); | ||
// subscribe to updated events | ||
const updated = this.database.updated.subscribe((event) => { | ||
if (event === current) | ||
observer.next(event); | ||
}); | ||
// subscribe to deleted events | ||
const deleted = this.database.deleted.subscribe((event) => { | ||
if (getEventUID(event) === uid && event === current) { | ||
this.database.removeClaim(current, observer); | ||
current = undefined; | ||
observer.next(undefined); | ||
} | ||
}); | ||
return () => { | ||
inserted.unsubscribe(); | ||
deleted.unsubscribe(); | ||
updated.unsubscribe(); | ||
if (current) | ||
this.database.removeClaim(current, observer); | ||
}; | ||
}); | ||
} | ||
/** Creates an observable that streams all events that match the filter */ | ||
stream(filters) { | ||
/** Creates an observable with the latest versions of replaceable events */ | ||
replaceableSet(pointers) { | ||
return new Observable((observer) => { | ||
let claimed = new Set(); | ||
let events = this.database.getForFilters(filters); | ||
for (const event of events) { | ||
observer.next(event); | ||
const coords = pointers.map((p) => getReplaceableUID(p.kind, p.pubkey, p.identifier)); | ||
const events = new Map(); | ||
const handleEvent = (event) => { | ||
const uid = getEventUID(event); | ||
const current = events.get(uid); | ||
if (current) { | ||
if (event.created_at > current.created_at) { | ||
this.database.removeClaim(current, observer); | ||
} | ||
else | ||
return; | ||
} | ||
events.set(uid, event); | ||
this.database.claimEvent(event, observer); | ||
claimed.add(event); | ||
}; | ||
// get latest version | ||
for (const pointer of pointers) { | ||
const events = this.database.getReplaceable(pointer.kind, pointer.pubkey, pointer.identifier); | ||
if (events) | ||
handleEvent(events[0]); | ||
} | ||
observer.next(events); | ||
// subscribe to future events | ||
const sub = this.database.inserted.subscribe((event) => { | ||
if (matchFilters(filters, event)) { | ||
observer.next(event); | ||
this.database.claimEvent(event, observer); | ||
claimed.add(event); | ||
const inserted = this.database.inserted.subscribe((event) => { | ||
if (isReplaceable(event.kind) && coords.includes(getEventUID(event))) { | ||
handleEvent(event); | ||
observer.next(events); | ||
} | ||
}); | ||
// subscribe to updated events | ||
const updated = this.database.updated.subscribe((event) => { | ||
if (isReplaceable(event.kind) && coords.includes(getEventUID(event))) { | ||
observer.next(events); | ||
} | ||
}); | ||
// subscribe to deleted events | ||
const deleted = this.database.deleted.subscribe((event) => { | ||
const uid = getEventUID(event); | ||
if (events.has(uid)) { | ||
events.delete(uid); | ||
this.database.removeClaim(event, observer); | ||
observer.next(events); | ||
} | ||
}); | ||
return () => { | ||
sub.unsubscribe(); | ||
// remove all claims | ||
for (const event of claimed) | ||
inserted.unsubscribe(); | ||
deleted.unsubscribe(); | ||
updated.unsubscribe(); | ||
for (const [_id, event] of events) { | ||
this.database.removeClaim(event, observer); | ||
claimed.clear(); | ||
} | ||
}; | ||
}); | ||
} | ||
/** | ||
* Creates an observable that streams all events that match the filter | ||
* @param filters | ||
* @param [onlyNew=false] Only subscribe to new events | ||
*/ | ||
stream(filters, onlyNew = false) { | ||
filters = Array.isArray(filters) ? filters : [filters]; | ||
return new Observable((observer) => { | ||
if (!onlyNew) { | ||
let events = this.database.getForFilters(filters); | ||
for (const event of events) | ||
observer.next(event); | ||
} | ||
// subscribe to future events | ||
const sub = this.database.inserted.subscribe((event) => { | ||
if (matchFilters(filters, event)) | ||
observer.next(event); | ||
}); | ||
return () => sub.unsubscribe(); | ||
}); | ||
} | ||
/** Creates an observable that updates with an array of sorted events */ | ||
timeline(filters) { | ||
timeline(filters, keepOldVersions = false) { | ||
filters = Array.isArray(filters) ? filters : [filters]; | ||
return new Observable((observer) => { | ||
const seen = new Map(); | ||
const timeline = []; | ||
// NOTE: only call this if we know the event is in timeline | ||
const removeFromTimeline = (event) => { | ||
timeline.splice(timeline.indexOf(event), 1); | ||
if (!keepOldVersions && isReplaceable(event.kind)) | ||
seen.delete(getEventUID(event)); | ||
this.database.removeClaim(event, observer); | ||
}; | ||
// inserts an event into the timeline and handles replaceable events | ||
const insertIntoTimeline = (event) => { | ||
// remove old versions | ||
if (!keepOldVersions && isReplaceable(event.kind)) { | ||
const uid = getEventUID(event); | ||
const old = seen.get(uid); | ||
if (old) { | ||
if (event.created_at > old.created_at) | ||
removeFromTimeline(old); | ||
else | ||
return; | ||
} | ||
seen.set(uid, event); | ||
} | ||
// insert into timeline | ||
insertEventIntoDescendingList(timeline, event); | ||
this.database.claimEvent(event, observer); | ||
}; | ||
// build initial timeline | ||
const events = this.database.getForFilters(filters); | ||
for (const event of events) { | ||
insertEventIntoDescendingList(timeline, event); | ||
this.database.claimEvent(event, observer); | ||
seen.set(getEventUID(event), event); | ||
} | ||
for (const event of events) | ||
insertIntoTimeline(event); | ||
observer.next([...timeline]); | ||
@@ -191,41 +367,17 @@ // subscribe to future events | ||
if (matchFilters(filters, event)) { | ||
const uid = getEventUID(event); | ||
let current = seen.get(uid); | ||
if (current) { | ||
if (event.created_at > current.created_at) { | ||
// replace event | ||
timeline.splice(timeline.indexOf(current), 1, event); | ||
observer.next([...timeline]); | ||
// update the claim | ||
seen.set(uid, event); | ||
this.database.removeClaim(current, observer); | ||
this.database.claimEvent(event, observer); | ||
} | ||
} | ||
else { | ||
insertEventIntoDescendingList(timeline, event); | ||
observer.next([...timeline]); | ||
// claim new event | ||
this.database.claimEvent(event, observer); | ||
seen.set(getEventUID(event), event); | ||
} | ||
insertIntoTimeline(event); | ||
observer.next([...timeline]); | ||
} | ||
}); | ||
// subscribe to updates | ||
// subscribe to updated events | ||
const updated = this.database.updated.subscribe((event) => { | ||
if (seen.has(getEventUID(event))) { | ||
if (timeline.includes(event)) { | ||
observer.next([...timeline]); | ||
} | ||
}); | ||
// subscribe to removed events | ||
// subscribe to deleted events | ||
const deleted = this.database.deleted.subscribe((event) => { | ||
const uid = getEventUID(event); | ||
let current = seen.get(uid); | ||
if (current) { | ||
// remove the event | ||
timeline.splice(timeline.indexOf(current), 1); | ||
if (timeline.includes(event)) { | ||
removeFromTimeline(event); | ||
observer.next([...timeline]); | ||
// remove the claim | ||
seen.delete(uid); | ||
this.database.removeClaim(current, observer); | ||
} | ||
@@ -238,5 +390,6 @@ }); | ||
// remove all claims | ||
for (const [_, event] of seen) { | ||
for (const event of timeline) { | ||
this.database.removeClaim(event, observer); | ||
} | ||
// forget seen replaceable events | ||
seen.clear(); | ||
@@ -243,0 +396,0 @@ }; |
@@ -8,2 +8,3 @@ export type ParsedInvoice = { | ||
}; | ||
/** Parses a lightning invoice */ | ||
export declare function parseBolt11(paymentRequest: string): ParsedInvoice; |
import { decode } from "light-bolt11-decoder"; | ||
/** Parses a lightning invoice */ | ||
export function parseBolt11(paymentRequest) { | ||
@@ -3,0 +4,0 @@ const decoded = decode(paymentRequest); |
import { EventTemplate, NostrEvent } from "nostr-tools"; | ||
export declare function getEmojiTag(event: NostrEvent | EventTemplate, code: string): ["emoji", string, string]; | ||
/** Gets an "emoji" tag that matches an emoji code */ | ||
export declare function getEmojiTag(event: NostrEvent | EventTemplate, code: string): ["emoji", string, string] | undefined; | ||
/** Returns the name of a NIP-30 emoji pack */ | ||
export declare function getPackName(pack: NostrEvent): string | undefined; | ||
export type Emoji = { | ||
name: string; | ||
url: string; | ||
}; | ||
/** Returns an array of emojis from a NIP-30 emoji pack */ | ||
export declare function getEmojis(pack: NostrEvent): Emoji[]; |
@@ -0,1 +1,3 @@ | ||
import { getTagValue } from "./event.js"; | ||
/** Gets an "emoji" tag that matches an emoji code */ | ||
export function getEmojiTag(event, code) { | ||
@@ -5,1 +7,11 @@ code = code.replace(/^:|:$/g, "").toLocaleLowerCase(); | ||
} | ||
/** Returns the name of a NIP-30 emoji pack */ | ||
export function getPackName(pack) { | ||
return getTagValue(pack, "title") || getTagValue(pack, "d"); | ||
} | ||
/** Returns an array of emojis from a NIP-30 emoji pack */ | ||
export function getEmojis(pack) { | ||
return pack.tags | ||
.filter((t) => t[0] === "emoji" && t[1] && t[2]) | ||
.map((t) => ({ name: t[1], url: t[2] })); | ||
} |
@@ -13,2 +13,7 @@ import { NostrEvent, VerifiedEvent } from "nostr-tools"; | ||
/** | ||
* Checks if an object is a nostr event | ||
* NOTE: does not validation the signature on the event | ||
*/ | ||
export declare function isEvent(event: any): event is NostrEvent; | ||
/** | ||
* Returns if a kind is replaceable ( 10000 <= n < 20000 || n == 0 || n == 3 ) | ||
@@ -28,3 +33,6 @@ * or parameterized replaceable ( 30000 <= n < 40000 ) | ||
export declare function getIndexableTags(event: NostrEvent): Set<string>; | ||
/** Returns the second index ( tag[1] ) of the first tag that matches the name */ | ||
/** | ||
* Returns the second index ( tag[1] ) of the first tag that matches the name | ||
* If the event has any hidden tags they will be searched first | ||
*/ | ||
export declare function getTagValue(event: NostrEvent, name: string): string | undefined; | ||
@@ -31,0 +39,0 @@ /** Sets events verified flag without checking anything */ |
import { kinds, verifiedSymbol } from "nostr-tools"; | ||
import { INDEXABLE_TAGS } from "../event-store/common.js"; | ||
import { getHiddenTags } from "./hidden-tags.js"; | ||
export const EventUIDSymbol = Symbol.for("event-uid"); | ||
@@ -7,2 +8,18 @@ export const EventIndexableTagsSymbol = Symbol.for("indexable-tags"); | ||
/** | ||
* Checks if an object is a nostr event | ||
* NOTE: does not validation the signature on the event | ||
*/ | ||
export function isEvent(event) { | ||
if (event === undefined || event === null) | ||
return false; | ||
return (event.id?.length === 64 && | ||
typeof event.sig === "string" && | ||
typeof event.pubkey === "string" && | ||
event.pubkey.length === 64 && | ||
typeof event.content === "string" && | ||
Array.isArray(event.tags) && | ||
typeof event.created_at === "number" && | ||
event.created_at > 0); | ||
} | ||
/** | ||
* Returns if a kind is replaceable ( 10000 <= n < 20000 || n == 0 || n == 3 ) | ||
@@ -50,4 +67,11 @@ * or parameterized replaceable ( 30000 <= n < 40000 ) | ||
} | ||
/** Returns the second index ( tag[1] ) of the first tag that matches the name */ | ||
/** | ||
* Returns the second index ( tag[1] ) of the first tag that matches the name | ||
* If the event has any hidden tags they will be searched first | ||
*/ | ||
export function getTagValue(event, name) { | ||
const hidden = getHiddenTags(event); | ||
const hiddenValue = hidden?.find((t) => t[0] === name)?.[1]; | ||
if (hiddenValue) | ||
return hiddenValue; | ||
return event.tags.find((t) => t[0] === name)?.[1]; | ||
@@ -54,0 +78,0 @@ } |
@@ -9,5 +9,3 @@ import { Filter, NostrEvent } from "nostr-tools"; | ||
export declare function matchFilters(filters: Filter[], event: NostrEvent): boolean; | ||
/** Stringify filters in a predictable way */ | ||
export declare function stringifyFilter(filter: Filter | Filter[]): string; | ||
/** Check if two filters are equal */ | ||
export declare function isFilterEqual(a: Filter | Filter[], b: Filter | Filter[]): boolean; |
import { getIndexableTags } from "./event.js"; | ||
import stringify from "json-stringify-deterministic"; | ||
import equal from "fast-deep-equal"; | ||
/** | ||
@@ -23,3 +23,3 @@ * Copied from nostr-tools and modified to use getIndexableTags | ||
const tags = getIndexableTags(event); | ||
if (values.some((v) => !tags.has(tagName + ":" + v))) | ||
if (values.some((v) => tags.has(tagName + ":" + v)) === false) | ||
return false; | ||
@@ -44,9 +44,5 @@ } | ||
} | ||
/** Stringify filters in a predictable way */ | ||
export function stringifyFilter(filter) { | ||
return stringify(filter); | ||
} | ||
/** Check if two filters are equal */ | ||
export function isFilterEqual(a, b) { | ||
return stringifyFilter(a) === stringifyFilter(b); | ||
return equal(a, b); | ||
} |
@@ -1,16 +0,24 @@ | ||
export * from "./profile.js"; | ||
export * from "./relays.js"; | ||
export * from "./bolt11.js"; | ||
export * from "./cache.js"; | ||
export * from "./comment.js"; | ||
export * from "./content.js"; | ||
export * from "./delete.js"; | ||
export * from "./emoji.js"; | ||
export * from "./event.js"; | ||
export * from "./external-id.js"; | ||
export * from "./filter.js"; | ||
export * from "./hashtag.js"; | ||
export * from "./hidden-tags.js"; | ||
export * from "./lnurl.js"; | ||
export * from "./lru.js"; | ||
export * from "./mailboxes.js"; | ||
export * from "./threading.js"; | ||
export * from "./media-attachment.js"; | ||
export * from "./pointers.js"; | ||
export * from "./profile.js"; | ||
export * from "./relays.js"; | ||
export * from "./string.js"; | ||
export * from "./tags.js"; | ||
export * from "./threading.js"; | ||
export * from "./time.js"; | ||
export * from "./tags.js"; | ||
export * from "./emoji.js"; | ||
export * from "./lru.js"; | ||
export * from "./hashtag.js"; | ||
export * from "./url.js"; | ||
export * from "./zap.js"; | ||
export * from "./bolt11.js"; |
@@ -1,16 +0,24 @@ | ||
export * from "./profile.js"; | ||
export * from "./relays.js"; | ||
export * from "./bolt11.js"; | ||
export * from "./cache.js"; | ||
export * from "./comment.js"; | ||
export * from "./content.js"; | ||
export * from "./delete.js"; | ||
export * from "./emoji.js"; | ||
export * from "./event.js"; | ||
export * from "./external-id.js"; | ||
export * from "./filter.js"; | ||
export * from "./hashtag.js"; | ||
export * from "./hidden-tags.js"; | ||
export * from "./lnurl.js"; | ||
export * from "./lru.js"; | ||
export * from "./mailboxes.js"; | ||
export * from "./threading.js"; | ||
export * from "./media-attachment.js"; | ||
export * from "./pointers.js"; | ||
export * from "./profile.js"; | ||
export * from "./relays.js"; | ||
export * from "./string.js"; | ||
export * from "./tags.js"; | ||
export * from "./threading.js"; | ||
export * from "./time.js"; | ||
export * from "./tags.js"; | ||
export * from "./emoji.js"; | ||
export * from "./lru.js"; | ||
export * from "./hashtag.js"; | ||
export * from "./url.js"; | ||
export * from "./zap.js"; | ||
export * from "./bolt11.js"; |
@@ -0,1 +1,2 @@ | ||
/** Returns the parsed JSON or undefined if invalid */ | ||
export declare function safeParse<T extends unknown = any>(str: string): T | undefined; |
@@ -0,1 +1,2 @@ | ||
/** Returns the parsed JSON or undefined if invalid */ | ||
export function safeParse(str) { | ||
@@ -2,0 +3,0 @@ try { |
@@ -0,1 +1,2 @@ | ||
import { describe, test, expect } from "vitest"; | ||
import { getInboxes, getOutboxes } from "./mailboxes.js"; | ||
@@ -17,3 +18,3 @@ const emptyEvent = { | ||
tags: [["r", "wss://inbox.com"]], | ||
}))).toIncludeAllMembers(["wss://inbox.com/"]); | ||
}))).toEqual(expect.arrayContaining(["wss://inbox.com/"])); | ||
}); | ||
@@ -24,11 +25,11 @@ test("should remove bad urls", () => { | ||
tags: [["r", "bad://inbox.com"]], | ||
}))).toBeArrayOfSize(0); | ||
}))).toHaveLength(0); | ||
expect(Array.from(getInboxes({ | ||
...emptyEvent, | ||
tags: [["r", "something that is not a url"]], | ||
}))).toBeArrayOfSize(0); | ||
}))).toHaveLength(0); | ||
expect(Array.from(getInboxes({ | ||
...emptyEvent, | ||
tags: [["r", "wss://inbox.com,wss://inbox.org"]], | ||
}))).toBeArrayOfSize(0); | ||
}))).toHaveLength(0); | ||
}); | ||
@@ -39,3 +40,3 @@ test("without marker", () => { | ||
tags: [["r", "wss://inbox.com/"]], | ||
}))).toIncludeAllMembers(["wss://inbox.com/"]); | ||
}))).toEqual(expect.arrayContaining(["wss://inbox.com/"])); | ||
}); | ||
@@ -46,3 +47,3 @@ test("with marker", () => { | ||
tags: [["r", "wss://inbox.com/", "read"]], | ||
}))).toIncludeAllMembers(["wss://inbox.com/"]); | ||
}))).toEqual(expect.arrayContaining(["wss://inbox.com/"])); | ||
}); | ||
@@ -55,3 +56,3 @@ }); | ||
tags: [["r", "wss://outbox.com"]], | ||
}))).toIncludeAllMembers(["wss://outbox.com/"]); | ||
}))).toEqual(expect.arrayContaining(["wss://outbox.com/"])); | ||
}); | ||
@@ -62,11 +63,11 @@ test("should remove bad urls", () => { | ||
tags: [["r", "bad://inbox.com"]], | ||
}))).toBeArrayOfSize(0); | ||
}))).toHaveLength(0); | ||
expect(Array.from(getOutboxes({ | ||
...emptyEvent, | ||
tags: [["r", "something that is not a url"]], | ||
}))).toBeArrayOfSize(0); | ||
}))).toHaveLength(0); | ||
expect(Array.from(getOutboxes({ | ||
...emptyEvent, | ||
tags: [["r", "wss://outbox.com,wss://inbox.org"]], | ||
}))).toBeArrayOfSize(0); | ||
}))).toHaveLength(0); | ||
}); | ||
@@ -77,3 +78,3 @@ test("without marker", () => { | ||
tags: [["r", "wss://outbox.com/"]], | ||
}))).toIncludeAllMembers(["wss://outbox.com/"]); | ||
}))).toEqual(expect.arrayContaining(["wss://outbox.com/"])); | ||
}); | ||
@@ -84,5 +85,5 @@ test("with marker", () => { | ||
tags: [["r", "wss://outbox.com/", "write"]], | ||
}))).toIncludeAllMembers(["wss://outbox.com/"]); | ||
}))).toEqual(expect.arrayContaining(["wss://outbox.com/"])); | ||
}); | ||
}); | ||
}); |
import { AddressPointer, DecodeResult, EventPointer, ProfilePointer } from "nostr-tools/nip19"; | ||
import { NostrEvent } from "nostr-tools"; | ||
export type AddressPointerWithoutD = Omit<AddressPointer, "identifier"> & { | ||
identifier?: string; | ||
}; | ||
/** Parse the value of an "a" tag into an AddressPointer */ | ||
export declare function parseCoordinate(a: string): AddressPointerWithoutD | null; | ||
@@ -12,12 +14,43 @@ export declare function parseCoordinate(a: string, requireD: false): AddressPointerWithoutD | null; | ||
export declare function parseCoordinate(a: string, requireD: false, silent: true): AddressPointerWithoutD | null; | ||
/** Extra a pubkey from the result of nip19.decode */ | ||
export declare function getPubkeyFromDecodeResult(result?: DecodeResult): string | undefined; | ||
/** Encodes the result of nip19.decode */ | ||
export declare function encodeDecodeResult(result: DecodeResult): "" | `nprofile1${string}` | `nevent1${string}` | `naddr1${string}` | `nsec1${string}` | `npub1${string}` | `note1${string}`; | ||
export declare function getEventPointerFromTag(tag: string[]): EventPointer; | ||
export declare function getAddressPointerFromTag(tag: string[]): AddressPointer; | ||
export declare function getProfilePointerFromTag(tag: string[]): ProfilePointer; | ||
/** | ||
* Gets an EventPointer form a common "e" tag | ||
* @throws | ||
*/ | ||
export declare function getEventPointerFromETag(tag: string[]): EventPointer; | ||
/** | ||
* Gets an EventPointer form a "q" tag | ||
* @throws | ||
*/ | ||
export declare function getEventPointerFromQTag(tag: string[]): EventPointer; | ||
/** | ||
* Get an AddressPointer from an "a" tag | ||
* @throws | ||
*/ | ||
export declare function getAddressPointerFromATag(tag: string[]): AddressPointer; | ||
/** | ||
* Gets a ProfilePointer from a "p" tag | ||
* @throws | ||
*/ | ||
export declare function getProfilePointerFromPTag(tag: string[]): ProfilePointer; | ||
/** Parses "e", "a", "p", and "q" tags into a pointer */ | ||
export declare function getPointerFromTag(tag: string[]): DecodeResult | null; | ||
export declare function isAddressPointer(pointer: DecodeResult["data"]): pointer is AddressPointer; | ||
export declare function isEventPointer(pointer: DecodeResult["data"]): pointer is EventPointer; | ||
/** Returns the coordinate string for an AddressPointer */ | ||
export declare function getCoordinateFromAddressPointer(pointer: AddressPointer): string; | ||
export declare function getATagFromAddressPointer(pointer: AddressPointer): ["a", ...string[]]; | ||
export declare function getETagFromEventPointer(pointer: EventPointer): ["e", ...string[]]; | ||
/** | ||
* Returns an AddressPointer for a replaceable event | ||
* @throws | ||
*/ | ||
export declare function getAddressPointerForEvent(event: NostrEvent, relays?: string[]): AddressPointer; | ||
/** | ||
* Returns an EventPointer for an event | ||
* @throws | ||
*/ | ||
export declare function getEventPointerForEvent(event: NostrEvent, relays?: string[]): EventPointer; | ||
/** Returns a pointer for a given event */ | ||
export declare function getPointerForEvent(event: NostrEvent, relays?: string[]): DecodeResult; |
import { naddrEncode, neventEncode, noteEncode, nprofileEncode, npubEncode, nsecEncode, } from "nostr-tools/nip19"; | ||
import { getPublicKey } from "nostr-tools"; | ||
import { getPublicKey, kinds } from "nostr-tools"; | ||
import { safeRelayUrls } from "./relays.js"; | ||
import { getTagValue } from "./index.js"; | ||
import { isParameterizedReplaceableKind } from "nostr-tools/kinds"; | ||
export function parseCoordinate(a, requireD = false, silent = true) { | ||
@@ -33,2 +35,3 @@ const parts = a.split(":"); | ||
} | ||
/** Extra a pubkey from the result of nip19.decode */ | ||
export function getPubkeyFromDecodeResult(result) { | ||
@@ -49,2 +52,3 @@ if (!result) | ||
} | ||
/** Encodes the result of nip19.decode */ | ||
export function encodeDecodeResult(result) { | ||
@@ -67,3 +71,7 @@ switch (result.type) { | ||
} | ||
export function getEventPointerFromTag(tag) { | ||
/** | ||
* Gets an EventPointer form a common "e" tag | ||
* @throws | ||
*/ | ||
export function getEventPointerFromETag(tag) { | ||
if (!tag[1]) | ||
@@ -76,4 +84,22 @@ throw new Error("Missing event id in tag"); | ||
} | ||
export function getAddressPointerFromTag(tag) { | ||
/** | ||
* Gets an EventPointer form a "q" tag | ||
* @throws | ||
*/ | ||
export function getEventPointerFromQTag(tag) { | ||
if (!tag[1]) | ||
throw new Error("Missing event id in tag"); | ||
let pointer = { id: tag[1] }; | ||
if (tag[2]) | ||
pointer.relays = safeRelayUrls([tag[2]]); | ||
if (tag[3] && tag[3].length === 64) | ||
pointer.author = tag[3]; | ||
return pointer; | ||
} | ||
/** | ||
* Get an AddressPointer from an "a" tag | ||
* @throws | ||
*/ | ||
export function getAddressPointerFromATag(tag) { | ||
if (!tag[1]) | ||
throw new Error("Missing coordinate in tag"); | ||
@@ -85,3 +111,7 @@ const pointer = parseCoordinate(tag[1], true, false); | ||
} | ||
export function getProfilePointerFromTag(tag) { | ||
/** | ||
* Gets a ProfilePointer from a "p" tag | ||
* @throws | ||
*/ | ||
export function getProfilePointerFromPTag(tag) { | ||
if (!tag[1]) | ||
@@ -94,2 +124,3 @@ throw new Error("Missing pubkey in tag"); | ||
} | ||
/** Parses "e", "a", "p", and "q" tags into a pointer */ | ||
export function getPointerFromTag(tag) { | ||
@@ -99,10 +130,13 @@ try { | ||
case "e": | ||
return { type: "nevent", data: getEventPointerFromTag(tag) }; | ||
return { type: "nevent", data: getEventPointerFromETag(tag) }; | ||
case "a": | ||
return { | ||
type: "naddr", | ||
data: getAddressPointerFromTag(tag), | ||
data: getAddressPointerFromATag(tag), | ||
}; | ||
case "p": | ||
return { type: "nprofile", data: getProfilePointerFromTag(tag) }; | ||
return { type: "nprofile", data: getProfilePointerFromPTag(tag) }; | ||
// NIP-18 quote tags | ||
case "q": | ||
return { type: "nevent", data: getEventPointerFromETag(tag) }; | ||
} | ||
@@ -115,19 +149,69 @@ } | ||
return (typeof pointer !== "string" && | ||
Object.hasOwn(pointer, "identifier") && | ||
Object.hasOwn(pointer, "pubkey") && | ||
Object.hasOwn(pointer, "kind")); | ||
Reflect.has(pointer, "identifier") && | ||
Reflect.has(pointer, "pubkey") && | ||
Reflect.has(pointer, "kind")); | ||
} | ||
export function isEventPointer(pointer) { | ||
return typeof pointer !== "string" && Object.hasOwn(pointer, "id"); | ||
return typeof pointer !== "string" && Reflect.has(pointer, "id"); | ||
} | ||
/** Returns the coordinate string for an AddressPointer */ | ||
export function getCoordinateFromAddressPointer(pointer) { | ||
return `${pointer.kind}:${pointer.pubkey}:${pointer.identifier}`; | ||
} | ||
export function getATagFromAddressPointer(pointer) { | ||
const relay = pointer.relays?.[0]; | ||
const coordinate = getCoordinateFromAddressPointer(pointer); | ||
return relay ? ["a", coordinate, relay] : ["a", coordinate]; | ||
/** | ||
* Returns an AddressPointer for a replaceable event | ||
* @throws | ||
*/ | ||
export function getAddressPointerForEvent(event, relays) { | ||
if (!isParameterizedReplaceableKind(event.kind)) | ||
throw new Error("Cant get AddressPointer for non-replaceable event"); | ||
const d = getTagValue(event, "d"); | ||
if (!d) | ||
throw new Error("Event missing identifier"); | ||
return { | ||
identifier: d, | ||
kind: event.kind, | ||
pubkey: event.pubkey, | ||
relays, | ||
}; | ||
} | ||
export function getETagFromEventPointer(pointer) { | ||
return pointer.relays?.length ? ["e", pointer.id, pointer.relays[0]] : ["e", pointer.id]; | ||
/** | ||
* Returns an EventPointer for an event | ||
* @throws | ||
*/ | ||
export function getEventPointerForEvent(event, relays) { | ||
return { | ||
id: event.id, | ||
kind: event.kind, | ||
author: event.pubkey, | ||
relays, | ||
}; | ||
} | ||
/** Returns a pointer for a given event */ | ||
export function getPointerForEvent(event, relays) { | ||
if (kinds.isParameterizedReplaceableKind(event.kind)) { | ||
const d = getTagValue(event, "d"); | ||
if (!d) | ||
throw new Error("Event missing identifier"); | ||
return { | ||
type: "naddr", | ||
data: { | ||
identifier: d, | ||
kind: event.kind, | ||
pubkey: event.pubkey, | ||
relays, | ||
}, | ||
}; | ||
} | ||
else { | ||
return { | ||
type: "nevent", | ||
data: { | ||
id: event.id, | ||
kind: event.kind, | ||
author: event.pubkey, | ||
relays, | ||
}, | ||
}; | ||
} | ||
} |
@@ -12,3 +12,3 @@ import { kinds } from "nostr-tools"; | ||
// add missing protocol to website | ||
if (profile.website?.startsWith("http") === false) { | ||
if (profile.website && profile.website?.length > 0 && profile.website?.startsWith("http") === false) { | ||
profile.website = "https://" + profile.website; | ||
@@ -15,0 +15,0 @@ } |
@@ -0,4 +1,10 @@ | ||
/** Tests if a string is hex */ | ||
export declare function isHex(str?: string): boolean; | ||
/** Tests if a string is a 64 length hex string */ | ||
export declare function isHexKey(key?: string): boolean; | ||
/** | ||
* Remove invisible characters from a string | ||
* @see read more https://www.regular-expressions.info/unicode.html#category | ||
*/ | ||
export declare function stripInvisibleChar(str: string): string; | ||
export declare function stripInvisibleChar(str?: string | undefined): string | undefined; |
@@ -0,1 +1,2 @@ | ||
/** Tests if a string is hex */ | ||
export function isHex(str) { | ||
@@ -6,2 +7,3 @@ if (str?.match(/^[0-9a-f]+$/i)) | ||
} | ||
/** Tests if a string is a 64 length hex string */ | ||
export function isHexKey(key) { | ||
@@ -8,0 +10,0 @@ if (key?.toLowerCase()?.match(/^[0-9a-f]{64}$/)) |
@@ -0,6 +1,12 @@ | ||
/** Checks if tag is an "e" tag and has at least one value */ | ||
export declare function isETag(tag: string[]): tag is ["e", string, ...string[]]; | ||
/** Checks if tag is an "p" tag and has at least one value */ | ||
export declare function isPTag(tag: string[]): tag is ["p", string, ...string[]]; | ||
/** Checks if tag is an "r" tag and has at least one value */ | ||
export declare function isRTag(tag: string[]): tag is ["r", string, ...string[]]; | ||
/** Checks if tag is an "d" tag and has at least one value */ | ||
export declare function isDTag(tag: string[]): tag is ["d", string, ...string[]]; | ||
/** Checks if tag is an "a" tag and has at least one value */ | ||
export declare function isATag(tag: string[]): tag is ["a", string, ...string[]]; | ||
/** Checks if tag is an "a" tag and has at least one value */ | ||
export declare function isTTag(tag: string[]): tag is ["t", string, ...string[]]; |
@@ -0,18 +1,24 @@ | ||
/** Checks if tag is an "e" tag and has at least one value */ | ||
export function isETag(tag) { | ||
return tag[0] === "e" && tag[1] !== undefined; | ||
} | ||
/** Checks if tag is an "p" tag and has at least one value */ | ||
export function isPTag(tag) { | ||
return tag[0] === "p" && tag[1] !== undefined; | ||
} | ||
/** Checks if tag is an "r" tag and has at least one value */ | ||
export function isRTag(tag) { | ||
return tag[0] === "r" && tag[1] !== undefined; | ||
} | ||
/** Checks if tag is an "d" tag and has at least one value */ | ||
export function isDTag(tag) { | ||
return tag[0] === "d" && tag[1] !== undefined; | ||
} | ||
/** Checks if tag is an "a" tag and has at least one value */ | ||
export function isATag(tag) { | ||
return tag[0] === "a" && tag[1] !== undefined; | ||
} | ||
/** Checks if tag is an "a" tag and has at least one value */ | ||
export function isTTag(tag) { | ||
return tag[0] === "a" && tag[1] !== undefined; | ||
} |
@@ -26,9 +26,9 @@ import { EventTemplate, NostrEvent } from "nostr-tools"; | ||
export declare const Nip10ThreadRefsSymbol: unique symbol; | ||
declare module "nostr-tools" { | ||
interface Event { | ||
[Nip10ThreadRefsSymbol]?: ThreadReferences; | ||
} | ||
} | ||
/** | ||
* Gets an EventPointer form a NIP-10 threading "e" tag | ||
* @throws | ||
*/ | ||
export declare function getEventPointerFromThreadTag(tag: string[]): EventPointer; | ||
/** Parses NIP-10 tags and handles legacy behavior */ | ||
export declare function interpretThreadTags(event: NostrEvent | EventTemplate): { | ||
export declare function interpretThreadTags(tags: string[][]): { | ||
root?: { | ||
@@ -35,0 +35,0 @@ e: string[]; |
@@ -1,8 +0,29 @@ | ||
import { getAddressPointerFromTag, getEventPointerFromTag } from "./pointers.js"; | ||
import { getAddressPointerFromATag } from "./pointers.js"; | ||
import { getOrComputeCachedValue } from "./cache.js"; | ||
import { safeRelayUrls } from "./relays.js"; | ||
export const Nip10ThreadRefsSymbol = Symbol.for("nip10-thread-refs"); | ||
/** | ||
* Gets an EventPointer form a NIP-10 threading "e" tag | ||
* @throws | ||
*/ | ||
export function getEventPointerFromThreadTag(tag) { | ||
if (!tag[1]) | ||
throw new Error("Missing event id in tag"); | ||
let pointer = { id: tag[1] }; | ||
if (tag[2]) | ||
pointer.relays = safeRelayUrls([tag[2]]); | ||
// get author from NIP-18 quote tags, nip-22 comments tags, or nip-10 thread tags | ||
if (tag[0] === "e" && | ||
(tag[3] === "root" || tag[3] === "reply" || tag[3] === "mention") && | ||
tag[4] && | ||
tag[4].length === 64) { | ||
// NIP-10 "e" tag | ||
pointer.author = tag[4]; | ||
} | ||
return pointer; | ||
} | ||
/** Parses NIP-10 tags and handles legacy behavior */ | ||
export function interpretThreadTags(event) { | ||
const eTags = event.tags.filter((t) => t[0] === "e" && t[1]); | ||
const aTags = event.tags.filter((t) => t[0] === "a" && t[1]); | ||
export function interpretThreadTags(tags) { | ||
const eTags = tags.filter((t) => t[0] === "e" && t[1]); | ||
const aTags = tags.filter((t) => t[0] === "a" && t[1]); | ||
// find the root and reply tags. | ||
@@ -47,3 +68,3 @@ let rootETag = eTags.find((t) => t[3] === "root"); | ||
return getOrComputeCachedValue(event, Nip10ThreadRefsSymbol, () => { | ||
const tags = interpretThreadTags(event); | ||
const tags = interpretThreadTags(event.tags); | ||
let root; | ||
@@ -53,4 +74,4 @@ if (tags.root) { | ||
root = { | ||
e: tags.root.e && getEventPointerFromTag(tags.root.e), | ||
a: tags.root.a && getAddressPointerFromTag(tags.root.a), | ||
e: tags.root.e && getEventPointerFromThreadTag(tags.root.e), | ||
a: tags.root.a && getAddressPointerFromATag(tags.root.a), | ||
}; | ||
@@ -64,4 +85,4 @@ } | ||
reply = { | ||
e: tags.reply.e && getEventPointerFromTag(tags.reply.e), | ||
a: tags.reply.a && getAddressPointerFromTag(tags.reply.a), | ||
e: tags.reply.e && getEventPointerFromThreadTag(tags.reply.e), | ||
a: tags.reply.a && getAddressPointerFromATag(tags.reply.a), | ||
}; | ||
@@ -68,0 +89,0 @@ } |
@@ -7,6 +7,9 @@ export declare const convertToUrl: (url: string | URL) => URL; | ||
export declare const AUDIO_EXT: string[]; | ||
export declare function isVisualMediaURL(url: string | URL): boolean; | ||
/** Checks if a url is a image URL */ | ||
export declare function isImageURL(url: string | URL): boolean; | ||
/** Checks if a url is a video URL */ | ||
export declare function isVideoURL(url: string | URL): boolean; | ||
/** Checks if a url is a stream URL */ | ||
export declare function isStreamURL(url: string | URL): boolean; | ||
/** Checks if a url is a audio URL */ | ||
export declare function isAudioURL(url: string | URL): boolean; |
@@ -7,5 +7,3 @@ export const convertToUrl = (url) => (url instanceof URL ? url : new URL(url)); | ||
export const AUDIO_EXT = [".mp3", ".wav", ".ogg", ".aac"]; | ||
export function isVisualMediaURL(url) { | ||
return isImageURL(url) || isVideoURL(url) || isStreamURL(url); | ||
} | ||
/** Checks if a url is a image URL */ | ||
export function isImageURL(url) { | ||
@@ -16,2 +14,3 @@ url = convertToUrl(url); | ||
} | ||
/** Checks if a url is a video URL */ | ||
export function isVideoURL(url) { | ||
@@ -22,2 +21,3 @@ url = convertToUrl(url); | ||
} | ||
/** Checks if a url is a stream URL */ | ||
export function isStreamURL(url) { | ||
@@ -28,2 +28,3 @@ url = convertToUrl(url); | ||
} | ||
/** Checks if a url is a audio URL */ | ||
export function isAudioURL(url) { | ||
@@ -30,0 +31,0 @@ url = convertToUrl(url); |
@@ -7,9 +7,34 @@ import { NostrEvent } from "nostr-tools"; | ||
export declare const ZapAddressPointerSymbol: unique symbol; | ||
/** Returns the senders pubkey */ | ||
export declare function getZapSender(zap: NostrEvent): string; | ||
/** | ||
* Gets the receivers pubkey | ||
* @throws | ||
*/ | ||
export declare function getZapRecipient(zap: NostrEvent): string; | ||
/** Returns the parsed bolt11 invoice */ | ||
export declare function getZapPayment(zap: NostrEvent): import("./bolt11.js").ParsedInvoice | undefined; | ||
/** Gets the AddressPointer that was zapped */ | ||
export declare function getZapAddressPointer(zap: NostrEvent): import("nostr-tools/nip19").AddressPointer | null; | ||
/** Gets the EventPointer that was zapped */ | ||
export declare function getZapEventPointer(zap: NostrEvent): import("nostr-tools/nip19").EventPointer | null; | ||
/** Gets the preimage for the bolt11 invoice */ | ||
export declare function getZapPreimage(zap: NostrEvent): string | undefined; | ||
/** | ||
* Returns the zap request event inside the zap receipt | ||
* @throws | ||
*/ | ||
export declare function getZapRequest(zap: NostrEvent): import("nostr-tools").Event; | ||
/** | ||
* Checks if the zap is valid | ||
* DOES NOT validate LNURL address | ||
*/ | ||
export declare function isValidZap(zap?: NostrEvent): boolean; | ||
export type ZapSplit = { | ||
pubkey: string; | ||
percent: number; | ||
weight: number; | ||
relay?: string; | ||
}; | ||
/** Returns the zap splits for an event */ | ||
export declare function getZapSplits(event: NostrEvent): ZapSplit[] | undefined; |
@@ -5,3 +5,3 @@ import { kinds, nip57 } from "nostr-tools"; | ||
import { isATag, isETag } from "./tags.js"; | ||
import { getAddressPointerFromTag, getEventPointerFromTag } from "./pointers.js"; | ||
import { getAddressPointerFromATag, getEventPointerFromETag } from "./pointers.js"; | ||
import { parseBolt11 } from "./bolt11.js"; | ||
@@ -13,5 +13,10 @@ export const ZapRequestSymbol = Symbol.for("zap-request"); | ||
export const ZapAddressPointerSymbol = Symbol.for("zap-address-pointer"); | ||
/** Returns the senders pubkey */ | ||
export function getZapSender(zap) { | ||
return getTagValue(zap, "P") || getZapRequest(zap).pubkey; | ||
} | ||
/** | ||
* Gets the receivers pubkey | ||
* @throws | ||
*/ | ||
export function getZapRecipient(zap) { | ||
@@ -23,2 +28,3 @@ const recipient = getTagValue(zap, "p"); | ||
} | ||
/** Returns the parsed bolt11 invoice */ | ||
export function getZapPayment(zap) { | ||
@@ -30,17 +36,24 @@ return getOrComputeCachedValue(zap, ZapInvoiceSymbol, () => { | ||
} | ||
/** Gets the AddressPointer that was zapped */ | ||
export function getZapAddressPointer(zap) { | ||
return getOrComputeCachedValue(zap, ZapAddressPointerSymbol, () => { | ||
const a = zap.tags.find(isATag); | ||
return a ? getAddressPointerFromTag(a) : null; | ||
return a ? getAddressPointerFromATag(a) : null; | ||
}); | ||
} | ||
/** Gets the EventPointer that was zapped */ | ||
export function getZapEventPointer(zap) { | ||
return getOrComputeCachedValue(zap, ZapEventPointerSymbol, () => { | ||
const e = zap.tags.find(isETag); | ||
return e ? getEventPointerFromTag(e) : null; | ||
return e ? getEventPointerFromETag(e) : null; | ||
}); | ||
} | ||
/** Gets the preimage for the bolt11 invoice */ | ||
export function getZapPreimage(zap) { | ||
return getTagValue(zap, "preimage"); | ||
} | ||
/** | ||
* Returns the zap request event inside the zap receipt | ||
* @throws | ||
*/ | ||
export function getZapRequest(zap) { | ||
@@ -57,2 +70,6 @@ return getOrComputeCachedValue(zap, ZapRequestSymbol, () => { | ||
} | ||
/** | ||
* Checks if the zap is valid | ||
* DOES NOT validate LNURL address | ||
*/ | ||
export function isValidZap(zap) { | ||
@@ -72,1 +89,13 @@ if (!zap) | ||
} | ||
/** Returns the zap splits for an event */ | ||
export function getZapSplits(event) { | ||
const tags = event.tags.filter((t) => t[0] === "zap" && t[1] && t[3]); | ||
if (tags.length > 0) { | ||
const targets = tags | ||
.map((t) => ({ pubkey: t[1], relay: t[2], weight: parseFloat(t[3]) })) | ||
.filter((p) => Number.isFinite(p.weight)); | ||
const total = targets.reduce((v, p) => v + p.weight, 0); | ||
return targets.map((p) => ({ ...p, percent: p.weight / total })); | ||
} | ||
return undefined; | ||
} |
@@ -1,2 +0,2 @@ | ||
export * from "./getValue.js"; | ||
export * from "./get-value.js"; | ||
export * from "./share-latest-value.js"; |
@@ -1,2 +0,2 @@ | ||
export * from "./getValue.js"; | ||
export * from "./get-value.js"; | ||
export * from "./share-latest-value.js"; |
@@ -5,2 +5,3 @@ export type Deferred<T> = Promise<T> & { | ||
}; | ||
/** Creates a controlled promise */ | ||
export declare function createDefer<T>(): Deferred<T>; |
@@ -0,1 +1,2 @@ | ||
/** Creates a controlled promise */ | ||
export function createDefer() { | ||
@@ -2,0 +3,0 @@ let _resolve; |
@@ -1,6 +0,7 @@ | ||
export * from "./simple.js"; | ||
export * from "./comments.js"; | ||
export * from "./mailboxes.js"; | ||
export * from "./profile.js"; | ||
export * from "./mailboxes.js"; | ||
export * from "./reactions.js"; | ||
export * from "./simple.js"; | ||
export * from "./thread.js"; | ||
export * from "./zaps.js"; |
@@ -1,6 +0,7 @@ | ||
export * from "./simple.js"; | ||
export * from "./comments.js"; | ||
export * from "./mailboxes.js"; | ||
export * from "./profile.js"; | ||
export * from "./mailboxes.js"; | ||
export * from "./reactions.js"; | ||
export * from "./simple.js"; | ||
export * from "./thread.js"; | ||
export * from "./zaps.js"; |
import { Query } from "../query-store/index.js"; | ||
/** A query that gets and parses the inbox and outbox relays for a pubkey */ | ||
export declare function MailboxesQuery(pubkey: string): Query<{ | ||
@@ -3,0 +4,0 @@ inboxes: string[]; |
import { kinds } from "nostr-tools"; | ||
import { map } from "rxjs/operators"; | ||
import { getInboxes, getOutboxes } from "../helpers/mailboxes.js"; | ||
/** A query that gets and parses the inbox and outbox relays for a pubkey */ | ||
export function MailboxesQuery(pubkey) { | ||
@@ -5,0 +6,0 @@ return { |
import { ProfileContent } from "../helpers/profile.js"; | ||
import { Query } from "../query-store/index.js"; | ||
/** A query that gets and parses the kind 0 metadata for a pubkey */ | ||
export declare function ProfileQuery(pubkey: string): Query<ProfileContent | undefined>; |
import { kinds } from "nostr-tools"; | ||
import { filter, map } from "rxjs/operators"; | ||
import { getProfileContent, isValidProfile } from "../helpers/profile.js"; | ||
/** A query that gets and parses the kind 0 metadata for a pubkey */ | ||
export function ProfileQuery(pubkey) { | ||
@@ -5,0 +6,0 @@ return { |
import { NostrEvent } from "nostr-tools"; | ||
import { Query } from "../query-store/index.js"; | ||
/** Creates a query that returns all reactions to an event (supports replaceable events) */ | ||
/** A query that returns all reactions to an event (supports replaceable events) */ | ||
export declare function ReactionsQuery(event: NostrEvent): Query<NostrEvent[]>; |
import { kinds } from "nostr-tools"; | ||
import { getEventUID, isReplaceable } from "../helpers/event.js"; | ||
/** Creates a query that returns all reactions to an event (supports replaceable events) */ | ||
/** A query that returns all reactions to an event (supports replaceable events) */ | ||
export function ReactionsQuery(event) { | ||
@@ -5,0 +5,0 @@ return { |
import { Filter, NostrEvent } from "nostr-tools"; | ||
import { Query } from "../query-store/index.js"; | ||
/** Creates a Query that returns a single event or undefined */ | ||
export declare function SingleEventQuery(uid: string): Query<NostrEvent | undefined>; | ||
export declare function SingleEventQuery(id: string): Query<NostrEvent | undefined>; | ||
/** Creates a Query that returns a multiple events in a map */ | ||
export declare function MultipleEventsQuery(uids: string[]): Query<Map<string, NostrEvent>>; | ||
export declare function MultipleEventsQuery(ids: string[]): Query<Map<string, NostrEvent>>; | ||
/** Creates a Query returning the latest version of a replaceable event */ | ||
export declare function ReplaceableQuery(kind: number, pubkey: string, d?: string): Query<NostrEvent | undefined>; | ||
/** Creates a Query that returns an array of sorted events matching the filters */ | ||
export declare function TimelineQuery(filters: Filter | Filter[]): Query<NostrEvent[]>; | ||
export declare function TimelineQuery(filters: Filter | Filter[], keepOldVersions?: boolean): Query<NostrEvent[]>; | ||
/** Creates a Query that returns a directory of events by their UID */ | ||
@@ -12,0 +12,0 @@ export declare function ReplaceableSetQuery(pointers: { |
@@ -1,15 +0,15 @@ | ||
import stringify from "json-stringify-deterministic"; | ||
import hash_sum from "hash-sum"; | ||
import { getReplaceableUID } from "../helpers/event.js"; | ||
/** Creates a Query that returns a single event or undefined */ | ||
export function SingleEventQuery(uid) { | ||
export function SingleEventQuery(id) { | ||
return { | ||
key: uid, | ||
run: (events) => events.event(uid), | ||
key: id, | ||
run: (events) => events.event(id), | ||
}; | ||
} | ||
/** Creates a Query that returns a multiple events in a map */ | ||
export function MultipleEventsQuery(uids) { | ||
export function MultipleEventsQuery(ids) { | ||
return { | ||
key: uids.join(","), | ||
run: (events) => events.events(uids), | ||
key: ids.join(","), | ||
run: (events) => events.events(ids), | ||
}; | ||
@@ -25,6 +25,7 @@ } | ||
/** Creates a Query that returns an array of sorted events matching the filters */ | ||
export function TimelineQuery(filters) { | ||
export function TimelineQuery(filters, keepOldVersions) { | ||
filters = Array.isArray(filters) ? filters : [filters]; | ||
return { | ||
key: stringify(filters), | ||
run: (events) => events.timeline(Array.isArray(filters) ? filters : [filters]), | ||
key: hash_sum(filters) + (keepOldVersions ? "-history" : ""), | ||
run: (events) => events.timeline(filters, keepOldVersions), | ||
}; | ||
@@ -34,7 +35,6 @@ } | ||
export function ReplaceableSetQuery(pointers) { | ||
const cords = pointers.map((pointer) => getReplaceableUID(pointer.kind, pointer.pubkey, pointer.identifier)); | ||
return { | ||
key: stringify(pointers), | ||
run: (events) => events.events(cords), | ||
key: hash_sum(pointers), | ||
run: (events) => events.replaceableSet(pointers), | ||
}; | ||
} |
@@ -24,1 +24,3 @@ import { NostrEvent } from "nostr-tools"; | ||
export declare function ThreadQuery(root: string | AddressPointer | EventPointer, opts?: ThreadQueryOptions): Query<Thread>; | ||
/** A query that gets all legacy and NIP-10, and NIP-22 replies for an event */ | ||
export declare function RepliesQuery(event: NostrEvent, overrideKinds?: number[]): Query<NostrEvent[]>; |
import { kinds } from "nostr-tools"; | ||
import { isParameterizedReplaceableKind } from "nostr-tools/kinds"; | ||
import { map } from "rxjs/operators"; | ||
import { getNip10References } from "../helpers/threading.js"; | ||
import { getCoordinateFromAddressPointer, isAddressPointer } from "../helpers/pointers.js"; | ||
import { getEventUID } from "../helpers/event.js"; | ||
import { getNip10References, interpretThreadTags } from "../helpers/threading.js"; | ||
import { getCoordinateFromAddressPointer, isAddressPointer, isEventPointer } from "../helpers/pointers.js"; | ||
import { getEventUID, getReplaceableUID, getTagValue, isEvent } from "../helpers/event.js"; | ||
import { COMMENT_KIND } from "../helpers/comment.js"; | ||
const defaultOptions = { | ||
@@ -67,1 +69,25 @@ kinds: [kinds.ShortTextNote], | ||
} | ||
/** A query that gets all legacy and NIP-10, and NIP-22 replies for an event */ | ||
export function RepliesQuery(event, overrideKinds) { | ||
return { | ||
key: getEventUID(event), | ||
run: (events) => { | ||
const kinds = overrideKinds || event.kind === 1 ? [1, COMMENT_KIND] : [COMMENT_KIND]; | ||
const filter = { kinds }; | ||
if (isEvent(parent) || isEventPointer(event)) | ||
filter["#e"] = [event.id]; | ||
const address = isParameterizedReplaceableKind(event.kind) | ||
? getReplaceableUID(event.kind, event.pubkey, getTagValue(event, "d")) | ||
: undefined; | ||
if (address) { | ||
filter["#a"] = [address]; | ||
} | ||
return events.timeline(filter).pipe(map((events) => { | ||
return events.filter((e) => { | ||
const refs = interpretThreadTags(e.tags); | ||
return refs.reply?.e?.[1] === event.id || refs.reply?.a?.[1] === address; | ||
}); | ||
})); | ||
}, | ||
}; | ||
} |
import { AddressPointer, EventPointer } from "nostr-tools/nip19"; | ||
import { NostrEvent } from "nostr-tools"; | ||
import { Query } from "../query-store/index.js"; | ||
/** A query that gets all zap events for an event */ | ||
export declare function EventZapsQuery(id: string | EventPointer | AddressPointer): Query<NostrEvent[]>; |
@@ -5,2 +5,3 @@ import { map } from "rxjs"; | ||
import { isValidZap } from "../helpers/zap.js"; | ||
/** A query that gets all zap events for an event */ | ||
export function EventZapsQuery(id) { | ||
@@ -7,0 +8,0 @@ return { |
@@ -8,3 +8,11 @@ import { BehaviorSubject, Observable } from "rxjs"; | ||
export type Query<T extends unknown> = { | ||
/** | ||
* A unique key for this query. this is used to detect duplicate queries | ||
*/ | ||
key: string; | ||
/** The args array this query was created with. This is mostly for debugging */ | ||
args?: Array<any>; | ||
/** | ||
* The meat of the query, this should return an Observables that subscribes to the eventStore in some way | ||
*/ | ||
run: (events: EventStore, store: QueryStore) => Observable<T>; | ||
@@ -17,15 +25,16 @@ }; | ||
constructor(store: EventStore); | ||
queries: LRU<Observable<any> | BehaviorSubject<any>>; | ||
queries: LRU<Query<any>>; | ||
observables: WeakMap<Query<any>, Observable<any> | BehaviorSubject<any>>; | ||
/** Creates a cached query */ | ||
runQuery<T extends unknown, Args extends Array<any>>(queryConstructor: (...args: Args) => { | ||
createQuery<T extends unknown, Args extends Array<any>>(queryConstructor: (...args: Args) => { | ||
key: string; | ||
run: (events: EventStore, store: QueryStore) => Observable<T>; | ||
}): (...args: Args) => Observable<T>; | ||
/** Returns a single event */ | ||
}, ...args: Args): Observable<T>; | ||
/** Creates a SingleEventQuery */ | ||
event(id: string): Observable<import("nostr-tools").Event | undefined>; | ||
/** Returns a single event */ | ||
/** Creates a MultipleEventsQuery */ | ||
events(ids: string[]): Observable<Map<string, import("nostr-tools").Event>>; | ||
/** Returns the latest version of a replaceable event */ | ||
/** Creates a ReplaceableQuery */ | ||
replaceable(kind: number, pubkey: string, d?: string): Observable<import("nostr-tools").Event | undefined>; | ||
/** Returns a directory of events by their UID */ | ||
/** Creates a ReplaceableSetQuery */ | ||
replaceableSet(pointers: { | ||
@@ -36,9 +45,9 @@ kind: number; | ||
}[]): Observable<Map<string, import("nostr-tools").Event>>; | ||
/** Returns an array of events that match the filter */ | ||
timeline(filters: Filter | Filter[]): Observable<import("nostr-tools").Event[]>; | ||
/** Returns the parsed profile (0) for a pubkey */ | ||
/** Creates a TimelineQuery */ | ||
timeline(filters: Filter | Filter[], keepOldVersions?: boolean): Observable<import("nostr-tools").Event[]>; | ||
/** Creates a ProfileQuery */ | ||
profile(pubkey: string): Observable<import("../helpers/profile.js").ProfileContent | undefined>; | ||
/** Returns all reactions for an event (supports replaceable events) */ | ||
/** Creates a ReactionsQuery */ | ||
reactions(event: NostrEvent): Observable<import("nostr-tools").Event[]>; | ||
/** Returns the parsed relay list (10002) for the pubkey */ | ||
/** Creates a MailboxesQuery */ | ||
mailboxes(pubkey: string): Observable<{ | ||
@@ -48,4 +57,5 @@ inboxes: string[]; | ||
} | undefined>; | ||
/** Creates a ThreadQuery */ | ||
thread(root: string | EventPointer | AddressPointer): Observable<Queries.Thread>; | ||
} | ||
export { Queries }; |
@@ -11,51 +11,57 @@ import { LRU } from "../helpers/lru.js"; | ||
queries = new LRU(); | ||
observables = new WeakMap(); | ||
/** Creates a cached query */ | ||
runQuery(queryConstructor) { | ||
return (...args) => { | ||
const query = queryConstructor(...args); | ||
const key = `${queryConstructor.name}|${query.key}`; | ||
if (!this.queries.has(key)) { | ||
const observable = query.run(this.store, this).pipe(shareLatestValue()); | ||
this.queries.set(key, observable); | ||
return observable; | ||
} | ||
return this.queries.get(key); | ||
}; | ||
createQuery(queryConstructor, ...args) { | ||
const tempQuery = queryConstructor(...args); | ||
const key = `${queryConstructor.name}|${tempQuery.key}`; | ||
let query = this.queries.get(key); | ||
if (!query) { | ||
query = tempQuery; | ||
this.queries.set(key, tempQuery); | ||
} | ||
if (!this.observables.has(query)) { | ||
query.args = args; | ||
const observable = query.run(this.store, this).pipe(shareLatestValue()); | ||
this.observables.set(query, observable); | ||
return observable; | ||
} | ||
return this.observables.get(query); | ||
} | ||
/** Returns a single event */ | ||
/** Creates a SingleEventQuery */ | ||
event(id) { | ||
return this.runQuery(Queries.SingleEventQuery)(id); | ||
return this.createQuery(Queries.SingleEventQuery, id); | ||
} | ||
/** Returns a single event */ | ||
/** Creates a MultipleEventsQuery */ | ||
events(ids) { | ||
return this.runQuery(Queries.MultipleEventsQuery)(ids); | ||
return this.createQuery(Queries.MultipleEventsQuery, ids); | ||
} | ||
/** Returns the latest version of a replaceable event */ | ||
/** Creates a ReplaceableQuery */ | ||
replaceable(kind, pubkey, d) { | ||
return this.runQuery(Queries.ReplaceableQuery)(kind, pubkey, d); | ||
return this.createQuery(Queries.ReplaceableQuery, kind, pubkey, d); | ||
} | ||
/** Returns a directory of events by their UID */ | ||
/** Creates a ReplaceableSetQuery */ | ||
replaceableSet(pointers) { | ||
return this.runQuery(Queries.ReplaceableSetQuery)(pointers); | ||
return this.createQuery(Queries.ReplaceableSetQuery, pointers); | ||
} | ||
/** Returns an array of events that match the filter */ | ||
timeline(filters) { | ||
return this.runQuery(Queries.TimelineQuery)(filters); | ||
/** Creates a TimelineQuery */ | ||
timeline(filters, keepOldVersions) { | ||
return this.createQuery(Queries.TimelineQuery, filters, keepOldVersions); | ||
} | ||
/** Returns the parsed profile (0) for a pubkey */ | ||
/** Creates a ProfileQuery */ | ||
profile(pubkey) { | ||
return this.runQuery(Queries.ProfileQuery)(pubkey); | ||
return this.createQuery(Queries.ProfileQuery, pubkey); | ||
} | ||
/** Returns all reactions for an event (supports replaceable events) */ | ||
/** Creates a ReactionsQuery */ | ||
reactions(event) { | ||
return this.runQuery(Queries.ReactionsQuery)(event); | ||
return this.createQuery(Queries.ReactionsQuery, event); | ||
} | ||
/** Returns the parsed relay list (10002) for the pubkey */ | ||
/** Creates a MailboxesQuery */ | ||
mailboxes(pubkey) { | ||
return this.runQuery(Queries.MailboxesQuery)(pubkey); | ||
return this.createQuery(Queries.MailboxesQuery, pubkey); | ||
} | ||
/** Creates a ThreadQuery */ | ||
thread(root) { | ||
return this.runQuery(Queries.ThreadQuery)(root); | ||
return this.createQuery(Queries.ThreadQuery, root); | ||
} | ||
} | ||
export { Queries }; |
{ | ||
"name": "applesauce-core", | ||
"version": "0.9.0", | ||
"version": "0.10.0", | ||
"description": "", | ||
@@ -55,25 +55,17 @@ "type": "module", | ||
"dependencies": { | ||
"@scure/base": "^1.1.9", | ||
"debug": "^4.3.7", | ||
"json-stringify-deterministic": "^1.0.12", | ||
"fast-deep-equal": "^3.1.3", | ||
"hash-sum": "^2.0.0", | ||
"light-bolt11-decoder": "^3.2.0", | ||
"nanoid": "^5.0.7", | ||
"nostr-tools": "^2.10.1", | ||
"nostr-tools": "^2.10.3", | ||
"rxjs": "^7.8.1" | ||
}, | ||
"devDependencies": { | ||
"@jest/globals": "^29.7.0", | ||
"@types/debug": "^4.1.12", | ||
"@types/jest": "^29.5.13", | ||
"jest": "^29.7.0", | ||
"jest-extended": "^4.0.2", | ||
"typescript": "^5.6.3" | ||
"@types/hash-sum": "^1.0.2", | ||
"typescript": "^5.6.3", | ||
"vitest": "^2.1.8" | ||
}, | ||
"jest": { | ||
"roots": [ | ||
"dist" | ||
], | ||
"setupFilesAfterEnv": [ | ||
"jest-extended/all" | ||
] | ||
}, | ||
"funding": { | ||
@@ -86,5 +78,5 @@ "type": "lightning", | ||
"watch:build": "tsc --watch > /dev/null", | ||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", | ||
"watch:test": "(trap 'kill 0' SIGINT; pnpm run build -w > /dev/null & pnpm run test --watch)" | ||
"test": "vitest run --passWithNoTests", | ||
"watch:test": "vitest" | ||
} | ||
} |
# applesauce-core | ||
AppleSauce Core is an interpretation layer for nostr clients, Push events into the in-memory [database](https://hzrd149.github.io/applesauce/classes/Database.html) and get nicely formatted data out with [queries](https://hzrd149.github.io/applesauce/modules/Queries) | ||
AppleSauce Core is an interpretation layer for nostr clients, Push events into the in-memory [database](https://hzrd149.github.io/applesauce/typedoc/classes/Database.html) and get nicely formatted data out with [queries](https://hzrd149.github.io/applesauce/typedoc/modules/Queries) | ||
@@ -5,0 +5,0 @@ # Example |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
129443
4
101
3303
8
1
+ Added@scure/base@^1.1.9
+ Addedfast-deep-equal@^3.1.3
+ Addedhash-sum@^2.0.0
+ Added@scure/base@1.1.91.2.4(transitive)
+ Addedfast-deep-equal@3.1.3(transitive)
+ Addedhash-sum@2.0.0(transitive)
+ Addedrxjs@7.8.2(transitive)
- Removedjson-stringify-deterministic@^1.0.12
- Removedjson-stringify-deterministic@1.0.12(transitive)
- Removedrxjs@7.8.1(transitive)
Updatednostr-tools@^2.10.3