From 27fdbde8df21aabe3371fae17a9e51e58d73a533 Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 09:27:04 +0200 Subject: [PATCH] fixed build and worked on memory leak --- Dockerfile | 9 ++++++ deno.json | 10 +++++++ import_map.json | 19 ++++++++---- src/lib/data_structures/websocket_pool.ts | 24 ++++++++++++++-- src/lib/ndk.ts | 35 ++++++++++++++++++++++- src/routes/+layout.svelte | 10 +++++-- 6 files changed, 95 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index c8ecacc..9bdfec7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,11 @@ FROM denoland/deno:alpine AS build WORKDIR /app/src COPY . . + +# Set memory limits for Deno to prevent memory leaks +ENV DENO_MEMORY_LIMIT=512MB +ENV DENO_GC_INTERVAL=1000 + RUN deno install RUN deno task build @@ -11,6 +16,10 @@ COPY --from=build /app/src/import_map.json . ENV ORIGIN=http://localhost:3000 +# Set memory limits for runtime to prevent memory leaks +ENV DENO_MEMORY_LIMIT=512MB +ENV DENO_GC_INTERVAL=1000 + RUN deno cache --import-map=import_map.json ./build/index.js EXPOSE 3000 diff --git a/deno.json b/deno.json index 9e2ecc6..350316e 100644 --- a/deno.json +++ b/deno.json @@ -2,5 +2,15 @@ "importMap": "./import_map.json", "compilerOptions": { "lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"] + }, + "tasks": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --plugin-search-dir . --check . && eslint .", + "format": "prettier --plugin-search-dir . --write .", + "test": "vitest" } } diff --git a/import_map.json b/import_map.json index b5aa95c..3c34c52 100644 --- a/import_map.json +++ b/import_map.json @@ -2,18 +2,25 @@ "imports": { "he": "npm:he@1.2.x", "@nostr-dev-kit/ndk": "npm:@nostr-dev-kit/ndk@^2.14.32", - "@nostr-dev-kit/ndk-cache-dexie": "npm:@nostr-dev-kit/ndk-cache-dexie@^2.6.33", + "@nostr-dev-kit/ndk-cache-dexie": "npm:@nostr-dev-kit/ndk-cache-dexie@2.6.x", "@popperjs/core": "npm:@popperjs/core@2.11.x", "@tailwindcss/forms": "npm:@tailwindcss/forms@0.5.x", "@tailwindcss/typography": "npm:@tailwindcss/typography@0.5.x", "asciidoctor": "npm:asciidoctor@3.0.x", - "d3": "npm:d3@7.9.x", - "nostr-tools": "npm:nostr-tools@^2.15.1", + "d3": "npm:d3@^7.9.0", + "nostr-tools": "npm:nostr-tools@2.15.x", "tailwind-merge": "npm:tailwind-merge@^3.3.1", "svelte": "npm:svelte@^5.36.8", - "flowbite": "npm:flowbite@^3.1.2", - "flowbite-svelte": "npm:flowbite-svelte@^1.10.10", - "flowbite-svelte-icons": "npm:flowbite-svelte-icons@^2.2.1", + "flowbite": "npm:flowbite@2.x", + "flowbite-svelte": "npm:flowbite-svelte@0.48.x", + "flowbite-svelte-icons": "npm:flowbite-svelte-icons@2.1.x", + "@noble/curves": "npm:@noble/curves@^1.9.4", + "@noble/hashes": "npm:@noble/hashes@^1.8.0", + "bech32": "npm:bech32@^2.0.0", + "highlight.js": "npm:highlight.js@^11.11.1", + "node-emoji": "npm:node-emoji@^2.2.0", + "plantuml-encoder": "npm:plantuml-encoder@^1.4.0", + "qrcode": "npm:qrcode@^1.5.4", "child_process": "node:child_process" } } diff --git a/src/lib/data_structures/websocket_pool.ts b/src/lib/data_structures/websocket_pool.ts index fca0325..5efcdf5 100644 --- a/src/lib/data_structures/websocket_pool.ts +++ b/src/lib/data_structures/websocket_pool.ts @@ -191,20 +191,27 @@ export class WebSocketPool { * Closes all WebSocket connections and "drains" the pool. */ public drain(): void { + console.debug(`[WebSocketPool] Draining pool with ${this.#pool.size} connections and ${this.#waitingQueue.length} waiting requests`); + // Clear all idle timers first for (const handle of this.#pool.values()) { this.#clearIdleTimer(handle); } + // Reject all waiting requests for (const { reject } of this.#waitingQueue) { reject(new Error('[WebSocketPool] Draining pool.')); } this.#waitingQueue = []; + // Close all connections and clean up for (const handle of this.#pool.values()) { - handle.ws.close(); + if (handle.ws && handle.ws.readyState === WebSocket.OPEN) { + handle.ws.close(); + } } this.#pool.clear(); + console.debug('[WebSocketPool] Pool drained successfully'); } // #endregion @@ -243,8 +250,18 @@ export class WebSocketPool { #removeSocket(handle: WebSocketHandle): void { this.#clearIdleTimer(handle); - handle.ws.onopen = handle.ws.onerror = handle.ws.onclose = null; - this.#pool.delete(this.#normalizeUrl(handle.ws.url)); + + // Clean up event listeners to prevent memory leaks + if (handle.ws) { + handle.ws.onopen = null; + handle.ws.onerror = null; + handle.ws.onclose = null; + handle.ws.onmessage = null; + } + + const url = this.#normalizeUrl(handle.ws.url); + this.#pool.delete(url); + console.debug(`[WebSocketPool] Removed socket for ${url}, pool size: ${this.#pool.size}`); this.#processWaitingQueue(); } @@ -261,6 +278,7 @@ export class WebSocketPool { handle.idleTimer = setTimeout(() => { const refCount = handle.refCount; if (refCount === 0 && handle.ws.readyState === WebSocket.OPEN) { + console.debug(`[WebSocketPool] Closing idle connection to ${handle.ws.url}`); handle.ws.close(); this.#removeSocket(handle); } diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index 74ed95a..55afb4d 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -654,7 +654,10 @@ export function initNdk(): NDK { if (retryCount < maxRetries) { retryCount++; console.debug(`[NDK.ts] Attempting to reconnect (${retryCount}/${maxRetries})...`); - setTimeout(attemptConnection, 2000); // Reduce timeout to 2 seconds + // Use a more reasonable retry delay and prevent memory leaks + setTimeout(() => { + attemptConnection(); + }, 2000 * retryCount); // Exponential backoff } else { console.warn("[NDK.ts] Max retries reached, continuing with limited functionality"); // Still try to update relay stores even if connection failed @@ -691,6 +694,36 @@ export function initNdk(): NDK { return ndk; } +/** + * Cleans up NDK resources to prevent memory leaks + * Should be called when the application is shutting down or when NDK needs to be reset + */ +export function cleanupNdk(): void { + console.debug("[NDK.ts] Cleaning up NDK resources"); + + const ndk = get(ndkInstance); + if (ndk) { + try { + // Disconnect from all relays + if (ndk.pool) { + for (const relay of ndk.pool.relays.values()) { + relay.disconnect(); + } + } + + // Drain the WebSocket pool + WebSocketPool.instance.drain(); + + // Stop network monitoring + stopNetworkStatusMonitoring(); + + console.debug("[NDK.ts] NDK cleanup completed"); + } catch (error) { + console.warn("[NDK.ts] Error during NDK cleanup:", error); + } + } +} + /** * Signs in with a NIP-07 browser extension using the new relay management system * @returns The user's profile, if it is available diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index dd7f835..43bfb5c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -6,7 +6,7 @@ import { goto } from "$app/navigation"; import { Alert } from "flowbite-svelte"; import { HammerSolid } from "flowbite-svelte-icons"; - import { logCurrentRelayConfiguration, activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; + import { logCurrentRelayConfiguration, activeInboxRelays, activeOutboxRelays, cleanupNdk } from "$lib/ndk"; // Define children prop for Svelte 5 let { children } = $props(); @@ -83,7 +83,13 @@ } document.addEventListener("click", handleInternalLinkClick); - return () => document.removeEventListener("click", handleInternalLinkClick); + + // Cleanup function to prevent memory leaks + return () => { + document.removeEventListener("click", handleInternalLinkClick); + // Clean up NDK resources when component unmounts + cleanupNdk(); + }; });