Skip to content

Comments

refactor: remove websocket-polyfill#531

Open
AkarshSahlot wants to merge 3 commits intogetAlby:masterfrom
AkarshSahlot:remove-websocket-polyfill
Open

refactor: remove websocket-polyfill#531
AkarshSahlot wants to merge 3 commits intogetAlby:masterfrom
AkarshSahlot:remove-websocket-polyfill

Conversation

@AkarshSahlot
Copy link

@AkarshSahlot AkarshSahlot commented Feb 13, 2026

Closes #519

websocket-polyfill is only needed for NodeJS < 22, and it incorrectly overwrites WebSocket even if it already exists on the global object.

This PR:

  • Removes websocket-polyfill dependency
  • Removes imports from tests and examples
  • Updates documentation to remove installation instructions
  • Updates error messages to be generic for missing WebSocket

Summary by CodeRabbit

  • Chores

    • Removed websocket-polyfill dependency from the package.
    • Updated documentation to reflect polyfill removal and guidance for Node.js versions.
    • Updated Jest configuration for ES module support and module resolution settings.
    • Updated internal import paths for improved compatibility.
  • Bug Fixes

    • Improved error handling for missing WebSocket support with clearer error messages.

AkarshSahlot and others added 2 commits February 13, 2026 16:15
The close() method in NWCClient was synchronous, which meant it didn't wait for WebSocket connections to actually close before returning. This caused Jest tests to hang because of open handles.

I made close() async so it waits for the underlying WebSocket capabilities to finish closing. I also updated the dependencies and config to support the latest nostr-tools exports.

Changes:
- Made NWCClient.close() async
- Updated imports to use specific paths from nostr-tools
- Set moduleResolution to bundler in tsconfig
- Updated jest.config.ts to export default
fix: NWCClient.close() hanging Jest tests
@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

📝 Walkthrough

Walkthrough

Removes the websocket-polyfill dependency from the codebase, updating documentation and example files to reflect this change. Updates import paths for nostr-tools modules, changes error handling to throw instead of logging when WebSocket is absent, updates Jest configuration to ES modules, and modifies tsconfig moduleResolution setting.

Changes

