Skip to content

Fix dockstat docker exports#132

Open
Its4Nik wants to merge 6 commits into
devfrom
fix-dockstat-docker-exports
Open

Fix dockstat docker exports#132
Its4Nik wants to merge 6 commits into
devfrom
fix-dockstat-docker-exports

Conversation

@Its4Nik

@Its4Nik Its4Nik commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

Introduce a unified WebSocket topic handler for API and frontend, wire logging/metrics over this channel, and normalize package metadata and packaging across DockStat packages with corresponding version bumps and changelogs.

New Features:

  • Add a centralized WebSocket handler on the API for topic-based subscriptions (logs, metrics, and plugin channels) and expose its routes from the main server.
  • Provide a reusable frontend helper for creating authenticated, auto-reconnecting WebSocket topic subscriptions.

Bug Fixes:

  • Resolve circular dependency issues between the API logger and WebSocket modules by lazily loading the WebSocket handler before emitting log events.
  • Simplify and correct the Docker client library exports to better support external consumption.

Enhancements:

  • Refactor frontend log and metrics feeds to use the new topic-based WebSocket subscription helper instead of per-endpoint subscriptions.
  • Align WebSocket routing in the API to use the shared handler entrypoint rather than separate Elysia plugins.
  • Improve several package descriptions, keywords, licenses, and repository metadata for better discoverability and consistency.

Build:

  • Adjust package exports, add .npmignore-based packaging, and bump versions across multiple packages and apps to reflect the new WebSocket and packaging changes.

Documentation:

  • Add changelog files for core apps and packages to document version history and dependency updates.

Chores:

  • Remove unused Eden-related hooks and helper files from the frontend and delete an obsolete DB build script.

@Its4Nik Its4Nik added this to the DockStat v1 - BETA milestone Jul 1, 2026
@Its4Nik Its4Nik self-assigned this Jul 1, 2026
@Its4Nik Its4Nik added this to DockStat Jul 1, 2026
@github-project-automation github-project-automation Bot moved this to Todo in DockStat Jul 1, 2026
@changeset-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: dcdca9b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@sourcery-ai

sourcery-ai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors DockStat’s WebSocket layer to a centralized topic-based handler shared by API and frontend, fixes logger/WebSocket circular dependencies, and normalizes package metadata, exports, and versioning across the monorepo with new changelogs and npm packaging configuration.

Sequence diagram for topic-based WebSocket logging flow

sequenceDiagram
  actor Browser
  participant LogFeedEffect as logFeedEffect
  participant TopicSub as createTopicSubscription
  participant WSClient as WebSocket
  participant DSWebSockerHandler as DSWebSockerHandler
  participant Logger as BaseLogger

  Browser->>LogFeedEffect: mount React component
  LogFeedEffect->>TopicSub: createTopicSubscription("logs", onData)
  TopicSub->>WSClient: new WebSocket(wsUrlWithAuth)
  WSClient-->>TopicSub: onopen
  TopicSub->>WSClient: send ClientMessage { type: "subscribe", topic: "logs" }
  WSClient->>DSWebSockerHandler: message {type: "subscribe", topic: "logs"}
  DSWebSockerHandler->>DSWebSockerHandler: subscribe(ws, "logs")

  Logger->>DSWebSockerHandler: send("logs", logEntry)
  DSWebSockerHandler->>DSWebSockerHandler: broadcast("logs", logEntry)
  DSWebSockerHandler-->>WSClient: ServerMessage { topic: "logs", data, timestamp }
  WSClient-->>TopicSub: onmessage(ServerMessage)
  TopicSub->>LogFeedEffect: onData(logEntry)
  LogFeedEffect-->>Browser: setLogMessage(logEntry)
Loading

File-Level Changes

