Compare commits

...

5 Commits

Author SHA1 Message Date
aefe8a8210 LRUCache ParsedZaps, import from system dir in worker
Some checks are pending
continuous-integration/drone/push Build is running
2024-02-05 11:50:05 +02:00
a97e895cb8 fix rendering glitch 2024-02-05 11:14:05 +02:00
7ceab04cbc set event seen_at times, sort by seen_at in ForYouFeed 2024-02-05 11:06:46 +02:00
5bc3c10d36 move for you feed creation to worker 2024-02-05 11:06:46 +02:00
351a249a32 migrations file, add seen_at column to events 2024-02-05 11:06:46 +02:00
13 changed files with 261 additions and 190 deletions

View File

@ -1,11 +1,12 @@
import { EventKind, NostrLink } from "@snort/system";
import classNames from "classnames";
import React, { useCallback, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { useInView } from "react-intersection-observer";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { LRUCache } from "typescript-lru-cache";
import { Relay } from "@/Cache";
import NoteHeader from "@/Components/Event/Note/NoteHeader";
import { NoteText } from "@/Components/Event/Note/NoteText";
import { TranslationInfo } from "@/Components/Event/Note/TranslationInfo";
@ -38,6 +39,7 @@ export function Note(props: NoteProps) {
const baseClassName = classNames("note min-h-[110px] flex flex-col gap-4 card", className ?? "");
const { isEventMuted } = useModeration();
const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" });
const { ref: setSeenAtRef, inView: setSeenAtInView } = useInView({ rootMargin: "0px", threshold: 1 });
const [showTranslation, setShowTranslation] = useState(true);
const [translated, setTranslated] = useState<NoteTranslation>(translationCache.get(ev.id));
const cachedSetTranslated = useCallback(
@ -48,6 +50,16 @@ export function Note(props: NoteProps) {
[ev.id],
);
useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
if (setSeenAtInView) {
timeout = setTimeout(() => {
Relay.setEventMetadata(ev.id, { seen_at: Math.round(Date.now() / 1000) });
}, 2000);
}
return () => clearTimeout(timeout);
}, [setSeenAtInView]);
const optionsMerged = { ...defaultOptions, ...opt };
const goToEvent = useGoToEvent(props, optionsMerged);
@ -76,6 +88,7 @@ export function Note(props: NoteProps) {
</div>
)}
</div>
<div ref={setSeenAtRef} />
</>
);
}

View File

