Skip to content

Implement WebSocket enhancements with backoff and validation features#16

Merged
sebamar88 merged 2 commits into
mainfrom
005-websocket-advanced
Mar 29, 2026
Merged

Implement WebSocket enhancements with backoff and validation features#16
sebamar88 merged 2 commits into
mainfrom
005-websocket-advanced

Conversation

@sebamar88
Copy link
Copy Markdown
Owner

Description

Related Issue

Fixes #(issue number)

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update
  • 🔧 Chore (refactoring, tooling, dependencies)

Changes Made

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • All tests passing
  • Manual testing completed

Coverage

Coverage: XX%

Screenshots

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Breaking Changes

Additional Notes

…etection

- Add BackoffStrategy, WebSocketReconnectHandler, WebSocketMaxRetriesHandler,
  WebSocketValidationErrorHandler types
- Extend WebSocketOptions: backoffStrategy, maxReconnectDelayMs, jitter,
  heartbeatTimeoutMs, schemas
- Add computeDelay() for linear/exponential/custom backoff with full jitter
- Upgrade attemptReconnect() to fire onReconnect/onMaxRetriesReached handlers
- Add notifyValidationError() + schema lookup in handleMessage() (US2)
- Add pong timeout timer in startHeartbeat()/stopHeartbeat()/handleMessage() (US3)
- Add onReconnect(), onMaxRetriesReached(), onValidationError() public methods
- 30 tests (15 existing + 15 new) — 654 total passing, 0 failures
- Coverage: statements 95.48%, functions 100%, lines 95.48%, branches 89.41%
- Add websocket-advanced-example.ts with 5 usage examples
- Update bytekit.wiki/WebSocketHelper.md and docs/guides/REALTIME.md

Closes T001–T033 in specs/005-websocket-advanced/tasks.md
@sebamar88 sebamar88 merged commit 5f4a40f into main Mar 29, 2026
7 of 13 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 752cfcb.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

None

@github-actions
Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

Total dist size: 1.2M

View detailed breakdown
4.0K	dist/api-client.d.ts
4.0K	dist/api-client.d.ts.map
4.0K	dist/api-client.js
4.0K	dist/api-client.js.map
112K	dist/cli
4.0K	dist/debug.d.ts
4.0K	dist/debug.d.ts.map
4.0K	dist/debug.js
4.0K	dist/debug.js.map
4.0K	dist/env-manager.d.ts
4.0K	dist/env-manager.d.ts.map
4.0K	dist/env-manager.js
4.0K	dist/env-manager.js.map
4.0K	dist/file-upload.d.ts
4.0K	dist/file-upload.d.ts.map
4.0K	dist/file-upload.js
4.0K	dist/file-upload.js.map
4.0K	dist/index.d.ts
4.0K	dist/index.d.ts.map
4.0K	dist/index.js
4.0K	dist/index.js.map
4.0K	dist/logger.d.ts
4.0K	dist/logger.d.ts.map
4.0K	dist/logger.js
4.0K	dist/logger.js.map
4.0K	dist/profiler.d.ts
4.0K	dist/profiler.d.ts.map
4.0K	dist/profiler.js
4.0K	dist/profiler.js.map
4.0K	dist/response-validator.d.ts
4.0K	dist/response-validator.d.ts.map
4.0K	dist/response-validator.js
4.0K	dist/response-validator.js.map
4.0K	dist/retry-policy.d.ts
4.0K	dist/retry-policy.d.ts.map
4.0K	dist/retry-policy.js
4.0K	dist/retry-policy.js.map
4.0K	dist/storage-utils.d.ts
4.0K	dist/storage-utils.d.ts.map
4.0K	dist/storage-utils.js
4.0K	dist/storage-utils.js.map
4.0K	dist/streaming.d.ts
4.0K	dist/streaming.d.ts.map
4.0K	dist/streaming.js
4.0K	dist/streaming.js.map
300K	dist/utils/core
324K	dist/utils/helpers
224K	dist/utils/async
868K	dist/utils
4.0K	dist/websocket.d.ts
4.0K	dist/websocket.d.ts.map
4.0K	dist/websocket.js
4.0K	dist/websocket.js.map

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot

See analysis details on SonarQube Cloud

@github-actions
Copy link
Copy Markdown
Contributor

📊 Code Coverage Report

Coverage: 87.38% ✅

Great coverage!

View full coverage report

