mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-22 22:44:08 +02:00
rebuild event requester
This commit is contained in:
parent
980c68a42a
commit
5c061cad48
5
.changeset/soft-walls-wink.md
Normal file
5
.changeset/soft-walls-wink.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add expiration to cached metadata events
|
5
.changeset/tender-lions-sort.md
Normal file
5
.changeset/tender-lions-sort.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Rebuild underlying event requester classes
|
@ -1,42 +0,0 @@
|
|||||||
import { NostrEvent } from "../types/nostr-event";
|
|
||||||
import { PubkeyEventRequester } from "./pubkey-event-requester";
|
|
||||||
|
|
||||||
export class CachedPubkeyEventRequester extends PubkeyEventRequester {
|
|
||||||
private readCacheDedupe = new Map<string, Promise<NostrEvent | undefined>>();
|
|
||||||
async readCache(pubkey: string): Promise<NostrEvent | undefined> {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
async writeCache(pubkey: string, event: NostrEvent): Promise<any> {}
|
|
||||||
|
|
||||||
handleEvent(event: NostrEvent) {
|
|
||||||
const sub = this.getSubject(event.pubkey);
|
|
||||||
if (!sub.value || event.created_at > sub.value.created_at) {
|
|
||||||
this.writeCache(event.pubkey, event);
|
|
||||||
}
|
|
||||||
super.handleEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
requestEvent(pubkey: string, relays: string[], alwaysRequest = false) {
|
|
||||||
const sub = this.getSubject(pubkey);
|
|
||||||
|
|
||||||
if (!sub.value) {
|
|
||||||
// only call this.readCache once per pubkey
|
|
||||||
if (!this.readCacheDedupe.has(pubkey)) {
|
|
||||||
const promise = this.readCacheDedupe.get(pubkey) || this.readCache(pubkey);
|
|
||||||
this.readCacheDedupe.set(pubkey, promise);
|
|
||||||
|
|
||||||
promise.then((cached) => {
|
|
||||||
this.readCacheDedupe.delete(pubkey);
|
|
||||||
|
|
||||||
if (cached) this.handleEvent(cached);
|
|
||||||
|
|
||||||
if (!sub.value || alwaysRequest) super.requestEvent(pubkey, relays);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (alwaysRequest) {
|
|
||||||
super.requestEvent(pubkey, relays);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sub;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,172 +0,0 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import debug, { Debugger } from "debug";
|
|
||||||
import { NostrSubscription } from "./nostr-subscription";
|
|
||||||
import { SuperMap } from "./super-map";
|
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
|
||||||
import Subject from "./subject";
|
|
||||||
import { NostrQuery } from "../types/nostr-query";
|
|
||||||
import { nameOrPubkey } from "../helpers/debug";
|
|
||||||
|
|
||||||
type pubkey = string;
|
|
||||||
type relay = string;
|
|
||||||
|
|
||||||
class PubkeyEventRequestSubscription {
|
|
||||||
private subscription: NostrSubscription;
|
|
||||||
private kind: number;
|
|
||||||
private dTag?: string;
|
|
||||||
|
|
||||||
private subjects = new SuperMap<pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
|
|
||||||
|
|
||||||
private requestNext = new Set<pubkey>();
|
|
||||||
|
|
||||||
private requestedPubkeys = new Map<pubkey, Date>();
|
|
||||||
|
|
||||||
log: Debugger;
|
|
||||||
|
|
||||||
constructor(relay: string, kind: number, name?: string, dTag?: string, log?: Debugger) {
|
|
||||||
this.kind = kind;
|
|
||||||
this.dTag = dTag;
|
|
||||||
this.subscription = new NostrSubscription(relay, undefined, name);
|
|
||||||
|
|
||||||
this.subscription.onEvent.subscribe(this.handleEvent.bind(this));
|
|
||||||
this.subscription.onEOSE.subscribe(this.handleEOSE.bind(this));
|
|
||||||
|
|
||||||
this.log = log || debug("misc");
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleEvent(event: NostrEvent) {
|
|
||||||
// reject the event if its the wrong kind
|
|
||||||
if (event.kind !== this.kind) return;
|
|
||||||
// reject the event if has the wrong d tag or is missing one
|
|
||||||
if (this.dTag && !event.tags.some((t) => t[0] === "d" && t[1] === this.dTag)) return;
|
|
||||||
|
|
||||||
// remove the pubkey from the waiting list
|
|
||||||
this.requestedPubkeys.delete(event.pubkey);
|
|
||||||
|
|
||||||
const sub = this.subjects.get(event.pubkey);
|
|
||||||
|
|
||||||
const current = sub.value;
|
|
||||||
if (!current || event.created_at > current.created_at) {
|
|
||||||
this.log(`Found newer event for ${nameOrPubkey(event.pubkey)}`);
|
|
||||||
sub.next(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private handleEOSE() {
|
|
||||||
// relays says it has nothing left
|
|
||||||
this.requestedPubkeys.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubject(pubkey: string) {
|
|
||||||
return this.subjects.get(pubkey);
|
|
||||||
}
|
|
||||||
|
|
||||||
requestEvent(pubkey: string) {
|
|
||||||
const sub = this.subjects.get(pubkey);
|
|
||||||
|
|
||||||
if (!sub.value) {
|
|
||||||
this.log(`Adding ${nameOrPubkey(pubkey)} to queue`);
|
|
||||||
this.requestNext.add(pubkey);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sub;
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
let needsUpdate = false;
|
|
||||||
for (const pubkey of this.requestNext) {
|
|
||||||
if (!this.requestedPubkeys.has(pubkey)) {
|
|
||||||
this.requestedPubkeys.set(pubkey, new Date());
|
|
||||||
needsUpdate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.requestNext.clear();
|
|
||||||
|
|
||||||
// prune pubkeys
|
|
||||||
const timeout = dayjs().subtract(1, "minute");
|
|
||||||
for (const [pubkey, date] of this.requestedPubkeys) {
|
|
||||||
if (dayjs(date).isBefore(timeout)) {
|
|
||||||
this.requestedPubkeys.delete(pubkey);
|
|
||||||
needsUpdate = true;
|
|
||||||
this.log(`Request for ${nameOrPubkey(pubkey)} expired`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the subscription
|
|
||||||
if (needsUpdate) {
|
|
||||||
if (this.requestedPubkeys.size > 0) {
|
|
||||||
const query: NostrQuery = { authors: Array.from(this.requestedPubkeys.keys()), kinds: [this.kind] };
|
|
||||||
if (this.dTag) query["#d"] = [this.dTag];
|
|
||||||
|
|
||||||
this.log(`Updating query with ${query.authors?.length} pubkeys`);
|
|
||||||
this.subscription.setQuery(query);
|
|
||||||
|
|
||||||
if (this.subscription.state !== NostrSubscription.OPEN) {
|
|
||||||
this.subscription.open();
|
|
||||||
}
|
|
||||||
} else if (this.subscription.state === NostrSubscription.OPEN) {
|
|
||||||
this.subscription.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PubkeyEventRequester {
|
|
||||||
private kind: number;
|
|
||||||
private name?: string;
|
|
||||||
private dTag?: string;
|
|
||||||
private subjects = new SuperMap<pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
|
|
||||||
|
|
||||||
private subscriptions = new SuperMap<relay, PubkeyEventRequestSubscription>(
|
|
||||||
(relay) => new PubkeyEventRequestSubscription(relay, this.kind, this.name, this.dTag, this.log.extend(relay))
|
|
||||||
);
|
|
||||||
|
|
||||||
log: Debugger;
|
|
||||||
|
|
||||||
constructor(kind: number, name?: string, dTag?: string, log?: Debugger) {
|
|
||||||
this.kind = kind;
|
|
||||||
this.name = name;
|
|
||||||
this.dTag = dTag;
|
|
||||||
|
|
||||||
this.log = log || debug("misc");
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubject(pubkey: string) {
|
|
||||||
return this.subjects.get(pubkey);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEvent(event: NostrEvent) {
|
|
||||||
if (event.kind !== this.kind) return;
|
|
||||||
|
|
||||||
const sub = this.subjects.get(event.pubkey);
|
|
||||||
const current = sub.value;
|
|
||||||
if (!current || event.created_at > current.created_at) {
|
|
||||||
this.log(`New event for ${nameOrPubkey(event.pubkey)}`);
|
|
||||||
sub.next(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestEvent(pubkey: string, relays: string[]) {
|
|
||||||
this.log(`Requesting event for ${nameOrPubkey(pubkey)}`);
|
|
||||||
const sub = this.subjects.get(pubkey);
|
|
||||||
|
|
||||||
for (const relay of relays) {
|
|
||||||
const relaySub = this.subscriptions.get(relay).requestEvent(pubkey);
|
|
||||||
|
|
||||||
sub.connectWithHandler(relaySub, (event, next, current) => {
|
|
||||||
if (event.kind !== this.kind) return;
|
|
||||||
if (!current || event.created_at > current.created_at) {
|
|
||||||
this.log(`Event for ${nameOrPubkey(event.pubkey)} from connection`);
|
|
||||||
next(event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return sub;
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
for (const [relay, subscription] of this.subscriptions) {
|
|
||||||
subscription.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,14 +6,15 @@ import { useUserMetadata } from "../../hooks/use-user-metadata";
|
|||||||
import RawValue from "./raw-value";
|
import RawValue from "./raw-value";
|
||||||
import RawJson from "./raw-json";
|
import RawJson from "./raw-json";
|
||||||
import { useSharableProfileId } from "../../hooks/use-shareable-profile-id";
|
import { useSharableProfileId } from "../../hooks/use-shareable-profile-id";
|
||||||
import userRelaysService from "../../services/user-relays";
|
|
||||||
import useUserLNURLMetadata from "../../hooks/use-user-lnurl-metadata";
|
import useUserLNURLMetadata from "../../hooks/use-user-lnurl-metadata";
|
||||||
|
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
|
||||||
|
import { Kind } from "nostr-tools";
|
||||||
|
|
||||||
export default function UserDebugModal({ pubkey, ...props }: { pubkey: string } & Omit<ModalProps, "children">) {
|
export default function UserDebugModal({ pubkey, ...props }: { pubkey: string } & Omit<ModalProps, "children">) {
|
||||||
const npub = useMemo(() => normalizeToBech32(pubkey, Bech32Prefix.Pubkey), [pubkey]);
|
const npub = useMemo(() => normalizeToBech32(pubkey, Bech32Prefix.Pubkey), [pubkey]);
|
||||||
const metadata = useUserMetadata(pubkey);
|
const metadata = useUserMetadata(pubkey);
|
||||||
const nprofile = useSharableProfileId(pubkey);
|
const nprofile = useSharableProfileId(pubkey);
|
||||||
const relays = userRelaysService.requester.getSubject(pubkey).value;
|
const relays = replaceableEventLoaderService.getEvent(Kind.RelayList, pubkey).value;
|
||||||
const tipMetadata = useUserLNURLMetadata(pubkey);
|
const tipMetadata = useUserLNURLMetadata(pubkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -15,7 +15,15 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<ReloadPrompt mb="2" />
|
<ReloadPrompt mb="2" />
|
||||||
<Container size="lg" display="flex" padding="0" gap="4" alignItems="flex-start">
|
<Container size="lg" display="flex" padding="0" gap="4" alignItems="flex-start">
|
||||||
{!isMobile && <DesktopSideNav position="sticky" top="0" />}
|
{!isMobile && <DesktopSideNav position="sticky" top="0" />}
|
||||||
<Flex flexGrow={1} direction="column" w="full" overflowX="hidden" overflowY="visible" pb={isMobile ? "14" : 0}>
|
<Flex
|
||||||
|
flexGrow={1}
|
||||||
|
direction="column"
|
||||||
|
w="full"
|
||||||
|
overflowX="hidden"
|
||||||
|
overflowY="visible"
|
||||||
|
pb={isMobile ? "14" : 0}
|
||||||
|
minH="50vh"
|
||||||
|
>
|
||||||
<ErrorBoundary>{children}</ErrorBoundary>
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
</Flex>
|
</Flex>
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { openDB, deleteDB } from "idb";
|
import { openDB, deleteDB } from "idb";
|
||||||
|
|
||||||
import { IDBPDatabase } from "idb";
|
import { IDBPDatabase } from "idb";
|
||||||
import { SchemaV1, SchemaV2 } from "./schema";
|
import { SchemaV1, SchemaV2, SchemaV3 } from "./schema";
|
||||||
|
|
||||||
const dbName = "storage";
|
const dbName = "storage";
|
||||||
const version = 2;
|
const version = 3;
|
||||||
const db = await openDB<SchemaV2>(dbName, version, {
|
const db = await openDB<SchemaV3>(dbName, version, {
|
||||||
upgrade(db, oldVersion, newVersion, transaction, event) {
|
upgrade(db, oldVersion, newVersion, transaction, event) {
|
||||||
if (oldVersion < 1) {
|
if (oldVersion < 1) {
|
||||||
const v0 = db as unknown as IDBPDatabase<SchemaV1>;
|
const v0 = db as unknown as IDBPDatabase<SchemaV1>;
|
||||||
@ -56,14 +56,29 @@ const db = await openDB<SchemaV2>(dbName, version, {
|
|||||||
});
|
});
|
||||||
settings.createIndex("created_at", "created_at");
|
settings.createIndex("created_at", "created_at");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < 3) {
|
||||||
|
const v2 = db as unknown as IDBPDatabase<SchemaV2>;
|
||||||
|
const v3 = db as unknown as IDBPDatabase<SchemaV3>;
|
||||||
|
|
||||||
|
// rename the old event caches
|
||||||
|
v3.deleteObjectStore("userMetadata");
|
||||||
|
v3.deleteObjectStore("userContacts");
|
||||||
|
v3.deleteObjectStore("userRelays");
|
||||||
|
v3.deleteObjectStore("settings");
|
||||||
|
|
||||||
|
// create new replaceable event object store
|
||||||
|
const settings = v3.createObjectStore("replaceableEvents", {
|
||||||
|
keyPath: "addr",
|
||||||
|
});
|
||||||
|
settings.createIndex("created", "created");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function clearCacheData() {
|
export async function clearCacheData() {
|
||||||
await db.clear("userMetadata");
|
await db.clear("replaceableEvents");
|
||||||
await db.clear("userContacts");
|
|
||||||
await db.clear("userFollows");
|
await db.clear("userFollows");
|
||||||
await db.clear("userRelays");
|
|
||||||
await db.clear("relayInfo");
|
await db.clear("relayInfo");
|
||||||
await db.clear("dnsIdentifiers");
|
await db.clear("dnsIdentifiers");
|
||||||
await db.clear("relayScoreboardStats");
|
await db.clear("relayScoreboardStats");
|
||||||
|
@ -61,3 +61,19 @@ export interface SchemaV2 extends SchemaV1 {
|
|||||||
value: any;
|
value: any;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SchemaV3 {
|
||||||
|
replaceableEvents: {
|
||||||
|
key: string;
|
||||||
|
value: {
|
||||||
|
addr: string;
|
||||||
|
created: number;
|
||||||
|
event: NostrEvent;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
userFollows: SchemaV2["userFollows"];
|
||||||
|
dnsIdentifiers: SchemaV2["dnsIdentifiers"];
|
||||||
|
relayInfo: SchemaV2["relayInfo"];
|
||||||
|
relayScoreboardStats: SchemaV2["relayScoreboardStats"];
|
||||||
|
misc: SchemaV2["misc"];
|
||||||
|
}
|
||||||
|
254
src/services/replaceable-event-requester.ts
Normal file
254
src/services/replaceable-event-requester.ts
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import debug, { Debugger } from "debug";
|
||||||
|
import { NostrSubscription } from "../classes/nostr-subscription";
|
||||||
|
import { SuperMap } from "../classes/super-map";
|
||||||
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
|
import Subject from "../classes/subject";
|
||||||
|
import { NostrQuery } from "../types/nostr-query";
|
||||||
|
import { logger, nameOrPubkey } from "../helpers/debug";
|
||||||
|
import db from "./db";
|
||||||
|
|
||||||
|
type Pubkey = string;
|
||||||
|
type Relay = string;
|
||||||
|
|
||||||
|
export function getReadableAddr(kind: number, pubkey: string, d?: string) {
|
||||||
|
return `${kind}:${nameOrPubkey(pubkey)}${d ? ":" + d : ""}`;
|
||||||
|
}
|
||||||
|
export function getAddr(kind: number, pubkey: string, d?: string) {
|
||||||
|
return `${kind}:${pubkey}${d ? ":" + d : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReplaceableEventRelayLoader {
|
||||||
|
private subscription: NostrSubscription;
|
||||||
|
private events = new SuperMap<Pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
|
||||||
|
|
||||||
|
private requestNext = new Set<string>();
|
||||||
|
private requested = new Map<string, Date>();
|
||||||
|
|
||||||
|
log: Debugger;
|
||||||
|
|
||||||
|
constructor(relay: string, log?: Debugger) {
|
||||||
|
this.subscription = new NostrSubscription(relay, undefined, `replaceable-event-loader`);
|
||||||
|
|
||||||
|
this.subscription.onEvent.subscribe(this.handleEvent.bind(this));
|
||||||
|
this.subscription.onEOSE.subscribe(this.handleEOSE.bind(this));
|
||||||
|
|
||||||
|
this.log = log || debug("misc");
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleEvent(event: NostrEvent) {
|
||||||
|
const d = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
|
||||||
|
const addr = getAddr(event.kind, event.pubkey, d);
|
||||||
|
|
||||||
|
// remove the pubkey from the waiting list
|
||||||
|
this.requested.delete(addr);
|
||||||
|
|
||||||
|
const sub = this.events.get(addr);
|
||||||
|
|
||||||
|
const current = sub.value;
|
||||||
|
if (!current || event.created_at > current.created_at) {
|
||||||
|
sub.next(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private handleEOSE() {
|
||||||
|
// relays says it has nothing left
|
||||||
|
this.requested.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
getEvent(kind: number, pubkey: string, d?: string) {
|
||||||
|
return this.events.get(getAddr(kind, pubkey, d));
|
||||||
|
}
|
||||||
|
|
||||||
|
requestEvent(kind: number, pubkey: string, d?: string) {
|
||||||
|
const addr = getAddr(kind, pubkey, d);
|
||||||
|
const event = this.events.get(addr);
|
||||||
|
|
||||||
|
if (!event.value) {
|
||||||
|
this.requestNext.add(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
let needsUpdate = false;
|
||||||
|
for (const addr of this.requestNext) {
|
||||||
|
if (!this.requested.has(addr)) {
|
||||||
|
this.requested.set(addr, new Date());
|
||||||
|
needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.requestNext.clear();
|
||||||
|
|
||||||
|
// prune requests
|
||||||
|
const timeout = dayjs().subtract(1, "minute");
|
||||||
|
for (const [addr, date] of this.requested) {
|
||||||
|
if (dayjs(date).isBefore(timeout)) {
|
||||||
|
this.requested.delete(addr);
|
||||||
|
needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the subscription
|
||||||
|
if (needsUpdate) {
|
||||||
|
if (this.requested.size > 0) {
|
||||||
|
const filters: Record<number, NostrQuery> = {};
|
||||||
|
|
||||||
|
for (const [addr] of this.requested) {
|
||||||
|
const [kindStr, pubkey, d] = addr.split(":") as [string, string] | [string, string, string];
|
||||||
|
const kind = parseInt(kindStr);
|
||||||
|
filters[kind] = filters[kind] || { kinds: [kind] };
|
||||||
|
|
||||||
|
const arr = (filters[kind].authors = filters[kind].authors || []);
|
||||||
|
arr.push(pubkey);
|
||||||
|
|
||||||
|
if (d) {
|
||||||
|
const arr = (filters[kind]["#d"] = filters[kind]["#d"] || []);
|
||||||
|
arr.push(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = Array.from(Object.values(filters));
|
||||||
|
|
||||||
|
this.log(
|
||||||
|
`Updating query`,
|
||||||
|
Array.from(Object.keys(filters))
|
||||||
|
.map((kind: string) => `kind ${kind}: ${filters[parseInt(kind)].authors?.length}`)
|
||||||
|
.join(", ")
|
||||||
|
);
|
||||||
|
this.subscription.setQuery(query);
|
||||||
|
|
||||||
|
if (this.subscription.state !== NostrSubscription.OPEN) {
|
||||||
|
this.subscription.open();
|
||||||
|
}
|
||||||
|
} else if (this.subscription.state === NostrSubscription.OPEN) {
|
||||||
|
this.subscription.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReplaceableEventLoaderService {
|
||||||
|
private events = new SuperMap<Pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
|
||||||
|
|
||||||
|
private loaders = new SuperMap<Relay, ReplaceableEventRelayLoader>(
|
||||||
|
(relay) => new ReplaceableEventRelayLoader(relay, this.log.extend(relay))
|
||||||
|
);
|
||||||
|
|
||||||
|
log = logger.extend("ReplaceableEventLoader");
|
||||||
|
|
||||||
|
handleEvent(event: NostrEvent) {
|
||||||
|
const d = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
|
||||||
|
const addr = getAddr(event.kind, event.pubkey, d);
|
||||||
|
|
||||||
|
const sub = this.events.get(addr);
|
||||||
|
const current = sub.value;
|
||||||
|
if (!current || event.created_at > current.created_at) {
|
||||||
|
sub.next(event);
|
||||||
|
this.saveToCache(addr, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getEvent(kind: number, pubkey: string, d?: string) {
|
||||||
|
return this.events.get(getAddr(kind, pubkey, d));
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadCacheDedupe = new Map<string, Promise<boolean>>();
|
||||||
|
private loadFromCache(addr: string) {
|
||||||
|
const dedupe = this.loadCacheDedupe.get(addr);
|
||||||
|
if (dedupe) return dedupe;
|
||||||
|
|
||||||
|
const promise = db.get("replaceableEvents", addr).then((cached) => {
|
||||||
|
this.loadCacheDedupe.delete(addr);
|
||||||
|
if (cached?.event) {
|
||||||
|
this.handleEvent(cached.event);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loadCacheDedupe.set(addr, promise);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
private async saveToCache(addr: string, event: NostrEvent) {
|
||||||
|
await db.put("replaceableEvents", { addr, event, created: dayjs().unix() });
|
||||||
|
}
|
||||||
|
|
||||||
|
async pruneCache() {
|
||||||
|
const keys = await db.getAllKeysFromIndex(
|
||||||
|
"replaceableEvents",
|
||||||
|
"created",
|
||||||
|
IDBKeyRange.upperBound(dayjs().subtract(1, "day").unix())
|
||||||
|
);
|
||||||
|
|
||||||
|
this.log(`Pruning ${keys.length} events`);
|
||||||
|
|
||||||
|
const transaction = db.transaction("replaceableEvents", "readwrite");
|
||||||
|
for (const key of keys) {
|
||||||
|
transaction.store.delete(key);
|
||||||
|
}
|
||||||
|
await transaction.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private requestEventFromRelays(relays: string[], kind: number, pubkey: string, d?: string) {
|
||||||
|
const addr = getAddr(kind, pubkey, d);
|
||||||
|
const sub = this.events.get(addr);
|
||||||
|
|
||||||
|
for (const relay of relays) {
|
||||||
|
const request = this.loaders.get(relay).requestEvent(kind, pubkey, d);
|
||||||
|
|
||||||
|
sub.connectWithHandler(request, (event, next, current) => {
|
||||||
|
if (!current || event.created_at > current.created_at) {
|
||||||
|
next(event);
|
||||||
|
this.saveToCache(addr, event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestEvent(relays: string[], kind: number, pubkey: string, d?: string, alwaysRequest = false) {
|
||||||
|
const addr = getAddr(kind, pubkey, d);
|
||||||
|
const sub = this.events.get(addr);
|
||||||
|
|
||||||
|
if (!sub.value) {
|
||||||
|
this.loadFromCache(addr).then((loaded) => {
|
||||||
|
if (!loaded) {
|
||||||
|
this.requestEventFromRelays(relays, kind, pubkey, d);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alwaysRequest) {
|
||||||
|
this.requestEventFromRelays(relays, kind, pubkey, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
for (const [relay, loader] of this.loaders) {
|
||||||
|
loader.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const replaceableEventLoaderService = new ReplaceableEventLoaderService();
|
||||||
|
|
||||||
|
replaceableEventLoaderService.pruneCache();
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
replaceableEventLoaderService.update();
|
||||||
|
}, 1000 * 2);
|
||||||
|
setInterval(() => {
|
||||||
|
replaceableEventLoaderService.pruneCache();
|
||||||
|
}, 1000 * 60 * 60);
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
//@ts-ignore
|
||||||
|
window.replaceableEventLoaderService = replaceableEventLoaderService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default replaceableEventLoaderService;
|
@ -1,45 +1,30 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||||
import db from "../db";
|
|
||||||
import { logger } from "../../helpers/debug";
|
|
||||||
|
|
||||||
import { SuperMap } from "../../classes/super-map";
|
import { SuperMap } from "../../classes/super-map";
|
||||||
import { PersistentSubject } from "../../classes/subject";
|
import { PersistentSubject } from "../../classes/subject";
|
||||||
import { CachedPubkeyEventRequester } from "../../classes/cached-pubkey-event-requester";
|
|
||||||
import { AppSettings, defaultSettings, parseAppSettings } from "./migrations";
|
import { AppSettings, defaultSettings, parseAppSettings } from "./migrations";
|
||||||
|
import replaceableEventLoaderService from "../replaceable-event-requester";
|
||||||
|
|
||||||
const DTAG = "nostrudel-settings";
|
const DTAG = "nostrudel-settings";
|
||||||
|
|
||||||
class UserAppSettings {
|
class UserAppSettings {
|
||||||
requester: CachedPubkeyEventRequester;
|
|
||||||
log = logger.extend("UserAppSettings");
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.requester = new CachedPubkeyEventRequester(30078, "user-app-data", DTAG, this.log.extend("requester"));
|
|
||||||
this.requester.readCache = (pubkey) => db.get("settings", pubkey);
|
|
||||||
this.requester.writeCache = (pubkey, event) => db.put("settings", event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private parsedSubjects = new SuperMap<string, PersistentSubject<AppSettings>>(
|
private parsedSubjects = new SuperMap<string, PersistentSubject<AppSettings>>(
|
||||||
(pubkey) => new PersistentSubject<AppSettings>(defaultSettings)
|
() => new PersistentSubject<AppSettings>(defaultSettings)
|
||||||
);
|
);
|
||||||
getSubject(pubkey: string) {
|
getSubject(pubkey: string) {
|
||||||
return this.parsedSubjects.get(pubkey);
|
return this.parsedSubjects.get(pubkey);
|
||||||
}
|
}
|
||||||
requestAppSettings(pubkey: string, relays: string[], alwaysRequest = false) {
|
requestAppSettings(pubkey: string, relays: string[], alwaysRequest = false) {
|
||||||
const sub = this.parsedSubjects.get(pubkey);
|
const sub = this.parsedSubjects.get(pubkey);
|
||||||
const requestSub = this.requester.requestEvent(pubkey, relays, alwaysRequest);
|
const requestSub = replaceableEventLoaderService.requestEvent(relays, 30078, pubkey, DTAG, alwaysRequest);
|
||||||
sub.connectWithHandler(requestSub, (event, next) => next(parseAppSettings(event)));
|
sub.connectWithHandler(requestSub, (event, next) => next(parseAppSettings(event)));
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
receiveEvent(event: NostrEvent) {
|
receiveEvent(event: NostrEvent) {
|
||||||
this.requester.handleEvent(event);
|
replaceableEventLoaderService.handleEvent(event);
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
this.requester.update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildAppSettingsEvent(settings: AppSettings): DraftNostrEvent {
|
buildAppSettingsEvent(settings: AppSettings): DraftNostrEvent {
|
||||||
@ -54,10 +39,6 @@ class UserAppSettings {
|
|||||||
|
|
||||||
const userAppSettings = new UserAppSettings();
|
const userAppSettings = new UserAppSettings();
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
userAppSettings.update();
|
|
||||||
}, 1000 * 2);
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.userAppSettings = userAppSettings;
|
window.userAppSettings = userAppSettings;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { isPTag, NostrEvent } from "../types/nostr-event";
|
import { isPTag, NostrEvent } from "../types/nostr-event";
|
||||||
import { safeJson } from "../helpers/parse";
|
import { safeJson } from "../helpers/parse";
|
||||||
import db from "./db";
|
|
||||||
import { CachedPubkeyEventRequester } from "../classes/cached-pubkey-event-requester";
|
|
||||||
import { SuperMap } from "../classes/super-map";
|
import { SuperMap } from "../classes/super-map";
|
||||||
import Subject from "../classes/subject";
|
import Subject from "../classes/subject";
|
||||||
import { RelayConfig, RelayMode } from "../classes/relay";
|
import { RelayConfig, RelayMode } from "../classes/relay";
|
||||||
import { normalizeRelayConfigs } from "../helpers/relay";
|
import { normalizeRelayConfigs } from "../helpers/relay";
|
||||||
|
import replaceableEventLoaderService from "./replaceable-event-requester";
|
||||||
|
import { Kind } from "nostr-tools";
|
||||||
|
|
||||||
export type UserContacts = {
|
export type UserContacts = {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
@ -48,21 +48,6 @@ function parseContacts(event: NostrEvent): UserContacts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class UserContactsService {
|
class UserContactsService {
|
||||||
requester: CachedPubkeyEventRequester;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.requester = new CachedPubkeyEventRequester(3, "user-contacts");
|
|
||||||
this.requester.readCache = this.readCache;
|
|
||||||
this.requester.writeCache = this.writeCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
readCache(pubkey: string) {
|
|
||||||
return db.get("userContacts", pubkey);
|
|
||||||
}
|
|
||||||
writeCache(pubkey: string, event: NostrEvent) {
|
|
||||||
return db.put("userContacts", event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private subjects = new SuperMap<string, Subject<UserContacts>>(() => new Subject<UserContacts>());
|
private subjects = new SuperMap<string, Subject<UserContacts>>(() => new Subject<UserContacts>());
|
||||||
getSubject(pubkey: string) {
|
getSubject(pubkey: string) {
|
||||||
return this.subjects.get(pubkey);
|
return this.subjects.get(pubkey);
|
||||||
@ -70,7 +55,13 @@ class UserContactsService {
|
|||||||
requestContacts(pubkey: string, relays: string[], alwaysRequest = false) {
|
requestContacts(pubkey: string, relays: string[], alwaysRequest = false) {
|
||||||
const sub = this.subjects.get(pubkey);
|
const sub = this.subjects.get(pubkey);
|
||||||
|
|
||||||
const requestSub = this.requester.requestEvent(pubkey, relays, alwaysRequest);
|
const requestSub = replaceableEventLoaderService.requestEvent(
|
||||||
|
relays,
|
||||||
|
Kind.Contacts,
|
||||||
|
pubkey,
|
||||||
|
undefined,
|
||||||
|
alwaysRequest
|
||||||
|
);
|
||||||
|
|
||||||
sub.connectWithHandler(requestSub, (event, next) => next(parseContacts(event)));
|
sub.connectWithHandler(requestSub, (event, next) => next(parseContacts(event)));
|
||||||
|
|
||||||
@ -78,20 +69,12 @@ class UserContactsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
receiveEvent(event: NostrEvent) {
|
receiveEvent(event: NostrEvent) {
|
||||||
this.requester.handleEvent(event);
|
replaceableEventLoaderService.handleEvent(event);
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
this.requester.update();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userContactsService = new UserContactsService();
|
const userContactsService = new UserContactsService();
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
userContactsService.update();
|
|
||||||
}, 1000 * 2);
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.userContactsService = userContactsService;
|
window.userContactsService = userContactsService;
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import db from "./db";
|
import db from "./db";
|
||||||
import { CachedPubkeyEventRequester } from "../classes/cached-pubkey-event-requester";
|
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
import { Kind0ParsedContent, parseKind0Event } from "../helpers/user-metadata";
|
import { Kind0ParsedContent, parseKind0Event } from "../helpers/user-metadata";
|
||||||
import { SuperMap } from "../classes/super-map";
|
import { SuperMap } from "../classes/super-map";
|
||||||
import Subject from "../classes/subject";
|
import Subject from "../classes/subject";
|
||||||
|
import replaceableEventLoaderService from "./replaceable-event-requester";
|
||||||
|
import { Kind } from "nostr-tools";
|
||||||
|
|
||||||
class UserMetadataService {
|
class UserMetadataService {
|
||||||
requester: CachedPubkeyEventRequester;
|
// requester: CachedPubkeyEventRequester;
|
||||||
constructor() {
|
// constructor() {
|
||||||
this.requester = new CachedPubkeyEventRequester(0, "user-metadata");
|
// this.requester = new CachedPubkeyEventRequester(0, "user-metadata");
|
||||||
this.requester.readCache = this.readCache;
|
// this.requester.readCache = this.readCache;
|
||||||
this.requester.writeCache = this.writeCache;
|
// this.requester.writeCache = this.writeCache;
|
||||||
}
|
// }
|
||||||
|
|
||||||
readCache(pubkey: string) {
|
readCache(pubkey: string) {
|
||||||
return db.get("userMetadata", pubkey);
|
return db.get("userMetadata", pubkey);
|
||||||
@ -26,26 +27,24 @@ class UserMetadataService {
|
|||||||
}
|
}
|
||||||
requestMetadata(pubkey: string, relays: string[], alwaysRequest = false) {
|
requestMetadata(pubkey: string, relays: string[], alwaysRequest = false) {
|
||||||
const sub = this.parsedSubjects.get(pubkey);
|
const sub = this.parsedSubjects.get(pubkey);
|
||||||
const requestSub = this.requester.requestEvent(pubkey, relays, alwaysRequest);
|
const requestSub = replaceableEventLoaderService.requestEvent(
|
||||||
|
relays,
|
||||||
|
Kind.Metadata,
|
||||||
|
pubkey,
|
||||||
|
undefined,
|
||||||
|
alwaysRequest
|
||||||
|
);
|
||||||
sub.connectWithHandler(requestSub, (event, next) => next(parseKind0Event(event)));
|
sub.connectWithHandler(requestSub, (event, next) => next(parseKind0Event(event)));
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
receiveEvent(event: NostrEvent) {
|
receiveEvent(event: NostrEvent) {
|
||||||
this.requester.handleEvent(event);
|
replaceableEventLoaderService.handleEvent(event);
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
this.requester.update();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMetadataService = new UserMetadataService();
|
const userMetadataService = new UserMetadataService();
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
userMetadataService.update();
|
|
||||||
}, 1000 * 2);
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.userMetadataService = userMetadataService;
|
window.userMetadataService = userMetadataService;
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import db from "./db";
|
|
||||||
import { isRTag, NostrEvent } from "../types/nostr-event";
|
import { isRTag, NostrEvent } from "../types/nostr-event";
|
||||||
import { RelayConfig } from "../classes/relay";
|
import { RelayConfig } from "../classes/relay";
|
||||||
import { parseRTag } from "../helpers/nostr-event";
|
import { parseRTag } from "../helpers/nostr-event";
|
||||||
import { CachedPubkeyEventRequester } from "../classes/cached-pubkey-event-requester";
|
|
||||||
import { SuperMap } from "../classes/super-map";
|
import { SuperMap } from "../classes/super-map";
|
||||||
import Subject from "../classes/subject";
|
import Subject from "../classes/subject";
|
||||||
import { normalizeRelayConfigs } from "../helpers/relay";
|
import { normalizeRelayConfigs } from "../helpers/relay";
|
||||||
import userContactsService from "./user-contacts";
|
import userContactsService from "./user-contacts";
|
||||||
|
import replaceableEventLoaderService from "./replaceable-event-requester";
|
||||||
|
import { Kind } from "nostr-tools";
|
||||||
|
|
||||||
export type ParsedUserRelays = {
|
export type ParsedUserRelays = {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
@ -23,20 +23,13 @@ function parseRelaysEvent(event: NostrEvent): ParsedUserRelays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class UserRelaysService {
|
class UserRelaysService {
|
||||||
requester: CachedPubkeyEventRequester;
|
|
||||||
constructor() {
|
|
||||||
this.requester = new CachedPubkeyEventRequester(10002, "user-relays");
|
|
||||||
this.requester.readCache = (pubkey) => db.get("userRelays", pubkey);
|
|
||||||
this.requester.writeCache = (pubkey, event) => db.put("userRelays", event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private subjects = new SuperMap<string, Subject<ParsedUserRelays>>(() => new Subject<ParsedUserRelays>());
|
private subjects = new SuperMap<string, Subject<ParsedUserRelays>>(() => new Subject<ParsedUserRelays>());
|
||||||
getRelays(pubkey: string) {
|
getRelays(pubkey: string) {
|
||||||
return this.subjects.get(pubkey);
|
return this.subjects.get(pubkey);
|
||||||
}
|
}
|
||||||
requestRelays(pubkey: string, relays: string[], alwaysRequest = false) {
|
requestRelays(pubkey: string, relays: string[], alwaysRequest = false) {
|
||||||
const sub = this.subjects.get(pubkey);
|
const sub = this.subjects.get(pubkey);
|
||||||
const requestSub = this.requester.requestEvent(pubkey, relays, alwaysRequest);
|
const requestSub = replaceableEventLoaderService.requestEvent(relays, Kind.RelayList, pubkey);
|
||||||
sub.connectWithHandler(requestSub, (event, next) => next(parseRelaysEvent(event)));
|
sub.connectWithHandler(requestSub, (event, next) => next(parseRelaysEvent(event)));
|
||||||
|
|
||||||
// also fetch the relays from the users contacts
|
// also fetch the relays from the users contacts
|
||||||
@ -51,20 +44,12 @@ class UserRelaysService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
receiveEvent(event: NostrEvent) {
|
receiveEvent(event: NostrEvent) {
|
||||||
this.requester.handleEvent(event);
|
replaceableEventLoaderService.handleEvent(event);
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
this.requester.update();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userRelaysService = new UserRelaysService();
|
const userRelaysService = new UserRelaysService();
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
userRelaysService.update();
|
|
||||||
}, 1000 * 2);
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.userRelaysService = userRelaysService;
|
window.userRelaysService = userRelaysService;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user