@ -1,16 +1,15 @@
import { EventKind, TaggedNostrEvent } from "@snort/system";
import { EventKind, NostrEvent } from "@snort/system";
import { memo, useEffect, useMemo, useState } from "react";
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
import { Relay } from "@/Cache";
import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector";
import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer";
import { TaskList } from "@/Components/Tasks/TaskList";
import { getForYouFeed } from "@/Db/getForYouFeed";
import useTimelineFeed, { TimelineFeedOptions, TimelineSubject } from "@/Feed/TimelineFeed";
import useLogin from "@/Hooks/useLogin";
import messages from "@/Pages/messages";
import { System } from "@/system";
const FollowsHint = () => {
const { publicKey: pubKey, follows } = useLogin();
@ -32,14 +31,14 @@ const FollowsHint = () => {
};
let forYouFeed = {
events: [] as TaggedNostrEvent[],
events: [] as NostrEvent[],
created_at: 0,
};
let getForYouFeedPromise: Promise<TaggedNostrEvent[]> | null = null;
let getForYouFeedPromise: Promise<NostrEvent[]> | null = null;
export const ForYouTab = memo(function ForYouTab() {
const [notes, setNotes] = useState<TaggedNostrEvent[]>(forYouFeed.events);
const [notes, setNotes] = useState<NostrEvent[]>(forYouFeed.events);
const { feedDisplayAs } = useLogin();
const displayAsInitial = feedDisplayAs ?? "list";
const [displayAs, setDisplayAs] = useState<DisplayAs>(displayAsInitial);
@ -64,7 +63,7 @@ export const ForYouTab = memo(function ForYouTab() {
const latestFeed = useTimelineFeed(subject, { method: "TIME_RANGE" } as TimelineFeedOptions);
const filteredLatestFeed = useMemo(() => {
// no replies
return latestFeed.main?.filter((ev: TaggedNostrEvent) => !ev.tags.some((tag: string[]) => tag[0] === "e")) ?? [];
return latestFeed.main?.filter((ev: NostrEvent) => !ev.tags.some((tag: string[]) => tag[0] === "e")) ?? [];
}, [latestFeed.main]);
const getFeed = () => {
@ -72,13 +71,13 @@ export const ForYouTab = memo(function ForYouTab() {
return [];
}
if (!getForYouFeedPromise) {
getForYouFeedPromise = getForYouFeed(publicKey);
getForYouFeedPromise = Relay.forYouFeed(publicKey);
}
getForYouFeedPromise!.then(notes => {
getForYouFeedPromise = null;
if (notes.length < 10) {
setTimeout(() => {
getForYouFeed(publicKey);
getForYouFeedPromise = Relay.forYouFeed(publicKey);
}, 1000);
}
forYouFeed = {
@ -86,11 +85,6 @@ export const ForYouTab = memo(function ForYouTab() {
created_at: Date.now(),
};
setNotes(notes);
notes.forEach(note => {
queueMicrotask(() => {
System.HandleEvent(note);
});
});
});
};

View File

@ -1,4 +1,4 @@
import { NostrEvent } from "nostr";
import { NostrEvent } from "../nostr";
import { DexieTableLike, FeedCache } from "@snort/shared";
export class EventsCache extends FeedCache<NostrEvent> {

View File

@ -1,4 +1,4 @@
import { ReqFilter } from "nostr";
import { ReqFilter } from "./nostr";
/**
* Remove empty filters, filters which would result in no results

View File

@ -24,8 +24,8 @@ import { CachedTable } from "@snort/shared";
import { EventsCache } from "../cache/events";
import { RelayMetricHandler } from "../relay-metric-handler";
import debug from "debug";
import { ConnectionPool } from "connection-pool";
import { CacheRelay } from "cache-relay";
import { ConnectionPool } from "../connection-pool";
import { CacheRelay } from "../cache-relay";
export class SystemWorker extends EventEmitter<NostrSystemEvents> implements SystemInterface {
#log = debug("SystemWorker");

View File

@ -4,9 +4,10 @@ import { findTag } from "./utils";
import { EventExt } from "./event-ext";
import { NostrLink } from "./nostr-link";
import debug from "debug";
import {LRUCache} from "lru-cache";
const Log = debug("zaps");
const ParsedZapCache = new Map<string, ParsedZap>();
const ParsedZapCache = new LRUCache<string, ParsedZap>({ max: 1000 });
function getInvoice(zap: NostrEvent): InvoiceDetails | undefined {
const bolt11 = findTag(zap, "bolt11");

View File

@ -1,49 +1,50 @@
import { NostrEvent, parseZap } from "@snort/system";
import debug from "debug";
import { Relay } from "@/Cache";
import { RelayHandler, NostrEvent } from "./types";
// import { parseZap } from "../../system/src/zaps";
// placeholder:
const parseZap = (_zap: NostrEvent) => {
return { event: null } as { event: null | NostrEvent };
}
const log = debug("getForYouFeed");
export async function getForYouFeed(pubkey: string): Promise<NostrEvent[]> {
export async function getForYouFeed(relay: RelayHandler, pubkey: string): Promise<NostrEvent[]> {
console.time("For You feed generation time");
log("pubkey", pubkey);
// Get events reacted to by me
const myReactedEventIds = await getMyReactedEvents(pubkey);
const myReactedEventIds = await getMyReactedEvents(relay, pubkey);
log("my reacted events", myReactedEventIds);
const myReactedAuthors = await getMyReactedAuthors(myReactedEventIds, pubkey);
const myReactedAuthors = await getMyReactedAuthors(relay, myReactedEventIds, pubkey);
log("my reacted authors", myReactedAuthors);
// Get others who reacted to the same events as me
const othersWhoReacted = await getOthersWhoReacted(myReactedEventIds, pubkey);
const othersWhoReacted = await getOthersWhoReacted(relay, myReactedEventIds, pubkey);
// this tends to be small when the user has just logged in, we should maybe subscribe for more from relays
log("others who reacted", othersWhoReacted);
// Get event ids reacted to by those others
const reactedByOthers = await getEventIdsReactedByOthers(othersWhoReacted, myReactedEventIds, pubkey);
const reactedByOthers = await getEventIdsReactedByOthers(relay, othersWhoReacted, myReactedEventIds, pubkey);
log("reacted by others", reactedByOthers);
// Get full events in sorted order
const feed = await getFeedEvents(reactedByOthers, myReactedAuthors);
const feed = await getFeedEvents(relay, reactedByOthers, myReactedAuthors);
log("feed.length", feed.length);
console.timeEnd("For You feed generation time");
return feed;
}
async function getMyReactedAuthors(myReactedEventIds: Set<string>, myPubkey: string) {
async function getMyReactedAuthors(relay: RelayHandler, myReactedEventIds: Set<string>, myPubkey: string) {
const myReactedAuthors = new Map<string, number>();
const myReactions = await Relay.query([
"REQ",
"getMyReactedAuthors",
{
"#e": Array.from(myReactedEventIds),
},
]);
const myReactions = relay.req("getMyReactedAuthors", {
"#e": Array.from(myReactedEventIds),
}) as NostrEvent[];
myReactions.forEach(reaction => {
if (reaction.pubkey !== myPubkey) {
@ -54,19 +55,15 @@ async function getMyReactedAuthors(myReactedEventIds: Set<string>, myPubkey: str
return myReactedAuthors;
}
async function getMyReactedEvents(pubkey: string) {
async function getMyReactedEvents(relay: RelayHandler, pubkey: string) {
const myReactedEventIds = new Set<string>();
const myEvents = await Relay.query([
"REQ",
"getMyReactedEventIds",
{
authors: [pubkey],
kinds: [1, 6, 7, 9735],
},
]);
const myEvents = relay.req("getMyReactedEventIds", {
authors: [pubkey],
kinds: [1, 6, 7, 9735],
}) as NostrEvent[];
myEvents.forEach(ev => {
const targetEventId = ev.kind === 9735 ? parseZap(ev).event?.id : ev.tags.find(tag => tag[0] === "e")?.[1];
const targetEventId = ev.kind === 9735 ? parseZap(ev).event?.id : ev.tags.find((tag: string[]) => tag[0] === "e")?.[1];
if (targetEventId) {
myReactedEventIds.add(targetEventId);
}
@ -75,16 +72,12 @@ async function getMyReactedEvents(pubkey: string) {
return myReactedEventIds;
}
async function getOthersWhoReacted(myReactedEventIds: Set<string>, myPubkey: string) {
async function getOthersWhoReacted(relay: RelayHandler, myReactedEventIds: Set<string>, myPubkey: string) {
const othersWhoReacted = new Map<string, number>();
const otherReactions = await Relay.query([
"REQ",
"getOthersWhoReacted",
{
"#e": Array.from(myReactedEventIds),
},
]);
const otherReactions = relay.req("getOthersWhoReacted", {
"#e": Array.from(myReactedEventIds),
}) as NostrEvent[];
otherReactions.forEach(reaction => {
if (reaction.pubkey !== myPubkey) {
@ -96,27 +89,24 @@ async function getOthersWhoReacted(myReactedEventIds: Set<string>, myPubkey: str
}
async function getEventIdsReactedByOthers(
relay: RelayHandler,
othersWhoReacted: Map<string, number>,
myReactedEvents: Set<string>,
myPub: string,
) {
const eventIdsReactedByOthers = new Map<string, number>();
const events = await Relay.query([
"REQ",
"getEventIdsReactedByOthers",
{
authors: [...othersWhoReacted.keys()],
kinds: [1, 6, 7, 9735],
},
]);
const events = relay.req("getEventIdsReactedByOthers", {
authors: [...othersWhoReacted.keys()],
kinds: [1, 6, 7, 9735],
}) as NostrEvent[];
events.forEach(event => {
if (event.pubkey === myPub || myReactedEvents.has(event.id)) {
// NIP-113 NOT filter could improve performance by not selecting these events in the first place
return;
}
event.tags.forEach(tag => {
event.tags.forEach((tag: string[]) => {
if (tag[0] === "e") {
const score = Math.ceil(Math.sqrt(othersWhoReacted.get(event.pubkey) || 0));
eventIdsReactedByOthers.set(tag[1], (eventIdsReactedByOthers.get(tag[1]) || 0) + score);
@ -127,16 +117,20 @@ async function getEventIdsReactedByOthers(
return eventIdsReactedByOthers;
}
async function getFeedEvents(reactedToIds: Map<string, number>, reactedToAuthors: Map<string, number>) {
const events = await Relay.query([
"REQ",
"getFeedEvents",
{
ids: Array.from(reactedToIds.keys()),
kinds: [1],
since: Math.floor(Date.now() / 1000) - 60 * 60 * 24 * 7,
},
]);
async function getFeedEvents(
relay: RelayHandler,
reactedToIds: Map<string, number>,
reactedToAuthors: Map<string, number>,
) {
const events = relay
.sql(
`select json from events where id in (${Array.from(reactedToIds.keys())
.map(() => "?")
.join(", ")}) and kind = 1 order by seen_at ASC, created DESC limit 1000`,
Array.from(reactedToIds.keys()),
)
.map(row => JSON.parse(row[0] as string) as NostrEvent);
const seen = new Set<string>(events.map(ev => ev.id));
log("reactedToAuthors", reactedToAuthors);
@ -145,16 +139,14 @@ async function getFeedEvents(reactedToIds: Map<string, number>, reactedToAuthors
.sort((a, b) => reactedToAuthors.get(b)! - reactedToAuthors.get(a)!)
.slice(20);
const eventsByFavoriteAuthors = await Relay.query([
"REQ",
"getFeedEvents",
{
authors: favoriteAuthors,
kinds: [1],
since: Math.floor(Date.now() / 1000) - 60 * 60 * 24,
limit: 100,
},
]);
const eventsByFavoriteAuthors = relay
.sql(
`select json from events where pubkey in (${favoriteAuthors
.map(() => "?")
.join(", ")}) and kind = 1 order by seen_at ASC, created DESC limit 100`,
favoriteAuthors,
)
.map(row => JSON.parse(row[0] as string) as NostrEvent);
eventsByFavoriteAuthors.forEach(ev => {
if (!seen.has(ev.id)) {
@ -163,7 +155,7 @@ async function getFeedEvents(reactedToIds: Map<string, number>, reactedToAuthors
});
// Filter out replies
const filteredEvents = events.filter(ev => !ev.tags.some(tag => tag[0] === "e"));
const filteredEvents = events.filter(ev => !ev.tags.some((tag: string[]) => tag[0] === "e"));
// Define constants for normalization
// const recentnessWeight = -1;

View File

@ -1,4 +1,12 @@
import { NostrEvent, OkResponse, ReqCommand, ReqFilter, WorkerMessage, WorkerMessageCommand } from "./types";
import {
EventMetadata,
NostrEvent,
OkResponse,
ReqCommand,
ReqFilter,
WorkerMessage,
WorkerMessageCommand,
} from "./types";
import { v4 as uuid } from "uuid";
export class WorkerRelayInterface {
@ -45,6 +53,14 @@ export class WorkerRelayInterface {
return await this.#workerRpc<void, Uint8Array>("dumpDb");
}
async forYouFeed(pubkey: string) {
return await this.#workerRpc<string, Array<NostrEvent>>("forYouFeed", pubkey);
}
setEventMetadata(id: string, meta: EventMetadata) {
return this.#workerRpc<[string, EventMetadata], void>("setEventMetadata", [id, meta]);
}
#workerRpc<T, R>(cmd: WorkerMessageCommand, args?: T) {
const id = uuid();
const msg = {

View File

@ -1,5 +1,5 @@
import EventEmitter from "eventemitter3";
import { NostrEvent, RelayHandler, RelayHandlerEvents, ReqFilter, eventMatchesFilter } from "./types";
import { NostrEvent, RelayHandler, RelayHandlerEvents, ReqFilter, eventMatchesFilter, EventMetadata } from "./types";
import debug from "debug";
/**
@ -79,4 +79,8 @@ export class InMemoryRelay extends EventEmitter<RelayHandlerEvents> implements R
}
return ret;
}
setEventMetadata(_id: string, _meta: EventMetadata) {
return;
}
}

View File

@ -0,0 +1,101 @@
import { NostrEvent } from "./types";
import { SqliteRelay } from "./sqlite-relay";
import debug from "debug";
const log = debug("SqliteRelay:migrations");
/**
* Do database migration
*/
function migrate(relay: SqliteRelay) {
if (!relay.db) throw new Error("DB must be open");
relay.db.exec(
'CREATE TABLE IF NOT EXISTS "__migration" (version INTEGER,migrated NUMERIC, CONSTRAINT "__migration_PK" PRIMARY KEY (version))',
);
const res = relay.db.exec("select max(version) from __migration", {
returnValue: "resultRows",
});
const version = (res[0][0] as number | undefined) ?? 0;
log(`Starting migration from: v${version}`);
if (version < 1) {
migrate_v1(relay);
log("Migrated to v1");
}
if (version < 2) {
migrate_v2(relay);
log("Migrated to v2");
}
if (version < 3) {
migrate_v3(relay);
log("Migrated to v3");
}
if (version < 4) {
migrate_v4(relay);
log("Migrated to v4");
}
}
function migrate_v1(relay: SqliteRelay) {
relay.db?.transaction(db => {
db.exec(
"CREATE TABLE events (\
id TEXT(64) PRIMARY KEY, \
pubkey TEXT(64), \
created INTEGER, \
kind INTEGER, \
json TEXT \
)",
);
db.exec(
"CREATE TABLE tags (\
event_id TEXT(64), \
key TEXT, \
value TEXT, \
CONSTRAINT tags_FK FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE \
)",
);
db.exec("CREATE INDEX tags_key_IDX ON tags (key,value)");
db.exec("insert into __migration values(1, ?)", {
bind: [new Date().getTime() / 1000],
});
});
}
function migrate_v2(relay: SqliteRelay) {
relay.db?.transaction(db => {
db.exec("CREATE INDEX pubkey_kind_IDX ON events (pubkey,kind)");
db.exec("CREATE INDEX pubkey_created_IDX ON events (pubkey,created)");
db.exec("insert into __migration values(2, ?)", {
bind: [new Date().getTime() / 1000],
});
});
}
function migrate_v3(relay: SqliteRelay) {
relay.db?.transaction(db => {
db.exec("CREATE VIRTUAL TABLE search_content using fts5(id UNINDEXED, content)");
const events = db.selectArrays("select json from events where kind in (?,?)", [0, 1]);
for (const json of events) {
const ev = JSON.parse(json[0] as string) as NostrEvent;
if (ev) {
relay.insertIntoSearchIndex(db, ev);
}
}
db.exec("insert into __migration values(3, ?)", {
bind: [new Date().getTime() / 1000],
});
});
}
async function migrate_v4(relay: SqliteRelay) {
relay.db?.transaction(db => {
db.exec("ALTER TABLE events ADD COLUMN seen_at INTEGER");
db.exec("insert into __migration values(4, ?)", {
bind: [new Date().getTime() / 1000],
});
});
}
export default migrate;

View File

@ -1,12 +1,13 @@
import sqlite3InitModule, { Database, Sqlite3Static } from "@sqlite.org/sqlite-wasm";
import { EventEmitter } from "eventemitter3";
import { NostrEvent, RelayHandler, RelayHandlerEvents, ReqFilter, unixNowMs } from "./types";
import { EventMetadata, NostrEvent, RelayHandler, RelayHandlerEvents, ReqFilter, unixNowMs } from "./types";
import debug from "debug";
import migrate from "./migrations";
export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements RelayHandler {
#sqlite?: Sqlite3Static;
#log = debug("SqliteRelay");
#db?: Database;
db?: Database;
#seenInserts = new Set<string>();
/**
@ -17,7 +18,7 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
this.#sqlite = await sqlite3InitModule();
this.#log(`Got SQLite version: ${this.#sqlite.version.libVersion}`);
await this.#open(path);
this.#migrate();
this.db && migrate(this);
}
/**
@ -25,13 +26,13 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
*/
async #open(path: string) {
if (!this.#sqlite) throw new Error("Must call init first");
if (this.#db) return;
if (this.db) return;
if ("opfs" in this.#sqlite) {
try {
this.#db = new this.#sqlite.oo1.OpfsDb(path, "cw");
this.#log(`Opened ${this.#db.filename}`);
this.#db.exec(
this.db = new this.#sqlite.oo1.OpfsDb(path, "cw");
this.#log(`Opened ${this.db.filename}`);
this.db.exec(
`PRAGMA cache_size=${
32 * 1024
}; PRAGMA page_size=8192; PRAGMA journal_mode=MEMORY; PRAGMA temp_store=MEMORY;`,
@ -46,44 +47,15 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
}
close() {
this.#db?.close();
this.#db = undefined;
}
/**
* Do database migration
*/
#migrate() {
if (!this.#db) throw new Error("DB must be open");
this.#db.exec(
'CREATE TABLE IF NOT EXISTS "__migration" (version INTEGER,migrated NUMERIC, CONSTRAINT "__migration_PK" PRIMARY KEY (version))',
);
const res = this.#db.exec("select max(version) from __migration", {
returnValue: "resultRows",
});
const version = (res[0][0] as number | undefined) ?? 0;
this.#log(`Starting migration from: v${version}`);
if (version < 1) {
this.#migrate_v1();
this.#log("Migrated to v1");
}
if (version < 2) {
this.#migrate_v2();
this.#log("Migrated to v2");
}
if (version < 3) {
this.#migrate_v3();
this.#log("Migrated to v3");
}
this.db?.close();
this.db = undefined;
}
/**
* Insert an event to the database
*/
event(ev: NostrEvent) {
if (this.#insertEvent(this.#db!, ev)) {
if (this.#insertEvent(this.db!, ev)) {
this.#log(`Inserted: kind=${ev.kind},authors=${ev.pubkey},id=${ev.id}`);
this.emit("event", [ev]);
return true;
@ -92,7 +64,7 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
}
sql(sql: string, params: Array<any>) {
return this.#db?.selectArrays(sql, params) as Array<Array<string | number>>;
return this.db?.selectArrays(sql, params) as Array<Array<string | number>>;
}
/**
@ -101,7 +73,7 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
eventBatch(evs: Array<NostrEvent>) {
const start = unixNowMs();
let eventsInserted: Array<NostrEvent> = [];
this.#db?.transaction(db => {
this.db?.transaction(db => {
for (const ev of evs) {
if (this.#insertEvent(db, ev)) {
eventsInserted.push(ev);
@ -115,6 +87,14 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
return eventsInserted.length > 0;
}
setEventMetadata(id: string, meta: EventMetadata) {
if (meta.seen_at) {
this.db?.exec("update events set seen_at = ? where id = ?", {
bind: [meta.seen_at, id],
});
}
}
#deleteById(db: Database, ids: Array<string>) {
db.exec(`delete from events where id in (${this.#repeatParams(ids.length)})`, {
bind: ids,
@ -164,14 +144,14 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
db.exec("insert or ignore into events(id, pubkey, created, kind, json) values(?,?,?,?,?)", {
bind: [ev.id, ev.pubkey, ev.created_at, ev.kind, JSON.stringify(ev)],
});
let eventInserted = (this.#db?.changes() as number) > 0;
let eventInserted = (this.db?.changes() as number) > 0;
if (eventInserted) {
for (const t of ev.tags.filter(a => a[0].length === 1)) {
db.exec("insert into tags(event_id, key, value) values(?, ?, ?)", {
bind: [ev.id, t[0], t[1]],
});
}
this.#insertSearchIndex(db, ev);
this.insertIntoSearchIndex(db, ev);
}
this.#seenInserts.add(ev.id);
return eventInserted;
@ -184,7 +164,7 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
const start = unixNowMs();
const [sql, params] = this.#buildQuery(req);
const res = this.#db?.selectArrays(sql, params);
const res = this.db?.selectArrays(sql, params);
const results =
res?.map(a => {
if (req.ids_only === true) {
@ -203,7 +183,7 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
count(req: ReqFilter) {
const start = unixNowMs();
const [sql, params] = this.#buildQuery(req, true);
const rows = this.#db?.exec(sql, {
const rows = this.db?.exec(sql, {
bind: params,
returnValue: "resultRows",
});
@ -217,7 +197,7 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
* Get a summary about events table
*/
summary() {
const res = this.#db?.exec("select kind, count(*) from events group by kind", {
const res = this.db?.exec("select kind, count(*) from events group by kind", {
returnValue: "resultRows",
});
return Object.fromEntries(res?.map(a => [String(a[0]), a[1] as number]) ?? []);
@ -227,10 +207,10 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
* Dump the database file
*/
async dump() {
const filePath = String(this.#db?.filename ?? "");
const filePath = String(this.db?.filename ?? "");
try {
this.#db?.close();
this.#db = undefined;
this.db?.close();
this.db = undefined;
const dir = await navigator.storage.getDirectory();
// @ts-expect-error
for await (const [name, file] of dir) {
@ -328,43 +308,7 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
return res;
}
#migrate_v1() {
this.#db?.transaction(db => {
db.exec(
"CREATE TABLE events (\
id TEXT(64) PRIMARY KEY, \
pubkey TEXT(64), \
created INTEGER, \
kind INTEGER, \
json TEXT \
)",
);
db.exec(
"CREATE TABLE tags (\
event_id TEXT(64), \
key TEXT, \
value TEXT, \
CONSTRAINT tags_FK FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE \
)",
);
db.exec("CREATE INDEX tags_key_IDX ON tags (key,value)");
db.exec("insert into __migration values(1, ?)", {
bind: [new Date().getTime() / 1000],
});
});
}
#migrate_v2() {
this.#db?.transaction(db => {
db.exec("CREATE INDEX pubkey_kind_IDX ON events (pubkey,kind)");
db.exec("CREATE INDEX pubkey_created_IDX ON events (pubkey,created)");
db.exec("insert into __migration values(2, ?)", {
bind: [new Date().getTime() / 1000],
});
});
}
#insertSearchIndex(db: Database, ev: NostrEvent) {
insertIntoSearchIndex(db: Database, ev: NostrEvent) {
if (ev.kind === 0) {
const profile = JSON.parse(ev.content) as {
name?: string;
@ -393,20 +337,4 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
});
}
}
#migrate_v3() {
this.#db?.transaction(db => {
db.exec("CREATE VIRTUAL TABLE search_content using fts5(id UNINDEXED, content)");
const events = db.selectArrays("select json from events where kind in (?,?)", [0, 1]);
for (const json of events) {
const ev = JSON.parse(json[0] as string) as NostrEvent;
if (ev) {
this.#insertSearchIndex(db, ev);
}
}
db.exec("insert into __migration values(3, ?)", {
bind: [new Date().getTime() / 1000],
});
});
}
}

View File

@ -9,7 +9,9 @@ export type WorkerMessageCommand =
| "summary"
| "close"
| "dumpDb"
| "emit-event";
| "emit-event"
| "forYouFeed"
| "setEventMetadata";
export interface WorkerMessage<T> {
id: string;
@ -28,6 +30,10 @@ export interface NostrEvent {
relays?: Array<string>;
}
export interface EventMetadata {
seen_at?: number;
}
export type ReqCommand = ["REQ", id: string, ...filters: Array<ReqFilter>];
export interface ReqFilter {
@ -64,6 +70,7 @@ export interface RelayHandler extends EventEmitter<RelayHandlerEvents> {
count(req: ReqFilter): number;
summary(): Record<string, number>;
dump(): Promise<Uint8Array>;
setEventMetadata(id: string, meta: EventMetadata): void;
}
export interface RelayHandlerEvents {

View File

@ -3,7 +3,8 @@
import { InMemoryRelay } from "./memory-relay";
import { WorkQueueItem, barrierQueue, processWorkQueue } from "./queue";
import { SqliteRelay } from "./sqlite-relay";
import { NostrEvent, RelayHandler, ReqCommand, ReqFilter, WorkerMessage, unixNowMs } from "./types";
import { NostrEvent, RelayHandler, ReqCommand, ReqFilter, WorkerMessage, unixNowMs, EventMetadata } from "./types";
import { getForYouFeed } from "./forYouFeed";
let relay: RelayHandler | undefined;
@ -130,6 +131,20 @@ globalThis.onmessage = async ev => {
});
break;
}
case "forYouFeed": {
await barrierQueue(cmdQueue, async () => {
const res = await getForYouFeed(relay!, msg.args as string);
reply(msg.id, res);
});
break;
}
case "setEventMetadata": {
await barrierQueue(cmdQueue, async () => {
const [id, metadata] = msg.args as [string, EventMetadata];
relay!.setEventMetadata(id, metadata);
});
break;
}
default: {
reply(msg.id, { error: "Unknown command" });
break;