// Example 1: Exponential backoff with onReconnect / onMaxRetriesReached
// ─────────────────────────────────────────────────────────────────────────────

async function example1_smartReconnection() {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused function example1_smartReconnection.

Copilot Autofix

AI 2 months ago

In general, an unused function should either be removed (if it is truly dead code) or have its usage made explicit (by calling or exporting it). Since this is an example function in an example file, removing it would defeat its purpose. The best non‑breaking fix is to mark it as an exported function so that it is clearly part of the public API of this example module, making it “used” from the perspective of tools that analyze across files.

Concretely, in examples/websocket-advanced-example.ts, change the declaration on line 7 from async function example1_smartReconnection() to export async function example1_smartReconnection(). No new imports or other definitions are required. This preserves all existing behavior inside the function and simply allows other files or tooling to import and run the example, which resolves the “unused function” finding.

Suggested changeset 1
examples/websocket-advanced-example.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/websocket-advanced-example.ts b/examples/websocket-advanced-example.ts
--- a/examples/websocket-advanced-example.ts
+++ b/examples/websocket-advanced-example.ts
@@ -4,7 +4,7 @@
 // Example 1: Exponential backoff with onReconnect / onMaxRetriesReached
 // ─────────────────────────────────────────────────────────────────────────────
 
-async function example1_smartReconnection() {
+export async function example1_smartReconnection() {
     const ws = new WebSocketHelper("wss://api.example.com/stream", {
         reconnect: true,
         maxReconnectAttempts: 8,
EOF
@@ -4,7 +4,7 @@
// Example 1: Exponential backoff with onReconnect / onMaxRetriesReached
// ─────────────────────────────────────────────────────────────────────────────

async function example1_smartReconnection() {
export async function example1_smartReconnection() {
const ws = new WebSocketHelper("wss://api.example.com/stream", {
reconnect: true,
maxReconnectAttempts: 8,
Copilot is powered by AI and may make mistakes. Always verify output.
});

// Observe the reconnect lifecycle — no string-matching on error messages needed
const unsubReconnect = ws.onReconnect((attempt, delay) => {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable unsubReconnect.

Copilot Autofix

AI 2 months ago

In general, unused variables should be removed or, if they represent necessary handles, actually used. Here, unsubReconnect is only referenced in commented-out code, so the best minimal fix is to stop assigning the result of ws.onReconnect(...) to a variable and instead either (a) call it without capturing the return value, or (b) also comment out the declaration line. To preserve the example’s instructional value and existing functionality, we should keep the onReconnect registration (since it has side effects) but drop the unused variable binding.

Concretely, in examples/websocket-advanced-example.ts, within example1_smartReconnection, replace the line that declares const unsubReconnect = ws.onReconnect(...) with a bare call ws.onReconnect(...). This keeps the behavior identical (the reconnect handler is still registered) while removing the unused variable. No imports, methods, or new definitions are required.

Suggested changeset 1
examples/websocket-advanced-example.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/websocket-advanced-example.ts b/examples/websocket-advanced-example.ts
--- a/examples/websocket-advanced-example.ts
+++ b/examples/websocket-advanced-example.ts
@@ -17,7 +17,7 @@
     });
 
     // Observe the reconnect lifecycle — no string-matching on error messages needed
-    const unsubReconnect = ws.onReconnect((attempt, delay) => {
+    ws.onReconnect((attempt, delay) => {
         console.log(`⟳ Reconnecting… attempt ${attempt} in ${delay}ms`);
     });
 
EOF
@@ -17,7 +17,7 @@
});

// Observe the reconnect lifecycle — no string-matching on error messages needed
const unsubReconnect = ws.onReconnect((attempt, delay) => {
ws.onReconnect((attempt, delay) => {
console.log(`⟳ Reconnecting… attempt ${attempt} in ${delay}ms`);
});

Copilot is powered by AI and may make mistakes. Always verify output.
console.log(`⟳ Reconnecting… attempt ${attempt} in ${delay}ms`);
});

const unsubMaxRetries = ws.onMaxRetriesReached(() => {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable unsubMaxRetries.

Copilot Autofix

AI 2 months ago

In general, unused variables should either be removed or their associated value should be used. Here, unsubMaxRetries and unsubReconnect are subscription handles that are meant to be called during cleanup but their calls are commented out, making both variables unused. The cleanest fix that preserves intended functionality is to turn the commented-out cleanup lines into real code, so the unsubscribe callbacks are actually invoked and the variables are read.

Concretely, within examples/websocket-advanced-example.ts, in example1_smartReconnection, change the “Cleanup” section at the bottom from comments into executable statements. This will read and invoke unsubReconnect and unsubMaxRetries, satisfying the rule and aligning behavior with the comment’s intention. No new imports or types are required, and no existing imports need to change.

Suggested changeset 1
examples/websocket-advanced-example.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/websocket-advanced-example.ts b/examples/websocket-advanced-example.ts
--- a/examples/websocket-advanced-example.ts
+++ b/examples/websocket-advanced-example.ts
@@ -38,9 +38,9 @@
     ws.send("subscribe", { channel: "prices" });
 
     // Cleanup
-    // ws.close();
-    // unsubReconnect();
-    // unsubMaxRetries();
+    ws.close();
+    unsubReconnect();
+    unsubMaxRetries();
 }
 
 // ─────────────────────────────────────────────────────────────────────────────
EOF
@@ -38,9 +38,9 @@
ws.send("subscribe", { channel: "prices" });

// Cleanup
// ws.close();
// unsubReconnect();
// unsubMaxRetries();
ws.close();
unsubReconnect();
unsubMaxRetries();
}

// ─────────────────────────────────────────────────────────────────────────────
Copilot is powered by AI and may make mistakes. Always verify output.
// Example 2: Custom backoff function (Fibonacci-style)
// ─────────────────────────────────────────────────────────────────────────────

async function example2_customBackoff() {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused function example2_customBackoff.

Copilot Autofix

AI 2 months ago

In general, to fix an “unused function” warning you either remove the function (if it is truly dead code) or make its usage explicit (by calling or exporting it) if it is meant to be part of the API or examples. Removing it entirely is risky in an examples file because it likely serves documentation value even if not currently referenced.

The best fix here, without changing existing behavior, is to mark example2_customBackoff as an exported function so that it becomes part of the module’s public API, similar to how example helpers are often exposed. This change does not alter any current behavior (no existing code paths are modified) but makes the function a legitimate, potentially used symbol from the module’s perspective, addressing the “unused function” warning. Concretely, in examples/websocket-advanced-example.ts, change the declaration on line 50 from async function example2_customBackoff() to export async function example2_customBackoff(). No additional imports or helpers are required.

Suggested changeset 1
examples/websocket-advanced-example.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/websocket-advanced-example.ts b/examples/websocket-advanced-example.ts
--- a/examples/websocket-advanced-example.ts
+++ b/examples/websocket-advanced-example.ts
@@ -47,7 +47,7 @@
 // Example 2: Custom backoff function (Fibonacci-style)
 // ─────────────────────────────────────────────────────────────────────────────
 
-async function example2_customBackoff() {
+export async function example2_customBackoff() {
     const fibonacci = [1000, 1000, 2000, 3000, 5000, 8000, 13000];
 
     const ws = new WebSocketHelper("wss://api.example.com/stream", {
EOF
@@ -47,7 +47,7 @@
// Example 2: Custom backoff function (Fibonacci-style)
// ─────────────────────────────────────────────────────────────────────────────

async function example2_customBackoff() {
export async function example2_customBackoff() {
const fibonacci = [1000, 1000, 2000, 3000, 5000, 8000, 13000];

const ws = new WebSocketHelper("wss://api.example.com/stream", {
Copilot is powered by AI and may make mistakes. Always verify output.
// from "bytekit/core" — both produce a SchemaAdapter that can be used here.
// ─────────────────────────────────────────────────────────────────────────────

async function example3_schemaValidation() {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused function example3_schemaValidation.

Copilot Autofix

AI 2 months ago

In general, to fix an unused-function warning you either remove the function entirely or ensure it is used (called or exported). Since this appears to be a worked example that should remain available, the best non‑disruptive fix is to convert example3_schemaValidation into an immediately invoked async function expression (an IIFE). That way, no named function is left unused, but the example logic (schema definition, WebSocket setup, and connect() call) still executes.

Concretely, in examples/websocket-advanced-example.ts, lines 74–120 should be refactored so that:

  • The async function example3_schemaValidation() { ... } declaration is replaced by an async arrow function wrapped in parentheses and immediately invoked:
    (async function example3_schemaValidation() { ... })();
    or, more idiomatically for examples:
    (async () => { ... })();
  • All the inner code (the strictSchema function, Trade type, tradeSchema, ws setup, event handlers, and await ws.connect();) remains unchanged inside that IIFE.
  • No new imports or external dependencies are needed; we are only changing the way the function is defined and invoked within the same file.

This removes the unused top-level function symbol while preserving the behavior of the example.

Suggested changeset 1
examples/websocket-advanced-example.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/websocket-advanced-example.ts b/examples/websocket-advanced-example.ts
--- a/examples/websocket-advanced-example.ts
+++ b/examples/websocket-advanced-example.ts
@@ -71,7 +71,7 @@
 // from "bytekit/core" — both produce a SchemaAdapter that can be used here.
 // ─────────────────────────────────────────────────────────────────────────────
 
-async function example3_schemaValidation() {
+(async () => {
     // Minimal mock SchemaAdapter (no external dependencies)
     function strictSchema<T>(validator: (data: unknown) => data is T) {
         return {
@@ -117,7 +117,7 @@
     });
 
     await ws.connect();
-}
+})();
 
 // ─────────────────────────────────────────────────────────────────────────────
 // Example 4: Pong detection — detect silent connection drops
EOF
@@ -71,7 +71,7 @@
// from "bytekit/core" — both produce a SchemaAdapter that can be used here.
// ─────────────────────────────────────────────────────────────────────────────

async function example3_schemaValidation() {
(async () => {
// Minimal mock SchemaAdapter (no external dependencies)
function strictSchema<T>(validator: (data: unknown) => data is T) {
return {
@@ -117,7 +117,7 @@
});

await ws.connect();
}
})();

// ─────────────────────────────────────────────────────────────────────────────
// Example 4: Pong detection — detect silent connection drops
Copilot is powered by AI and may make mistakes. Always verify output.
// Example 4: Pong detection — detect silent connection drops
// ─────────────────────────────────────────────────────────────────────────────

async function example4_heartbeatPongDetection() {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused function example4_heartbeatPongDetection.

Copilot Autofix

AI 2 months ago

In general, to fix an unused-function warning in an examples file without removing useful sample code, the best options are either (a) reference the function in a way that makes its purpose clear (e.g., export it so it can be invoked from elsewhere), or (b) explicitly call it from some example runner. Removing the function would satisfy the linter but lose a documented example, which is usually undesirable in an examples file.

The minimal, non‑behavior‑changing fix here is to export the example4_heartbeatPongDetection function (and, ideally, the other example functions too, but the warning is about this one). Turning it into an exported function makes it a public API of the examples module, which counts as “used” from the module’s perspective and allows external callers or tooling to invoke this specific example while preserving existing behavior. Concretely, in examples/websocket-advanced-example.ts, change the declaration on line 126 from async function example4_heartbeatPongDetection() to export async function example4_heartbeatPongDetection(). No new imports or additional definitions are required.

Suggested changeset 1
examples/websocket-advanced-example.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/websocket-advanced-example.ts b/examples/websocket-advanced-example.ts
--- a/examples/websocket-advanced-example.ts
+++ b/examples/websocket-advanced-example.ts
@@ -123,7 +123,7 @@
 // Example 4: Pong detection — detect silent connection drops
 // ─────────────────────────────────────────────────────────────────────────────
 
-async function example4_heartbeatPongDetection() {
+export async function example4_heartbeatPongDetection() {
     const ws = new WebSocketHelper("wss://api.example.com/stream", {
         heartbeatIntervalMs: 15_000, // ping every 15 s
         heartbeatTimeoutMs: 5_000,   // reconnect if no message within 5 s of ping
EOF
@@ -123,7 +123,7 @@
// Example 4: Pong detection — detect silent connection drops
// ─────────────────────────────────────────────────────────────────────────────

async function example4_heartbeatPongDetection() {
export async function example4_heartbeatPongDetection() {
const ws = new WebSocketHelper("wss://api.example.com/stream", {
heartbeatIntervalMs: 15_000, // ping every 15 s
heartbeatTimeoutMs: 5_000, // reconnect if no message within 5 s of ping
Copilot is powered by AI and may make mistakes. Always verify output.
// Example 5: All features combined
// ─────────────────────────────────────────────────────────────────────────────

async function example5_allFeatures() {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused function example5_allFeatures.

Copilot Autofix

AI 2 months ago

To fix an “unused function” finding without changing behavior, you either (1) remove the function if it’s truly dead, (2) ensure it is actually called, or (3) expose it via an export so it becomes part of the module’s API but is not auto-invoked. Here, removing example5_allFeatures would destroy a documented example, and auto-calling it would change behavior. The best fix is to export it, matching the file’s role as an examples module.

Concretely, in examples/websocket-advanced-example.ts, change the definition of example5_allFeatures to be an exported function: export async function example5_allFeatures() { ... }. This is a minimal, one-line change that keeps all existing logic identical, doesn’t introduce side effects, and gives the function a clear use: other code (or tests) can import and run this example explicitly. No additional imports or definitions are required.

Suggested changeset 1
examples/websocket-advanced-example.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/websocket-advanced-example.ts b/examples/websocket-advanced-example.ts
--- a/examples/websocket-advanced-example.ts
+++ b/examples/websocket-advanced-example.ts
@@ -148,7 +148,7 @@
 // Example 5: All features combined
 // ─────────────────────────────────────────────────────────────────────────────
 
-async function example5_allFeatures() {
+export async function example5_allFeatures() {
     // Simple structural validator (replace with zodAdapter/valibotAdapter in production)
     const priceSchema = {
         parse(data: unknown) {
EOF
@@ -148,7 +148,7 @@
// Example 5: All features combined
// ─────────────────────────────────────────────────────────────────────────────

async function example5_allFeatures() {
export async function example5_allFeatures() {
// Simple structural validator (replace with zodAdapter/valibotAdapter in production)
const priceSchema = {
parse(data: unknown) {
Copilot is powered by AI and may make mistakes. Always verify output.
});
const log: string[] = [];
const unsub1 = wsh.onReconnect(() => log.push("h1"));
const unsub2 = wsh.onReconnect(() => log.push("h2"));

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note test

Unused variable unsub2.

Copilot Autofix

AI 2 months ago

In general, to fix an unused variable in tests you either remove the variable if it’s genuinely unnecessary, or use it in an assertion that matches the intended test behavior. Here, the test title explicitly mentions that “each unsubscribes independently,” but only unsub1 is ever invoked, so the best fix is to use unsub2 as well and extend the test to actually verify its independent unsubscription.

Concretely, in tests/websocket-helper.test.ts, within the "US1 [T015] multiple onReconnect subscribers — all notified; each unsubscribes independently" test, we should:

  1. After the first reconnect where both handlers fire and unsub1() is called, perform a second reconnect to confirm only h2 fires (this already happens).
  2. Then call unsub2() and trigger another reconnect to confirm no handlers fire anymore, showing that unsub2 works and eliminating the unused-variable warning. This uses unsub2 in a way that matches the described behavior without altering existing semantics, only strengthening the test.

No new imports or helper functions are needed; we just add a unsub2(); call and an additional reconnect plus assertion using the existing log array and timing pattern.

Suggested changeset 1
tests/websocket-helper.test.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tests/websocket-helper.test.ts b/tests/websocket-helper.test.ts
--- a/tests/websocket-helper.test.ts
+++ b/tests/websocket-helper.test.ts
@@ -513,6 +513,13 @@
     wsh.ws.close();
     assert.deepEqual(log, ["h1", "h2", "h2"]); // only h2 fires
 
+    unsub2();
+    await new Promise((r) => setTimeout(r, 50));
+
+    // @ts-expect-error - Test type override
+    wsh.ws.close();
+    assert.deepEqual(log, ["h1", "h2", "h2"]); // no further handlers fire
+
     wsh.close();
 });
 
EOF
@@ -513,6 +513,13 @@
wsh.ws.close();
assert.deepEqual(log, ["h1", "h2", "h2"]); // only h2 fires

unsub2();
await new Promise((r) => setTimeout(r, 50));

// @ts-expect-error - Test type override
wsh.ws.close();
assert.deepEqual(log, ["h1", "h2", "h2"]); // no further handlers fire

wsh.close();
});

Copilot is powered by AI and may make mistakes. Always verify output.
@github-actions
Copy link
Copy Markdown
Contributor

📊 Code Coverage Report

Coverage: %

❌ Low coverage - please add more tests

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 91.76471% with 7 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/utils/helpers/WebSocketHelper.ts 91.76% 7 Missing ⚠️

📢 Thoughts on this report? Let us know!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants