mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
Add option to prune older events in wasm relay
This commit is contained in:
parent
cfef0cc8b5
commit
423632fdc5
5
.changeset/neat-maps-perform.md
Normal file
5
.changeset/neat-maps-perform.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add option to prune older events in wasm relay
|
@ -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>
|
||||
|
@ -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;
|
||||
|
117
src/services/local-settings.ts
Normal file
117
src/services/local-settings.ts
Normal 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;
|
41
src/views/relays/cache/database/internal.tsx
vendored
41
src/views/relays/cache/database/internal.tsx
vendored
@ -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 && (
|
||||
<>
|
||||
|
50
src/views/relays/cache/database/wasm.tsx
vendored
50
src/views/relays/cache/database/wasm.tsx
vendored
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user