Add option to prune older events in wasm relay

This commit is contained in:
hzrd149 2024-07-21 10:07:24 -05:00
parent cfef0cc8b5
commit 423632fdc5
7 changed files with 236 additions and 10 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add option to prune older events in wasm relay

View File

@ -1,4 +1,4 @@
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { ButtonGroup, IconButton, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react";
import { TrashIcon } from "../icons";
@ -9,6 +9,7 @@ export default function EventKindsTable({
kinds: Record<string, number>;
deleteKind?: (kind: string) => Promise<void>;
}) {
const [deleting, setDeleting] = useState<string>();
const sorted = useMemo(
() =>
Object.entries(kinds)
@ -24,7 +25,7 @@ export default function EventKindsTable({
<Tr>
<Th isNumeric>Kind</Th>
<Th isNumeric>Count</Th>
{deleteKind && <Th></Th>}
{deleteKind && <Th w="4"></Th>}
</Tr>
</Thead>
<Tbody>
@ -36,11 +37,15 @@ export default function EventKindsTable({
<Td isNumeric>
<ButtonGroup size="xs">
<IconButton
isLoading={deleting === kind}
icon={<TrashIcon />}
aria-label="Delete kind"
colorScheme="red"
variant="ghost"
onClick={() => deleteKind(kind)}
onClick={() => {
setDeleting(kind);
deleteKind(kind).finally(() => setDeleting(undefined));
}}
/>
</ButtonGroup>
</Td>

View File

@ -7,6 +7,8 @@ import WasmRelay from "./wasm-relay";
import MemoryRelay from "../classes/memory-relay";
import { fakeVerifyEvent } from "./verify-event";
import relayPoolService from "./relay-pool";
import localSettings from "./local-settings";
import dayjs from "dayjs";
// save the local relay from query params to localStorage
const params = new URLSearchParams(location.search);
@ -40,7 +42,7 @@ export const localDatabase = await openDB();
// Setup relay
function createInternalRelay() {
return new CacheRelay(localDatabase, { maxEvents: 10000 });
return new CacheRelay(localDatabase, { maxEvents: localSettings.idbMaxEvents.value });
}
async function createRelay() {
const localRelayURL = localStorage.getItem("localRelay");
@ -99,6 +101,17 @@ setInterval(() => {
if (localRelay && !localRelay.connected) localRelay.connect().then(() => log("Reconnected"));
}, 1000 * 5);
// every minute, prune the database
setInterval(() => {
if (localRelay instanceof WasmRelay) {
const days = localSettings.wasmPersistForDays.value;
if (days) {
log(`Removing all events older than ${days} days in WASM relay`);
localRelay.worker?.delete(["REQ", "prune", { until: dayjs().subtract(days, "days").unix() }]);
}
}
}, 60_000);
if (import.meta.env.DEV) {
//@ts-ignore
window.localDatabase = localDatabase;

View File

@ -0,0 +1,117 @@
import { PersistentSubject } from "../classes/subject";
class NullableLocalStorageEntry<T = string> extends PersistentSubject<T | null> {
key: string;
decode?: (raw: string | null) => T | null;
encode?: (value: T) => string | null;
constructor(
key: string,
initValue: T | null = null,
decode?: (raw: string | null) => T | null,
encode?: (value: T) => string | null,
) {
let value = initValue;
if (localStorage.hasOwnProperty(key)) {
const raw = localStorage.getItem(key);
if (decode) value = decode(raw);
else value = raw as T | null;
}
super(value);
this.key = key;
this.decode = decode;
this.encode = encode;
}
next(value: T | null) {
if (value === null) {
localStorage.removeItem(this.key);
super.next(value);
} else {
const encoded = this.encode ? this.encode(value) : String(value);
if (encoded !== null) localStorage.setItem(this.key, encoded);
else localStorage.removeItem(this.key);
super.next(value);
}
}
clear() {
this.next(null);
}
}
class LocalStorageEntry<T = string> extends PersistentSubject<T> {
key: string;
fallback: T;
decode?: (raw: string) => T;
encode?: (value: T) => string | null;
constructor(key: string, fallback: T, decode?: (raw: string) => T, encode?: (value: T) => string | null) {
let value = fallback;
if (localStorage.hasOwnProperty(key)) {
const raw = localStorage.getItem(key);
if (decode && raw) value = decode(raw);
else if (raw) value = raw as T;
}
super(value);
this.key = key;
this.decode = decode;
this.encode = encode;
this.fallback = fallback;
}
next(value: T) {
const encoded = this.encode ? this.encode(value) : String(value);
if (encoded !== null) localStorage.setItem(this.key, encoded);
else localStorage.removeItem(this.key);
super.next(value);
}
clear() {
localStorage.removeItem(this.key);
super.next(this.fallback);
}
}
class NumberLocalStorageEntry extends LocalStorageEntry<number> {
constructor(key: string, fallback: number) {
super(
key,
fallback,
(raw) => parseInt(raw),
(value) => String(value),
);
}
}
class NullableNumberLocalStorageEntry extends NullableLocalStorageEntry<number> {
constructor(key: string, fallback: number) {
super(
key,
fallback,
(raw) => (raw !== null ? parseInt(raw) : raw),
(value) => String(value),
);
}
}
// local relay
const idbMaxEvents = new NumberLocalStorageEntry("nostr-idb-max-events", 10_000);
const wasmPersistForDays = new NullableNumberLocalStorageEntry("wasm-relay-oldest-event", 365);
const localSettings = {
idbMaxEvents,
wasmPersistForDays,
};
if (import.meta.env.DEV) {
// @ts-expect-error
window.localSettings = localSettings;
}
export default localSettings;

View File

@ -1,15 +1,32 @@
import { useState } from "react";
import { addEvents, countEvents, countEventsByKind, getEventUID, updateUsed } from "nostr-idb";
import { Button, ButtonGroup, Card, Flex, Heading, Text } from "@chakra-ui/react";
import {
Button,
ButtonGroup,
Card,
Flex,
FormControl,
FormLabel,
Heading,
Input,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Text,
} from "@chakra-ui/react";
import { useAsync } from "react-use";
import { NostrEvent } from "nostr-tools";
import { localDatabase, localRelay } from "../../../../services/local-relay";
import { localDatabase } from "../../../../services/local-relay";
import EventKindsPieChart from "../../../../components/charts/event-kinds-pie-chart";
import EventKindsTable from "../../../../components/charts/event-kinds-table";
import ImportEventsButton from "./components/import-events-button";
import ExportEventsButton from "./components/export-events-button";
import { clearCacheData, deleteDatabase } from "../../../../services/db";
import useSubject from "../../../../hooks/use-subject";
import localSettings from "../../../../services/local-settings";
async function importEvents(events: NostrEvent[]) {
await addEvents(localDatabase, events);
@ -26,6 +43,8 @@ export default function InternalDatabasePage() {
const { value: count } = useAsync(async () => await countEvents(localDatabase), []);
const { value: kinds } = useAsync(async () => await countEventsByKind(localDatabase), []);
const maxEvents = useSubject(localSettings.idbMaxEvents);
const [clearing, setClearing] = useState(false);
const handleClearData = async () => {
setClearing(true);
@ -55,6 +74,24 @@ export default function InternalDatabasePage() {
Delete database
</Button>
</ButtonGroup>
<FormControl>
<FormLabel>Maximum number of events</FormLabel>
<NumberInput
maxW="xs"
value={maxEvents}
onChange={(s, v) => {
if (Number.isFinite(v)) localSettings.idbMaxEvents.next(v);
else localSettings.idbMaxEvents.clear();
}}
step={1000}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormControl>
<Flex gap="2" wrap="wrap" alignItems="flex-start" w="full">
{kinds && (
<>

View File

@ -1,5 +1,18 @@
import { useCallback, useEffect, useState } from "react";
import { ButtonGroup, Card, Flex, Heading, Text } from "@chakra-ui/react";
import {
ButtonGroup,
Card,
Flex,
FormControl,
FormLabel,
Heading,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Text,
} from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { localRelay } from "../../../../services/local-relay";
@ -8,6 +21,8 @@ import EventKindsPieChart from "../../../../components/charts/event-kinds-pie-ch
import EventKindsTable from "../../../../components/charts/event-kinds-table";
import ImportEventsButton from "./components/import-events-button";
import ExportEventsButton from "./components/export-events-button";
import useSubject from "../../../../hooks/use-subject";
import localSettings from "../../../../services/local-settings";
export default function WasmDatabasePage() {
const relay = localRelay;
@ -16,6 +31,7 @@ export default function WasmDatabasePage() {
if (!worker) return null;
const [summary, setSummary] = useState<Record<string, number>>();
const persistForDays = useSubject(localSettings.wasmPersistForDays);
const total = summary ? Object.values(summary).reduce((t, v) => t + v, 0) : undefined;
@ -50,6 +66,17 @@ export default function WasmDatabasePage() {
return worker.query(["REQ", "export", {}]);
}, [worker]);
const deleteKind = useCallback(
async (kind: string) => {
const k = parseInt(kind);
if (confirm(`Are you sure you want to delete all kind ${k} events?`)) {
await worker.delete(["REQ", "delete-" + k, { kinds: [k] }]);
refresh();
}
},
[worker, refresh],
);
useEffect(() => {
refresh();
}, []);
@ -62,6 +89,25 @@ export default function WasmDatabasePage() {
<ImportEventsButton onLoad={importEvents} />
<ExportEventsButton getEvents={exportEvents} />
</ButtonGroup>
<FormControl>
<FormLabel>Remove events older than X days</FormLabel>
<NumberInput
maxW="xs"
value={persistForDays ?? undefined}
onChange={(s, v) => {
if (Number.isFinite(v)) localSettings.wasmPersistForDays.next(v);
else localSettings.wasmPersistForDays.clear();
}}
step={1000}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormControl>
<Flex gap="2" wrap="wrap" alignItems="flex-start" w="full">
{summary && (
<>
@ -70,7 +116,7 @@ export default function WasmDatabasePage() {
<EventKindsPieChart kinds={summary} />
</Card>
<Card p="2" minW="sm" maxW="md" flex={1}>
<EventKindsTable kinds={summary} />
<EventKindsTable kinds={summary} deleteKind={deleteKind} />
</Card>
</>
)}

View File

@ -1,5 +1,5 @@
import { Outlet, Link as RouterLink, useLocation } from "react-router-dom";
import { Button, Flex } from "@chakra-ui/react";
import { Button, Flex, Spinner } from "@chakra-ui/react";
import VerticalPageLayout from "../../components/vertical-page-layout";
import useCurrentAccount from "../../hooks/use-current-account";
@ -13,6 +13,7 @@ import useUserContactRelays from "../../hooks/use-user-contact-relays";
import UserSquare from "../../components/icons/user-square";
import Image01 from "../../components/icons/image-01";
import Server05 from "../../components/icons/server-05";
import { Suspense } from "react";
export default function RelaysView() {
const account = useCurrentAccount();
@ -125,7 +126,9 @@ export default function RelaysView() {
return (
<Flex gap="2" minH="100vh" overflow="hidden">
{nav}
<Outlet />
<Suspense fallback={<Spinner />}>
<Outlet />
</Suspense>
</Flex>
);
};