Change Details Files
Introduce a centralized topic-based WebSocket handler on the API and wire it into the Elysia app.
  • Add WebSocketHandler class that manages topic subscriptions, plugin channels, broadcasting, and introspection using a single /ws route
  • Define normalized topic key format (logs, metrics/*, plugin//) and typed client/server message schemas
  • Expose DSWebSockerHandler instance from websockets index and mount its routes instead of the previous DockStatWebsockets plugin in the main API bootstrap
apps/api/src/websockets/handler.ts
apps/api/src/websockets/index.ts
apps/api/src/index.ts
Refactor logging over WebSockets to avoid circular dependencies and use the new handler-based topic publishing.
  • Remove direct imports of logClients and hook-based broadcasting from the logger module
  • Lazily require the WebSocket handler in the logger callback to avoid circular imports and publish log entries via handler.send("logs", entry)
apps/api/src/logger.ts
Unify frontend WebSocket consumption to a reusable topic subscription helper and adapt existing log/RSS effects.
  • Add createTopicSubscription helper that manages auth, /ws connection, subscribe/unsubscribe messages, reconnection, and topic filtering client-side
  • Refactor logFeedEffect and rssFeedEffect to subscribe to "logs" and "metrics/containers" topics through the new helper and return a cleanup function
  • Introduce (but do not fully wire) a generic useWebSocket hook scaffold in websocketEffects index for future consolidation
apps/dockstat/src/lib/websocketEffects/topicSubscription.ts
apps/dockstat/src/lib/websocketEffects/logFeed.ts
apps/dockstat/src/lib/websocketEffects/rssFeed.ts
apps/dockstat/src/lib/websocketEffects/index.ts
Normalize package metadata, exports, and TypeScript peer requirements across multiple packages and bump patch versions to reflect the changes.
  • Convert simple author strings and bugs/homepage URLs to structured author objects with canonical lowercase GitHub repository URLs
  • Add or standardize description, keywords, MPL-2.0 license, repository blocks, and peerDependencies.typescript set to ^5
  • Adjust exports (e.g., @dockstat/logger exporting "." instead of default, bun-docker simplifying "." export), remove redundant files arrays in favor of .npmignore, and bump patch versions for affected packages and apps
packages/logger/package.json
packages/docker-swarm/package.json
packages/repo-cli/package.json
packages/docker-client/package.json
packages/errors/package.json
packages/outline-sync/package.json
packages/theme-handler/package.json
packages/typings/package.json
packages/utils/package.json
packages/auth/package.json
packages/sqlite-wrapper/package.json
packages/plugin-builder/package.json
packages/bun-docker/package.json
apps/docknode/package.json
packages/db/package.json
packages/ui/package.json
apps/api/package.json
apps/dockstat/package.json
packages/plugin-handler/package.json
packages/template-renderer/package.json
Add changelogs and npm packaging controls for consistency and release tracking.
  • Create CHANGELOG.md files for core apps and packages documenting version bumps and metadata/packaging changes
  • Add .npmignore files to several packages to control published file sets instead of using package.json files arrays
  • Remove obsolete changeset file, eden hooks, and db/build script that are no longer needed in the new release process
apps/api/CHANGELOG.md
packages/docker-client/CHANGELOG.md
apps/dockstat/CHANGELOG.md
apps/docknode/CHANGELOG.md
packages/plugin-handler/CHANGELOG.md
packages/db/CHANGELOG.md
packages/theme-handler/CHANGELOG.md
packages/plugin-builder/CHANGELOG.md
packages/ui/CHANGELOG.md
packages/auth/CHANGELOG.md
packages/docker-swarm/CHANGELOG.md
packages/sqlite-wrapper/CHANGELOG.md
packages/repo-cli/CHANGELOG.md
packages/template-renderer/CHANGELOG.md
packages/bun-docker/CHANGELOG.md
packages/errors/CHANGELOG.md
packages/logger/CHANGELOG.md
packages/outline-sync/CHANGELOG.md
packages/typings/CHANGELOG.md
packages/utils/CHANGELOG.md
packages/bun-docker/.npmignore
packages/db/.npmignore
packages/docker-client/.npmignore
packages/docker-swarm/.npmignore
packages/outline-sync/.npmignore
packages/ui/.npmignore
.changeset/short-eggs-begin.md
apps/dockstat/src/hooks/eden/helper.ts
apps/dockstat/src/hooks/eden/types.ts
apps/dockstat/src/hooks/eden/useBaseEdenMutation.ts
apps/dockstat/src/hooks/eden/useEdenMutation.ts
apps/dockstat/src/hooks/eden/useEdenRouteMutation.ts
packages/db/build.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 567e0d9f-3324-4696-ba95-0c5357abc789

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-dockstat-docker-exports

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 security issue, 3 other issues, and left some high level feedback:

Security issues:

  • Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections. (link)

General comments:

  • The new useWebSocket hook in apps/dockstat/src/lib/websocketEffects/index.ts looks unfinished (e.g. onUnmount: () => has no implementation and the hook is never exported/used); either complete its cleanup logic and usage or remove it to avoid dead/broken code.
  • In apps/api/src/logger.ts you're using require("./websockets") inside an ESM/Bun module; consider switching to a dynamic import("./websockets") to avoid runtime issues with require not being defined and to keep the module system consistent.
  • The new createTopicSubscription client in topicSubscription.ts builds the WebSocket URL from import.meta.env.DOCKSTAT_API_PORT || 'http://localhost:3030' and manually appends ?token=..., but the server-side handler doesn’t read this token and getAuthHeaders is now unused; it would be safer to derive the base URL from window.location plus the configured API port and align the auth mechanism (or remove the unused header helper) so client and server expectations match.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `useWebSocket` hook in `apps/dockstat/src/lib/websocketEffects/index.ts` looks unfinished (e.g. `onUnmount: () =>` has no implementation and the hook is never exported/used); either complete its cleanup logic and usage or remove it to avoid dead/broken code.
- In `apps/api/src/logger.ts` you're using `require("./websockets")` inside an ESM/Bun module; consider switching to a dynamic `import("./websockets")` to avoid runtime issues with `require` not being defined and to keep the module system consistent.
- The new `createTopicSubscription` client in `topicSubscription.ts` builds the WebSocket URL from `import.meta.env.DOCKSTAT_API_PORT || 'http://localhost:3030'` and manually appends `?token=...`, but the server-side handler doesn’t read this token and `getAuthHeaders` is now unused; it would be safer to derive the base URL from `window.location` plus the configured API port and align the auth mechanism (or remove the unused header helper) so client and server expectations match.

## Individual Comments

### Comment 1
<location path="apps/dockstat/src/lib/websocketEffects/index.ts" line_range="8-17" />
<code_context>
+import {api, getAuthHeaders} from "../api.ts"
+import { useGeneralSettings } from "@/components/settings/general/sections/useGeneralSettings";
+
+const useWebSocket = () => {
+  const settings = useGeneralSettings()
+  const [wsData, setWsData] = useState()
+
+  useEffect(() => {
+    const ws = api.ws.subscribe({ headers: getAuthHeaders() })
+    ws.subscribe((msg) => {
+      setWsData(JSON.parse(String(msg.data)))
+    })
+  }, [])
+
+  return {
+    wsData,
+    onUnmount: () =>
+  }
+}
</code_context>
<issue_to_address>
**issue (bug_risk):** The new `useWebSocket` hook is syntactically incomplete and never cleans up the WebSocket connection.

`onUnmount: () =>` is currently invalid syntax and will stop this file from compiling. Please either implement `onUnmount` or return a cleanup function from `useEffect` that closes/unsubscribes the WebSocket. Also, either remove the unused `useGeneralSettings` dependency or add it to the `useEffect` dependency array if it affects the connection configuration.
</issue_to_address>

### Comment 2
<location path="apps/api/src/logger.ts" line_range="4-10" />
<code_context>
-import { logClients } from "./websockets/logSocket"
+
+// Lazy load WebSocket handler to avoid circular dependency
+let wsHandler: typeof import("./websockets").DSWebSockerHandler | null = null

 const BaseLogger = new Logger("DockStatAPI", [], (entry) => {
-  for (const client of logClients) {
+  // Lazy load and send logs to WebSocket
+  if (!wsHandler) {
     try {
-      client.send(entry, true)
-    } catch (err) {
-      BaseLogger.error(`Failed to send log to client - ${JSON.stringify(err)}`)
+      wsHandler = require("./websockets").DSWebSockerHandler
+    } catch (error) {
+      // WebSocket handler not available yet, will retry on next log
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `require` in an ESM/TypeScript module can break at runtime; prefer a dynamic import for lazy-loading.

Since this is an ESM project (`"type": "module"`), using CommonJS `require` here can throw at runtime in Node/Bun unless you set up `createRequire`. To keep lazy loading and avoid circular imports, use a dynamic import instead, e.g. `const { DSWebSockerHandler } = await import("./websockets")` (or inside the callback). You can then type `wsHandler` as `WebSocketHandler | null` rather than `typeof import("./websockets").DSWebSockerHandler` for clearer, accurate typing.
</issue_to_address>

### Comment 3
<location path="apps/dockstat/src/lib/websocketEffects/topicSubscription.ts" line_range="34" />
<code_context>
+ * @param onData - Callback function called when data is received for this topic
+ * @returns Cleanup function to unsubscribe and close the connection
+ */
+export function createTopicSubscription<T>(
+  topic: string,
+  onData: (data: T) => void
</code_context>
<issue_to_address>
**issue (complexity):** Consider splitting URL/auth construction and reconnection logic into reusable WebSocket utilities so this helper focuses only on topic-specific behavior.

You can keep the same behavior but reduce complexity by separating “connection management” from “topic subscription” and by centralizing URL/auth construction.

### 1. Centralize URL & auth handling

Instead of re-implementing URL + token logic inside this helper, delegate to a small, reusable utility so other WebSocket consumers can share it:

```ts
// websocketClient.ts
import { getAuthHeaders } from "../api";

export function buildWebSocketUrl(path: string): string {
  const baseUrl = import.meta.env.DOCKSTAT_API_PORT || "http://localhost:3030";
  const wsBase = baseUrl.replace("http://", "ws://").replace("https://", "wss://");

  const headers = getAuthHeaders(); // already knows how to read token
  const token = headers.Authorization?.replace("Bearer ", "");

  const query = token ? `?token=${encodeURIComponent(token)}` : "";
  return `${wsBase}${path}${query}`;
}
```

Then your helper becomes simpler and avoids duplicating auth concerns:

```ts
import { buildWebSocketUrl } from "./websocketClient";

export function createTopicSubscription<T>(
  topic: string,
  onData: (data: T) => void
): () => void {
  const wsUrlWithAuth = buildWebSocketUrl("/ws");
  // ...rest unchanged...
}
```

### 2. Extract the reconnection/state machine into a small client

Right now `createTopicSubscription` owns flags + reconnect logic for each call. You can move that into a small WebSocket client and keep `createTopicSubscription` focused on topic filtering:

```ts
// websocketClient.ts
type MessageHandler = (event: MessageEvent) => void;

export function createReconnectingWebSocket(
  url: string,
  onMessage: MessageHandler
) {
  let ws: WebSocket | null = null;
  let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
  let closed = false;

  const connect = () => {
    ws = new WebSocket(url);

    ws.onopen = () => {/* no topic logic here */};
    ws.onmessage = onMessage;

    ws.onclose = () => {
      if (!closed) {
        reconnectTimeout = setTimeout(connect, 3000);
      }
    };

    ws.onerror = console.error;
  };

  connect();

  return {
    send(data: unknown) {
      if (ws?.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify(data));
      }
    },
    close() {
      closed = true;
      if (reconnectTimeout) clearTimeout(reconnectTimeout);
      ws?.close();
    },
  };
}
```

Now `createTopicSubscription` can be a thin wrapper:

```ts
// topicSubscription.ts
import { buildWebSocketUrl, createReconnectingWebSocket } from "./websocketClient";

export function createTopicSubscription<T>(
  topic: string,
  onData: (data: T) => void
): () => void {
  const client = createReconnectingWebSocket(buildWebSocketUrl("/ws"), (event) => {
    try {
      const message: ServerMessage = JSON.parse(event.data);
      if (message.topic === topic) onData(message.data as T);
    } catch (err) {
      console.error(`Failed to parse WebSocket message for topic "${topic}":`, err);
    }
  });

  client.send({ type: "subscribe", topic } satisfies ClientMessage);

  return () => {
    client.send({ type: "unsubscribe", topic } satisfies ClientMessage);
    client.close();
  };
}
```

This keeps your public API identical (`createTopicSubscription` returning a cleanup function) but:

- Connection/reconnect/auth logic is reusable and centralized.
- Topic-specific code is small and easy to reason about.
- Future multi-topic / shared-connection support can be added inside `websocketClient` without touching consumers.
</issue_to_address>

### Comment 4
<location path="apps/dockstat/src/lib/websocketEffects/topicSubscription.ts" line_range="40" />
<code_context>
  const wsUrl = baseUrl.replace("http://", "ws://").replace("https://", "wss://")
</code_context>
<issue_to_address>
**security (javascript.lang.security.detect-insecure-websocket):** Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +8 to +17
const useWebSocket = () => {
const settings = useGeneralSettings()
const [wsData, setWsData] = useState()

useEffect(() => {
const ws = api.ws.subscribe({ headers: getAuthHeaders() })
ws.subscribe((msg) => {
setWsData(JSON.parse(String(msg.data)))
})
}, [])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The new useWebSocket hook is syntactically incomplete and never cleans up the WebSocket connection.

onUnmount: () => is currently invalid syntax and will stop this file from compiling. Please either implement onUnmount or return a cleanup function from useEffect that closes/unsubscribes the WebSocket. Also, either remove the unused useGeneralSettings dependency or add it to the useEffect dependency array if it affects the connection configuration.

Comment thread apps/api/src/logger.ts
Comment on lines +4 to +10
let wsHandler: typeof import("./websockets").DSWebSockerHandler | null = null

const BaseLogger = new Logger("DockStatAPI", [], (entry) => {
for (const client of logClients) {
// Lazy load and send logs to WebSocket
if (!wsHandler) {
try {
client.send(entry, true)
} catch (err) {
BaseLogger.error(`Failed to send log to client - ${JSON.stringify(err)}`)
wsHandler = require("./websockets").DSWebSockerHandler

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Using require in an ESM/TypeScript module can break at runtime; prefer a dynamic import for lazy-loading.

Since this is an ESM project ("type": "module"), using CommonJS require here can throw at runtime in Node/Bun unless you set up createRequire. To keep lazy loading and avoid circular imports, use a dynamic import instead, e.g. const { DSWebSockerHandler } = await import("./websockets") (or inside the callback). You can then type wsHandler as WebSocketHandler | null rather than typeof import("./websockets").DSWebSockerHandler for clearer, accurate typing.

* @param onData - Callback function called when data is received for this topic
* @returns Cleanup function to unsubscribe and close the connection
*/
export function createTopicSubscription<T>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider splitting URL/auth construction and reconnection logic into reusable WebSocket utilities so this helper focuses only on topic-specific behavior.

You can keep the same behavior but reduce complexity by separating “connection management” from “topic subscription” and by centralizing URL/auth construction.

1. Centralize URL & auth handling

Instead of re-implementing URL + token logic inside this helper, delegate to a small, reusable utility so other WebSocket consumers can share it:

// websocketClient.ts
import { getAuthHeaders } from "../api";

export function buildWebSocketUrl(path: string): string {
  const baseUrl = import.meta.env.DOCKSTAT_API_PORT || "http://localhost:3030";
  const wsBase = baseUrl.replace("http://", "ws://").replace("https://", "wss://");

  const headers = getAuthHeaders(); // already knows how to read token
  const token = headers.Authorization?.replace("Bearer ", "");

  const query = token ? `?token=${encodeURIComponent(token)}` : "";
  return `${wsBase}${path}${query}`;
}

Then your helper becomes simpler and avoids duplicating auth concerns:

import { buildWebSocketUrl } from "./websocketClient";

export function createTopicSubscription<T>(
  topic: string,
  onData: (data: T) => void
): () => void {
  const wsUrlWithAuth = buildWebSocketUrl("/ws");
  // ...rest unchanged...
}

2. Extract the reconnection/state machine into a small client

Right now createTopicSubscription owns flags + reconnect logic for each call. You can move that into a small WebSocket client and keep createTopicSubscription focused on topic filtering:

// websocketClient.ts
type MessageHandler = (event: MessageEvent) => void;

export function createReconnectingWebSocket(
  url: string,
  onMessage: MessageHandler
) {
  let ws: WebSocket | null = null;
  let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
  let closed = false;

  const connect = () => {
    ws = new WebSocket(url);

    ws.onopen = () => {/* no topic logic here */};
    ws.onmessage = onMessage;

    ws.onclose = () => {
      if (!closed) {
        reconnectTimeout = setTimeout(connect, 3000);
      }
    };

    ws.onerror = console.error;
  };

  connect();

  return {
    send(data: unknown) {
      if (ws?.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify(data));
      }
    },
    close() {
      closed = true;
      if (reconnectTimeout) clearTimeout(reconnectTimeout);
      ws?.close();
    },
  };
}

Now createTopicSubscription can be a thin wrapper:

// topicSubscription.ts
import { buildWebSocketUrl, createReconnectingWebSocket } from "./websocketClient";

export function createTopicSubscription<T>(
  topic: string,
  onData: (data: T) => void
): () => void {
  const client = createReconnectingWebSocket(buildWebSocketUrl("/ws"), (event) => {
    try {
      const message: ServerMessage = JSON.parse(event.data);
      if (message.topic === topic) onData(message.data as T);
    } catch (err) {
      console.error(`Failed to parse WebSocket message for topic "${topic}":`, err);
    }
  });

  client.send({ type: "subscribe", topic } satisfies ClientMessage);

  return () => {
    client.send({ type: "unsubscribe", topic } satisfies ClientMessage);
    client.close();
  };
}

This keeps your public API identical (createTopicSubscription returning a cleanup function) but:

  • Connection/reconnect/auth logic is reusable and centralized.
  • Topic-specific code is small and easy to reason about.
  • Future multi-topic / shared-connection support can be added inside websocketClient without touching consumers.

): () => void {
// Get the base API URL and convert to WebSocket protocol
const baseUrl = import.meta.env.DOCKSTAT_API_PORT || `http://localhost:3030`
const wsUrl = baseUrl.replace("http://", "ws://").replace("https://", "wss://")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (javascript.lang.security.detect-insecure-websocket): Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.

Source: opengrep

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New security issues found

): () => void {
// Get the base API URL and convert to WebSocket protocol
const baseUrl = import.meta.env.DOCKSTAT_API_PORT || `http://localhost:3030`
const wsUrl = baseUrl.replace("http://", "ws://").replace("https://", "wss://")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (javascript.lang.security.detect-insecure-websocket): Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.

Source: opengrep

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant