mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-27 20:17:05 +02:00
Add option to prune older events in wasm relay
This commit is contained in:
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 { ButtonGroup, IconButton, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react";
|
||||||
import { TrashIcon } from "../icons";
|
import { TrashIcon } from "../icons";
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ export default function EventKindsTable({
|
|||||||
kinds: Record<string, number>;
|
kinds: Record<string, number>;
|
||||||
deleteKind?: (kind: string) => Promise<void>;
|
deleteKind?: (kind: string) => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
|
const [deleting, setDeleting] = useState<string>();
|
||||||
const sorted = useMemo(
|
const sorted = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.entries(kinds)
|
Object.entries(kinds)
|
||||||
@@ -24,7 +25,7 @@ export default function EventKindsTable({
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th isNumeric>Kind</Th>
|
<Th isNumeric>Kind</Th>
|
||||||
<Th isNumeric>Count</Th>
|
<Th isNumeric>Count</Th>
|
||||||
{deleteKind && <Th></Th>}
|
{deleteKind && <Th w="4"></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -36,11 +37,15 @@ export default function EventKindsTable({
|
|||||||
<Td isNumeric>
|
<Td isNumeric>
|
||||||
<ButtonGroup size="xs">
|
<ButtonGroup size="xs">
|
||||||
<IconButton
|
<IconButton
|
||||||
|
isLoading={deleting === kind}
|
||||||
icon={<TrashIcon />}
|
icon={<TrashIcon />}
|
||||||
aria-label="Delete kind"
|
aria-label="Delete kind"
|
||||||
colorScheme="red"
|
colorScheme="red"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => deleteKind(kind)}
|
onClick={() => {
|
||||||
|
setDeleting(kind);
|
||||||
|
deleteKind(kind).finally(() => setDeleting(undefined));
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Td>
|
</Td>
|
||||||
|
@@ -7,6 +7,8 @@ import WasmRelay from "./wasm-relay";
|
|||||||
import MemoryRelay from "../classes/memory-relay";
|
import MemoryRelay from "../classes/memory-relay";
|
||||||
import { fakeVerifyEvent } from "./verify-event";
|
import { fakeVerifyEvent } from "./verify-event";
|
||||||
import relayPoolService from "./relay-pool";
|
import relayPoolService from "./relay-pool";
|
||||||
|
import localSettings from "./local-settings";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
// save the local relay from query params to localStorage
|
// save the local relay from query params to localStorage
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
@@ -40,7 +42,7 @@ export const localDatabase = await openDB();
|
|||||||
|
|
||||||
// Setup relay
|
// Setup relay
|
||||||
function createInternalRelay() {
|
function createInternalRelay() {
|
||||||
return new CacheRelay(localDatabase, { maxEvents: 10000 });
|
return new CacheRelay(localDatabase, { maxEvents: localSettings.idbMaxEvents.value });
|
||||||
}
|
}
|
||||||
async function createRelay() {
|
async function createRelay() {
|
||||||
const localRelayURL = localStorage.getItem("localRelay");
|
const localRelayURL = localStorage.getItem("localRelay");
|
||||||
@@ -99,6 +101,17 @@ setInterval(() => {
|
|||||||
if (localRelay && !localRelay.connected) localRelay.connect().then(() => log("Reconnected"));
|
if (localRelay && !localRelay.connected) localRelay.connect().then(() => log("Reconnected"));
|
||||||
}, 1000 * 5);
|
}, 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) {
|
if (import.meta.env.DEV) {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
window.localDatabase = localDatabase;
|
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 { useState } from "react";
|
||||||
import { addEvents, countEvents, countEventsByKind, getEventUID, updateUsed } from "nostr-idb";
|
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 { useAsync } from "react-use";
|
||||||
import { NostrEvent } from "nostr-tools";
|
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 EventKindsPieChart from "../../../../components/charts/event-kinds-pie-chart";
|
||||||
import EventKindsTable from "../../../../components/charts/event-kinds-table";
|
import EventKindsTable from "../../../../components/charts/event-kinds-table";
|
||||||
import ImportEventsButton from "./components/import-events-button";
|
import ImportEventsButton from "./components/import-events-button";
|
||||||
import ExportEventsButton from "./components/export-events-button";
|
import ExportEventsButton from "./components/export-events-button";
|
||||||
import { clearCacheData, deleteDatabase } from "../../../../services/db";
|
import { clearCacheData, deleteDatabase } from "../../../../services/db";
|
||||||
|
import useSubject from "../../../../hooks/use-subject";
|
||||||
|
import localSettings from "../../../../services/local-settings";
|
||||||
|
|
||||||
async function importEvents(events: NostrEvent[]) {
|
async function importEvents(events: NostrEvent[]) {
|
||||||
await addEvents(localDatabase, events);
|
await addEvents(localDatabase, events);
|
||||||
@@ -26,6 +43,8 @@ export default function InternalDatabasePage() {
|
|||||||
const { value: count } = useAsync(async () => await countEvents(localDatabase), []);
|
const { value: count } = useAsync(async () => await countEvents(localDatabase), []);
|
||||||
const { value: kinds } = useAsync(async () => await countEventsByKind(localDatabase), []);
|
const { value: kinds } = useAsync(async () => await countEventsByKind(localDatabase), []);
|
||||||
|
|
||||||
|
const maxEvents = useSubject(localSettings.idbMaxEvents);
|
||||||
|
|
||||||
const [clearing, setClearing] = useState(false);
|
const [clearing, setClearing] = useState(false);
|
||||||
const handleClearData = async () => {
|
const handleClearData = async () => {
|
||||||
setClearing(true);
|
setClearing(true);
|
||||||
@@ -55,6 +74,24 @@ export default function InternalDatabasePage() {
|
|||||||
Delete database
|
Delete database
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</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">
|
<Flex gap="2" wrap="wrap" alignItems="flex-start" w="full">
|
||||||
{kinds && (
|
{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 { 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 { NostrEvent } from "nostr-tools";
|
||||||
|
|
||||||
import { localRelay } from "../../../../services/local-relay";
|
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 EventKindsTable from "../../../../components/charts/event-kinds-table";
|
||||||
import ImportEventsButton from "./components/import-events-button";
|
import ImportEventsButton from "./components/import-events-button";
|
||||||
import ExportEventsButton from "./components/export-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() {
|
export default function WasmDatabasePage() {
|
||||||
const relay = localRelay;
|
const relay = localRelay;
|
||||||
@@ -16,6 +31,7 @@ export default function WasmDatabasePage() {
|
|||||||
if (!worker) return null;
|
if (!worker) return null;
|
||||||
|
|
||||||
const [summary, setSummary] = useState<Record<string, number>>();
|
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;
|
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", {}]);
|
return worker.query(["REQ", "export", {}]);
|
||||||
}, [worker]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
refresh();
|
refresh();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -62,6 +89,25 @@ export default function WasmDatabasePage() {
|
|||||||
<ImportEventsButton onLoad={importEvents} />
|
<ImportEventsButton onLoad={importEvents} />
|
||||||
<ExportEventsButton getEvents={exportEvents} />
|
<ExportEventsButton getEvents={exportEvents} />
|
||||||
</ButtonGroup>
|
</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">
|
<Flex gap="2" wrap="wrap" alignItems="flex-start" w="full">
|
||||||
{summary && (
|
{summary && (
|
||||||
<>
|
<>
|
||||||
@@ -70,7 +116,7 @@ export default function WasmDatabasePage() {
|
|||||||
<EventKindsPieChart kinds={summary} />
|
<EventKindsPieChart kinds={summary} />
|
||||||
</Card>
|
</Card>
|
||||||
<Card p="2" minW="sm" maxW="md" flex={1}>
|
<Card p="2" minW="sm" maxW="md" flex={1}>
|
||||||
<EventKindsTable kinds={summary} />
|
<EventKindsTable kinds={summary} deleteKind={deleteKind} />
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Outlet, Link as RouterLink, useLocation } from "react-router-dom";
|
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 VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import useCurrentAccount from "../../hooks/use-current-account";
|
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 UserSquare from "../../components/icons/user-square";
|
||||||
import Image01 from "../../components/icons/image-01";
|
import Image01 from "../../components/icons/image-01";
|
||||||
import Server05 from "../../components/icons/server-05";
|
import Server05 from "../../components/icons/server-05";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
export default function RelaysView() {
|
export default function RelaysView() {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
@@ -125,7 +126,9 @@ export default function RelaysView() {
|
|||||||
return (
|
return (
|
||||||
<Flex gap="2" minH="100vh" overflow="hidden">
|
<Flex gap="2" minH="100vh" overflow="hidden">
|
||||||
{nav}
|
{nav}
|
||||||
|
<Suspense fallback={<Spinner />}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
</Suspense>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user