Cohort / File(s) Summary
Documentation
README.md, docs/lnclient.md, docs/nwc.md
Removed Node.js-specific websocket-polyfill import instructions and code snippets. Replaced with concise guidance for Node.js < v22, keeping existing crypto handling notes.
Example Files (lnclient)
examples/lnclient/pay_ln_address.ts, examples/lnclient/paywall.ts, examples/lnclient/splitter.ts
Removed top-level websocket-polyfill imports. No other logic changes.
Example Files (nwc)
examples/nwc/client/*, examples/nwc/get-*.ts, examples/nwc/keysend.ts, examples/nwc/list-transactions.ts, examples/nwc/lookup-invoice.ts, examples/nwc/make-invoice.ts, examples/nwc/multi-*.ts, examples/nwc/send-*.ts, examples/nwc/sign-message.ts, examples/nwc/wallet-service/example.ts
Removed websocket-polyfill imports from 28+ example files. No control flow or logic changes.
Test Files
src/nwc/NWAClient.test.ts, src/nwc/NWCClient.test.ts, src/oauth/AlbyResponseError.test.ts
Removed websocket-polyfill imports from test suite files.
Source Code (Import Updates & Error Handling)
src/nwc/NWAClient.ts, src/nwc/NWCClient.ts, src/nwc/NWCWalletService.ts
Updated imports of SubCloser/Subscription from nostr-tools/lib/types/* to nostr-tools/*. Changed WebSocket absence error handling from console.error logging to throwing descriptive Error.
Source Code (Method Signature)
src/nwc/NWCClient.ts
Made close() method async (async close(): Promise<void>). Enhanced logic to inspect relays, await WebSocket close events, and properly clean up connections before pool closure.
Configuration & Dependencies
jest.config.ts, package.json, tsconfig.json
Removed websocket-polyfill from devDependencies. Migrated Jest config from CommonJS to ES module syntax with ts-jest transform. Changed tsconfig moduleResolution from "node" to "bundler".

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Out goes the polyfill, no longer needed here,
Modern WebSockets stand native and clear,
Node v22 brings them built-in at last,
Cleaner imports mean janky days past! ✨
Async close methods await their farewell—
Hopping toward futures where globals work well! 🌙

🚥 Pre-merge checks | ✅ 5 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes some changes beyond issue #519 scope: jest.config.ts migration to ES modules, tsconfig.json moduleResolution change, and NWCClient.close() async enhancement appear unrelated to websocket-polyfill removal. Separate the unrelated changes (Jest config, tsconfig moduleResolution, NWCClient.close async refactor) into distinct PRs aligned with their specific issues or objectives.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'refactor: remove websocket-polyfill' directly and clearly summarizes the main change: removing the websocket-polyfill dependency from the project.
Linked Issues check ✅ Passed The PR successfully addresses issue #519 by removing websocket-polyfill dependency, removing imports from tests/examples, updating documentation, and updating error messages to be generic.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

- Removes websocket-polyfill dependency
- Removes imports from tests and examples
- Updates documentation to remove installation instructions
- Updates error messages to be generic for missing WebSocket
@rolznz
Copy link
Contributor

rolznz commented Feb 13, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

close() {
return this.pool.close(this.relayUrls);

async close(): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

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

these changes are unrelated to this PR

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@jest.config.ts`:
- Around line 2-7: The Jest config is missing extensionsToTreatAsEsm which
causes a mismatch when using ts-jest with useESM: true; update the exported
config object to include extensionsToTreatAsEsm: ['.ts'] alongside
preset/testEnvironment and the transform entry so Jest treats .ts files as ESM
(modify the object containing transform and the ts-jest { useESM: true } option
to add the extensionsToTreatAsEsm key).

In `@src/nwc/NWCClient.ts`:
- Around line 203-218: The close() method currently reaches into SimplePool
internals via (this.pool as any).relays to await relay.ws "close" events but
doesn’t guard against missing events; update close() to wrap each close promise
with a timeout (e.g., reject/resolve after configurable ms) so
Promise.all(closePromises) cannot hang indefinitely, and ensure the timeout
value is configurable/constant. Also add an inline comment above the use of
(this.pool as any).relays documenting that this is a deliberate workaround for
nostr-tools' synchronous pool.close() (since pool.close(relayUrls) only
initiates closes and does not await them) and mark reliance on the private API
as fragile so future maintainers know why it’s used. Ensure references: close(),
relayUrls, this.pool/(SimplePool).relays, closePromises, and WebSocket close
event are updated accordingly.
🧹 Nitpick comments (3)
package.json (1)

94-96: Consider updating engines to reflect actual minimum Node.js requirements.

The engines field still specifies "node": ">=14", but the SDK already requires fetch (Node ≥ 18) and now expects a global WebSocket (native in Node ≥ 22, or user-provided polyfill). This could mislead users on older Node versions. Consider bumping to >=18 at minimum.

README.md (1)

86-87: Minor text clarity suggestion.

The current phrasing "We recommend websocket-polyfill but checking your node version is preferred" reads a bit ambiguously. Consider rephrasing for clarity:

📝 Suggested wording
 #### WebSocket Polyfill
-If you are using Node.js < v22, you may need a WebSocket polyfill. We recommend `websocket-polyfill` but checking your node version is preferred.
+If you are using Node.js < v22, you may need a WebSocket polyfill such as `websocket-polyfill`. Consider upgrading to Node.js v22+ where WebSocket is available natively.
src/nwc/NWCWalletService.ts (1)

60-66: Constructor creates Relay before the WebSocket check — consider reordering.

new Relay(this.relayUrl) on line 60 is called before verifying globalThis.WebSocket on line 62. If the Relay constructor internally references WebSocket (e.g., for validation or eager setup), this could throw a less informative error before your guard is reached. Swapping the order ensures the developer-friendly message is always shown first.

Suggested reorder
   constructor(options: NewNWCWalletServiceOptions) {
     this.relayUrl = options.relayUrl;
 
+    if (globalThis.WebSocket === undefined) {
+      throw new Error(
+        "WebSocket is undefined. Make sure to polyfill it for nodejs environments",
+      );
+    }
+
     this.relay = new Relay(this.relayUrl);
 
-    if (globalThis.WebSocket === undefined) {
-      throw new Error(
-        "WebSocket is undefined. Make sure to polyfill it for nodejs environments",
-      );
-    }
   }

Comment on lines +2 to +7
export default {
preset: "ts-jest",
testEnvironment: "node",
transform: {
"^.+\\.tsx?$": ["ts-jest", { useESM: true }],
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check full jest config
echo "=== jest.config.ts ===" 
cat jest.config.ts 2>/dev/null || echo "File not found"

echo ""
echo "=== package.json (test script and ts-jest version) ==="
cat package.json | grep -A5 '"test"' || echo "Not found"
echo ""
cat package.json | grep -A2 '"ts-jest"' || echo "Not found"

echo ""
echo "=== Searching for extensionsToTreatAsEsm ==="
rg -n "extensionsToTreatAsEsm" 2>/dev/null || echo "Not found"

echo ""
echo "=== Searching for experimental-vm-modules ==="
rg -n "experimental-vm-modules" 2>/dev/null || echo "Not found"

Repository: getAlby/js-sdk

Length of output: 650


🌐 Web query:

ts-jest useESM configuration extensionsToTreatAsEsm requirements 2025

💡 Result:

When using ts-jest in ESM mode (useESM: true), you generally must also tell Jest to treat TypeScript files as native ESM via extensionsToTreatAsEsm, otherwise Jest may still execute .ts as CJS semantics. [1][2]

Minimal working pattern (no preset):

// jest.config.ts
import type { Config } from 'jest'

export default {
  extensionsToTreatAsEsm: ['.ts'],           // (often also '.tsx' if you use it)
  transform: {
    '^.+\\.tsx?$': ['ts-jest', { useESM: true }],
  },
} satisfies Config

Key requirements / notes:

  • extensionsToTreatAsEsm should include .ts (and .tsx if applicable) when you want those files to run as ESM in Jest. [1][2]
  • useESM: true is the ts-jest switch that makes ts-jest emit/transform to ESM where possible; it’s separate from Jest’s extensionsToTreatAsEsm toggle. [3][2]
  • If you use ts-jest’s ESM helpers, you can use their provided constants/patterns (e.g., TS_EXT_TO_TREAT_AS_ESM, ESM_TS_TRANSFORM_PATTERN) instead of hardcoding extensions/regex. [1]
  • For .mts / .mjs-style setups, ts-jest documents additional requirements such as having package.json include "type": "module", and sometimes a custom resolver for .mjs.mts resolution. [1]

Add extensionsToTreatAsEsm to the Jest configuration.

When using ts-jest with useESM: true, you must also configure extensionsToTreatAsEsm: ['.ts'] in the Jest config. Without it, Jest will still execute .ts files as CommonJS, creating a mismatch between ts-jest's ESM transformation and Jest's runtime expectations.

Add to jest.config.ts:

export default {
  preset: "ts-jest",
  testEnvironment: "node",
  extensionsToTreatAsEsm: ['.ts'],
  transform: {
    "^.+\\.tsx?$": ["ts-jest", { useESM: true }],
  },
};
🤖 Prompt for AI Agents
In `@jest.config.ts` around lines 2 - 7, The Jest config is missing
extensionsToTreatAsEsm which causes a mismatch when using ts-jest with useESM:
true; update the exported config object to include extensionsToTreatAsEsm:
['.ts'] alongside preset/testEnvironment and the transform entry so Jest treats
.ts files as ESM (modify the object containing transform and the ts-jest {
useESM: true } option to add the extensionsToTreatAsEsm key).

Comment on lines +203 to +218
async close(): Promise<void> {
const closePromises: Promise<void>[] = [];
for (const url of this.relayUrls) {
const relayUrl = normalizeURL(url);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const relay = (this.pool as any).relays.get(relayUrl);
if (relay?.ws && relay.ws.readyState !== WebSocket.CLOSED) {
closePromises.push(
new Promise<void>((resolve) => {
relay.ws?.addEventListener("close", () => resolve(), { once: true });
}),
);
}
}
this.pool.close(this.relayUrls);
await Promise.all(closePromises);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check if SimplePool is from nostr-tools and understand its type
fd "pool" --type ts | head -20

Repository: getAlby/js-sdk

Length of output: 227


🏁 Script executed:

# Search for SimplePool type definitions and usage in the codebase
rg "SimplePool" --type ts -B 2 -A 2

Repository: getAlby/js-sdk

Length of output: 1430


🏁 Script executed:

# Check imports to understand where SimplePool comes from
rg "import.*SimplePool\|from.*nostr" --type ts | head -20

Repository: getAlby/js-sdk

Length of output: 40


🏁 Script executed:

# Look for any type definitions or interfaces for pool
ast-grep --pattern 'interface.*Pool {
  $$$
}' | head -50

Repository: getAlby/js-sdk

Length of output: 40


🌐 Web query:

nostr-tools SimplePool public API relay close

💡 Result:

In nostr-tools, the pool’s “relay close” API works like this:

  • Close (disconnect) specific relay connections managed by the pool:
    pool.close(relayUrls) closes only the relays whose URLs you pass in, and removes them from the pool’s internal relay map. Example:

    pool.close(['wss://relay.example.com'])

    (Implementation: normalize URL → relay.close() → delete from map.) [2]

  • Close a Relay directly (if you created/obtained a Relay instance):
    The README shows calling:

    relay.close()

    to close that relay’s websocket connection. [1]

  • Close all relays in the pool:
    pool.close([]) closes none (empty list). If you want to disconnect everything, use:

    pool.destroy()

    which iterates all connected relays, closes them, and resets the pool’s relay map. [2]
    (The README snippet that calls pool.close([]) is just passing an empty list—i.e., it won’t disconnect relays.) [1]

Sources: nostr-tools README examples [1], AbstractSimplePool implementation (close, destroy) [2].


🏁 Script executed:

rg "pool\.close|relay\.close" --type=typescript -B 2 -A 5 | head -40

Repository: getAlby/js-sdk

Length of output: 92


🏁 Script executed:

# Look for how close() is typically used elsewhere in codebase
rg "\.close\(" --type=typescript | grep -E "(pool|relay)" | head -20

Repository: getAlby/js-sdk

Length of output: 92


🌐 Web query:

nostr-tools SimplePool close method async await behavior

💡 Result:

In nostr-tools / @nostr/tools, SimplePool.close(relays) is not async and returns void, so await pool.close(...) does not wait for anything. The implementation just normalizes each URL, calls the cached relay’s .close(), and removes it from the pool’s internal map. [1]

Under the hood, relay.close() is also synchronous: it marks the relay as intentionally closed, clears reconnect/ping timers, closes all subscriptions, flips connected to false, calls the relay’s onclose callback, and then initiates the WebSocket close (ws.close()) if the socket is open—without returning a Promise you can await. [2]

Implication: pool.close() only starts shutdown; it doesn’t provide an “await until sockets are fully closed” mechanism.
If you need an awaitable close in your code, you’ll have to build it yourself (e.g., track the WebSocket close event for each relay connection you created).

Sources
[1] AbstractSimplePool.close(relays): void implementation (JSR, abstract-pool.ts) (jsr.io)
[2] AbstractRelay.close(): void implementation (JSR, abstract-relay.ts) (jsr.io)

Citations:


Add timeout to WebSocket close promises and document reliance on private API.

Two concerns with this close() implementation:

  1. Accessing private internals: (this.pool as any).relays accesses SimplePool's internal Map. While nostr-tools exposes pool.close(relayUrls) as a public API, it is synchronous and does not wait for WebSocket close events—it only initiates them. Your manual event tracking is necessary to ensure connections fully close, but this fragile reliance on private API will break silently if the field is renamed or restructured. Document this as a workaround for nostr-tools' lack of awaitable close.

  2. No timeout on close promises: If a WebSocket close event never fires (e.g., network stall), await Promise.all(closePromises) hangs indefinitely. Add a timeout to each close promise:

Suggested timeout wrapper
       if (relay?.ws && relay.ws.readyState !== WebSocket.CLOSED) {
         closePromises.push(
-          new Promise<void>((resolve) => {
-            relay.ws?.addEventListener("close", () => resolve(), { once: true });
-          }),
+          new Promise<void>((resolve) => {
+            const timer = setTimeout(resolve, 5000);
+            relay.ws?.addEventListener("close", () => { clearTimeout(timer); resolve(); }, { once: true });
+          }),
         );
       }
🤖 Prompt for AI Agents
In `@src/nwc/NWCClient.ts` around lines 203 - 218, The close() method currently
reaches into SimplePool internals via (this.pool as any).relays to await
relay.ws "close" events but doesn’t guard against missing events; update close()
to wrap each close promise with a timeout (e.g., reject/resolve after
configurable ms) so Promise.all(closePromises) cannot hang indefinitely, and
ensure the timeout value is configurable/constant. Also add an inline comment
above the use of (this.pool as any).relays documenting that this is a deliberate
workaround for nostr-tools' synchronous pool.close() (since
pool.close(relayUrls) only initiates closes and does not await them) and mark
reliance on the private API as fragile so future maintainers know why it’s used.
Ensure references: close(), relayUrls, this.pool/(SimplePool).relays,
closePromises, and WebSocket close event are updated accordingly.

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.

Remove websocket-polyfill

2 participants